full refactor
This commit is contained in:
parent
1fe1137f80
commit
7d8c9be9e6
@ -1,39 +1,29 @@
|
||||
package cz.kamma.processmonitor;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import cz.kamma.processmonitor.config.AppConfig;
|
||||
import cz.kamma.processmonitor.handler.DashboardHandler;
|
||||
import cz.kamma.processmonitor.handler.DataApiHandler;
|
||||
import cz.kamma.processmonitor.handler.HeartbeatHandler;
|
||||
import cz.kamma.processmonitor.repository.ProcessRepository;
|
||||
import cz.kamma.processmonitor.service.ProcessService;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
public class Main {
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
AppConfig config = AppConfig.load();
|
||||
Database database = new Database(config);
|
||||
ProcessRepository repository = new ProcessRepository(config);
|
||||
ProcessService processService = new ProcessService();
|
||||
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", 8080), 0);
|
||||
server.createContext("/hb/api", new HeartbeatHandler(database));
|
||||
|
||||
server.createContext("/hb/api", new HeartbeatHandler(repository));
|
||||
server.createContext("/hb/dashboard", new DashboardHandler(config));
|
||||
server.createContext("/hb/api/data", new DataApiHandler(database, config));
|
||||
server.createContext("/hb/api/data", new DataApiHandler(config, repository, processService));
|
||||
|
||||
server.setExecutor(null);
|
||||
server.start();
|
||||
|
||||
@ -42,562 +32,7 @@ public class Main {
|
||||
log("API endpoint: http://127.0.0.1:8080/hb/api");
|
||||
}
|
||||
|
||||
private static final class HeartbeatHandler implements HttpHandler {
|
||||
private final Database database;
|
||||
|
||||
private HeartbeatHandler(Database database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
Headers headers = exchange.getRequestHeaders();
|
||||
String contentType = headers.getFirst("Content-Type");
|
||||
if (contentType == null || !contentType.toLowerCase().contains("application/json")) {
|
||||
sendJson(exchange, 415, "{\"error\":\"unsupported_media_type\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String body = readBody(exchange.getRequestBody());
|
||||
log("Incoming heartbeat request: " + body);
|
||||
HeartbeatRequest request = HeartbeatRequest.parse(body);
|
||||
int insertedRows = database.saveHeartbeat(request);
|
||||
log("Stored heartbeat for machine " + request.machineName + ", inserted rows: " + insertedRows);
|
||||
sendJson(exchange, 200, String.format("{\"ok\":true,\"inserted\":%d}", insertedRows));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log("Request validation failed: " + ex.getMessage());
|
||||
sendJson(exchange, 400, "{\"error\":\"" + escapeJson(ex.getMessage()) + "\"}");
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
log("Internal server error: " + ex.getMessage());
|
||||
sendJson(exchange, 500, "{\"error\":\"internal_error\"}");
|
||||
}
|
||||
}
|
||||
|
||||
private static String readBody(InputStream inputStream) throws IOException {
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||
outputStream.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AppConfig {
|
||||
private final String jdbcUrl;
|
||||
private final String dbUser;
|
||||
private final String dbPassword;
|
||||
private final String dashboardApiKey;
|
||||
|
||||
private AppConfig(String jdbcUrl, String dbUser, String dbPassword, String dashboardApiKey) {
|
||||
this.jdbcUrl = jdbcUrl;
|
||||
this.dbUser = dbUser;
|
||||
this.dbPassword = dbPassword;
|
||||
this.dashboardApiKey = dashboardApiKey;
|
||||
}
|
||||
|
||||
private static AppConfig load() throws IOException {
|
||||
Properties properties = new Properties();
|
||||
try (InputStream inputStream = Main.class.getClassLoader().getResourceAsStream("application.properties")) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Missing application.properties");
|
||||
}
|
||||
properties.load(inputStream);
|
||||
}
|
||||
|
||||
String jdbcUrl = required(properties, "db.url");
|
||||
String dbUser = required(properties, "db.user");
|
||||
String dbPassword = required(properties, "db.password");
|
||||
String dashboardApiKey = required(properties, "dashboard.apiKey");
|
||||
return new AppConfig(jdbcUrl, dbUser, dbPassword, dashboardApiKey);
|
||||
}
|
||||
|
||||
private static String required(Properties properties, String key) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing config key: " + key);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Database {
|
||||
private final AppConfig config;
|
||||
|
||||
private Database(AppConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private int saveHeartbeat(HeartbeatRequest request) throws SQLException {
|
||||
List<String> processes = request.processes.isEmpty() ? Collections.singletonList(null) : request.processes;
|
||||
String sql = "INSERT INTO process_heartbeat (machine_name, status, detected_at, process_name) VALUES (?, ?, ?, ?)";
|
||||
|
||||
try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword);
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
int inserted = 0;
|
||||
for (String processName : processes) {
|
||||
statement.setString(1, request.machineName);
|
||||
statement.setString(2, request.status);
|
||||
statement.setTimestamp(3, Timestamp.from(request.detectedAt.atZone(ZoneId.systemDefault()).toInstant()));
|
||||
statement.setString(4, processName);
|
||||
inserted += statement.executeUpdate();
|
||||
}
|
||||
return inserted;
|
||||
}
|
||||
}
|
||||
|
||||
private FilterOptions getFilterOptions() throws SQLException {
|
||||
String sqlMachines = "SELECT DISTINCT machine_name FROM process_heartbeat ORDER BY machine_name";
|
||||
String sqlProcesses = "SELECT DISTINCT process_name FROM process_heartbeat WHERE process_name IS NOT NULL ORDER BY process_name";
|
||||
String sqlStatuses = "SELECT DISTINCT status FROM process_heartbeat ORDER BY status";
|
||||
|
||||
try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword)) {
|
||||
List<String> machines = new java.util.ArrayList<>();
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sqlMachines);
|
||||
java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
machines.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> processes = new java.util.ArrayList<>();
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sqlProcesses);
|
||||
java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
processes.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> statuses = new java.util.ArrayList<>();
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sqlStatuses);
|
||||
java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
statuses.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
|
||||
return new FilterOptions(machines, processes, statuses);
|
||||
}
|
||||
}
|
||||
|
||||
private String extractMainName(String processName, java.util.List<String> allProcessNames) {
|
||||
if (processName == null || processName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
// Odstranit .exe
|
||||
String name = processName;
|
||||
int dotIndex = name.lastIndexOf('.');
|
||||
if (dotIndex > 0) {
|
||||
name = name.substring(0, dotIndex);
|
||||
}
|
||||
|
||||
// Najít všechny unikátní názvy bez .exe
|
||||
java.util.Set<String> uniqueNames = new java.util.TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
||||
for (String procName : allProcessNames) {
|
||||
if (procName == null || procName.isBlank()) continue;
|
||||
int dotIdx = procName.lastIndexOf('.');
|
||||
if (dotIdx > 0) {
|
||||
uniqueNames.add(procName.substring(0, dotIdx));
|
||||
} else {
|
||||
uniqueNames.add(procName);
|
||||
}
|
||||
}
|
||||
|
||||
// Najít skupinu procesů, které patří k tomuto (mají společnou předponu)
|
||||
java.util.List<String> group = new java.util.ArrayList<>();
|
||||
for (String n : uniqueNames) {
|
||||
// Zkontrolovat, zda název začíná na stejný základ jako target nebo naopak
|
||||
if (hasCommonPrefix(name, n)) {
|
||||
group.add(n);
|
||||
}
|
||||
}
|
||||
|
||||
// Pokud je jen jeden v group, vrať ho
|
||||
if (group.size() == 1) {
|
||||
return group.get(0);
|
||||
}
|
||||
|
||||
// Najít nejkratší společnou předponu pro tuto skupinu
|
||||
group.sort((a, b) -> Integer.compare(a.length(), b.length()));
|
||||
String shortest = group.get(0);
|
||||
|
||||
while (shortest.length() > 0) {
|
||||
boolean isPrefixOfAll = true;
|
||||
for (String n : group) {
|
||||
if (!n.toLowerCase().startsWith(shortest.toLowerCase())) {
|
||||
isPrefixOfAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isPrefixOfAll) {
|
||||
return shortest;
|
||||
}
|
||||
shortest = shortest.substring(0, shortest.length() - 1);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private boolean hasCommonPrefix(String a, String b) {
|
||||
// Dva názvy patří do stejné skupiny, pokud jeden začíná na druhý
|
||||
// nebo mají společnou předponu alespoň 3 znaky
|
||||
return a.toLowerCase().startsWith(b.toLowerCase())
|
||||
|| b.toLowerCase().startsWith(a.toLowerCase())
|
||||
|| getCommonPrefix(a, b).length() >= 3;
|
||||
}
|
||||
|
||||
private String getCommonPrefix(String a, String b) {
|
||||
int minLen = Math.min(a.length(), b.length());
|
||||
int i = 0;
|
||||
while (i < minLen && a.toLowerCase().charAt(i) == b.toLowerCase().charAt(i)) {
|
||||
i++;
|
||||
}
|
||||
return a.substring(0, i);
|
||||
}
|
||||
|
||||
private StatsResponse getStats(String machine, String process, String status, String from, String to) throws SQLException {
|
||||
StringBuilder sql = new StringBuilder("SELECT id, machine_name, status, detected_at, process_name FROM process_heartbeat WHERE 1=1");
|
||||
java.util.List<String> params = new java.util.ArrayList<>();
|
||||
|
||||
if (machine != null && !machine.isBlank()) {
|
||||
sql.append(" AND machine_name = ?");
|
||||
params.add(machine);
|
||||
}
|
||||
if (process != null && !process.isBlank()) {
|
||||
sql.append(" AND process_name = ?");
|
||||
params.add(process);
|
||||
}
|
||||
if (status != null && !status.isBlank()) {
|
||||
sql.append(" AND status = ?");
|
||||
params.add(status);
|
||||
}
|
||||
if (from != null && !from.isBlank()) {
|
||||
sql.append(" AND detected_at >= ?");
|
||||
params.add(from);
|
||||
}
|
||||
if (to != null && !to.isBlank()) {
|
||||
sql.append(" AND detected_at <= ?");
|
||||
params.add(to);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY detected_at DESC LIMIT 10000");
|
||||
|
||||
try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword);
|
||||
PreparedStatement stmt = connection.prepareStatement(sql.toString())) {
|
||||
|
||||
for (int i = 0; i < params.size(); i++) {
|
||||
stmt.setString(i + 1, params.get(i));
|
||||
}
|
||||
|
||||
// Nejprve načíst všechny unikátní názvy procesů pro určení hlavního názvu
|
||||
java.util.List<String> allProcessNames = new java.util.ArrayList<>();
|
||||
try (PreparedStatement allStmt = connection.prepareStatement(
|
||||
"SELECT DISTINCT process_name FROM process_heartbeat WHERE process_name IS NOT NULL")) {
|
||||
try (java.sql.ResultSet rs = allStmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
allProcessNames.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
java.util.List<Record> records = new java.util.ArrayList<>();
|
||||
try (java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
String processName = rs.getString(5);
|
||||
String mainName = extractMainName(processName, allProcessNames);
|
||||
records.add(new Record(
|
||||
rs.getLong(1),
|
||||
rs.getString(2),
|
||||
rs.getString(3),
|
||||
rs.getTimestamp(4),
|
||||
processName,
|
||||
mainName
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return new StatsResponse(records);
|
||||
}
|
||||
}
|
||||
|
||||
private LastRecordTimeResponse getLastRecordTime(String date) throws SQLException {
|
||||
String sql = "SELECT detected_at FROM process_heartbeat WHERE DATE(detected_at) = ? ORDER BY detected_at DESC LIMIT 1";
|
||||
|
||||
try (Connection connection = DriverManager.getConnection(config.jdbcUrl, config.dbUser, config.dbPassword);
|
||||
PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
|
||||
if (date != null && !date.isBlank()) {
|
||||
stmt.setString(1, date);
|
||||
} else {
|
||||
stmt.setString(1, java.time.LocalDate.now(ZoneId.systemDefault()).toString());
|
||||
}
|
||||
|
||||
try (java.sql.ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
Timestamp ts = rs.getTimestamp(1);
|
||||
return new LastRecordTimeResponse(ts.toInstant().toString());
|
||||
}
|
||||
return new LastRecordTimeResponse(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FilterOptions {
|
||||
List<String> machines;
|
||||
List<String> processes;
|
||||
List<String> statuses;
|
||||
|
||||
FilterOptions(List<String> machines, List<String> processes, List<String> statuses) {
|
||||
this.machines = machines;
|
||||
this.processes = processes;
|
||||
this.statuses = statuses;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Record {
|
||||
long id;
|
||||
String machine_name;
|
||||
String status;
|
||||
String detected_at;
|
||||
String process_name;
|
||||
String main_name;
|
||||
|
||||
Record(long id, String machine_name, String status, Timestamp detected_at, String process_name, String main_name) {
|
||||
this.id = id;
|
||||
this.machine_name = machine_name;
|
||||
this.status = status;
|
||||
this.detected_at = detected_at != null ? detected_at.toInstant().toString() : null;
|
||||
this.process_name = process_name;
|
||||
this.main_name = main_name;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StatsResponse {
|
||||
List<Record> records;
|
||||
|
||||
StatsResponse(List<Record> records) {
|
||||
this.records = records;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LastRecordTimeResponse {
|
||||
String lastRecordTime;
|
||||
|
||||
LastRecordTimeResponse(String lastRecordTime) {
|
||||
this.lastRecordTime = lastRecordTime;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HeartbeatRequest {
|
||||
private String machine_name;
|
||||
private String status;
|
||||
private String detected_at;
|
||||
private List<String> processes;
|
||||
|
||||
private transient String machineName;
|
||||
private transient Instant detectedAt;
|
||||
|
||||
private static HeartbeatRequest parse(String json) {
|
||||
try {
|
||||
HeartbeatRequest request = GSON.fromJson(json, HeartbeatRequest.class);
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("Empty request body");
|
||||
}
|
||||
request.validate();
|
||||
return request;
|
||||
} catch (JsonSyntaxException ex) {
|
||||
throw new IllegalArgumentException("Invalid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
machineName = required(machine_name, "machine_name");
|
||||
status = required(status, "status");
|
||||
try {
|
||||
detectedAt = Instant.parse(required(detected_at, "detected_at"));
|
||||
} catch (DateTimeParseException ex) {
|
||||
throw new IllegalArgumentException("Invalid detected_at");
|
||||
}
|
||||
if (processes == null) {
|
||||
processes = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private static String required(String value, String fieldName) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing field: " + fieldName);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DashboardHandler implements HttpHandler {
|
||||
private final AppConfig config;
|
||||
|
||||
private DashboardHandler(AppConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
sendHtml(exchange, 405, "<h1>Method Not Allowed</h1>");
|
||||
return;
|
||||
}
|
||||
|
||||
String query = exchange.getRequestURI().getQuery();
|
||||
String apiKey = getParam(query, "apiKey");
|
||||
|
||||
if (apiKey == null || apiKey.isBlank()) {
|
||||
String html = loadResource("login.html");
|
||||
sendHtml(exchange, 200, html);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKey.equals(config.dashboardApiKey)) {
|
||||
String html = loadResource("error.html").replace("%MESSAGE%", escapeHtml("Neplatný API klíč"));
|
||||
sendHtml(exchange, 401, html);
|
||||
return;
|
||||
}
|
||||
|
||||
String html = loadResource("dashboard.html").replace("%API_KEY%", apiKey);
|
||||
sendHtml(exchange, 200, html);
|
||||
}
|
||||
|
||||
private static String getParam(String query, String name) {
|
||||
if (query == null) return null;
|
||||
String[] pairs = query.split("&");
|
||||
for (String pair : pairs) {
|
||||
String[] parts = pair.split("=", 2);
|
||||
if (parts.length == 2 && parts[0].equals(name)) {
|
||||
try {
|
||||
return java.net.URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String loadResource(String resourceName) throws IOException {
|
||||
try (InputStream inputStream = Main.class.getClassLoader().getResourceAsStream(resourceName)) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Resource not found: " + resourceName);
|
||||
}
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendHtml(HttpExchange exchange, int statusCode, String html) throws IOException {
|
||||
byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||
outputStream.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DataApiHandler implements HttpHandler {
|
||||
private final Database database;
|
||||
private final AppConfig config;
|
||||
|
||||
private DataApiHandler(Database database, AppConfig config) {
|
||||
this.database = database;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String query = exchange.getRequestURI().getQuery();
|
||||
String apiKey = getParam(query, "apiKey");
|
||||
|
||||
if (apiKey == null || apiKey.isBlank() || !apiKey.equals(config.dashboardApiKey)) {
|
||||
sendJson(exchange, 401, "{\"error\":\"unauthorized\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String type = getParam(query, "type");
|
||||
|
||||
if ("filters".equals(type)) {
|
||||
String response = GSON.toJson(database.getFilterOptions());
|
||||
sendJson(exchange, 200, response);
|
||||
} else if ("lastRecordTime".equals(type)) {
|
||||
String date = getParam(query, "date");
|
||||
String response = GSON.toJson(database.getLastRecordTime(date));
|
||||
sendJson(exchange, 200, response);
|
||||
} else if ("stats".equals(type)) {
|
||||
String machine = getParam(query, "machine");
|
||||
String process = getParam(query, "process");
|
||||
String status = getParam(query, "status");
|
||||
String from = getParam(query, "from");
|
||||
String to = getParam(query, "to");
|
||||
|
||||
String response = GSON.toJson(database.getStats(machine, process, status, from, to));
|
||||
sendJson(exchange, 200, response);
|
||||
} else {
|
||||
sendJson(exchange, 400, "{\"error\":\"invalid_type\"}");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
sendJson(exchange, 500, "{\"error\":\"internal_error\"}");
|
||||
}
|
||||
}
|
||||
|
||||
private static String getParam(String query, String name) {
|
||||
if (query == null) return null;
|
||||
String[] pairs = query.split("&");
|
||||
for (String pair : pairs) {
|
||||
String[] parts = pair.split("=", 2);
|
||||
if (parts.length == 2 && parts[0].equals(name)) {
|
||||
try {
|
||||
return java.net.URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||
outputStream.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void log(String message) {
|
||||
System.out.printf("[%s] %s%n", Instant.now(), message);
|
||||
}
|
||||
|
||||
private static String escapeJson(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private static String escapeHtml(String value) {
|
||||
return value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """).replace("'", "'");
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
package cz.kamma.processmonitor.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
|
||||
public class AppConfig {
|
||||
private final String jdbcUrl;
|
||||
private final String dbUser;
|
||||
private final String dbPassword;
|
||||
private final String dashboardApiKey;
|
||||
|
||||
public AppConfig(String jdbcUrl, String dbUser, String dbPassword, String dashboardApiKey) {
|
||||
this.jdbcUrl = jdbcUrl;
|
||||
this.dbUser = dbUser;
|
||||
this.dbPassword = dbPassword;
|
||||
this.dashboardApiKey = dashboardApiKey;
|
||||
}
|
||||
|
||||
public String getJdbcUrl() {
|
||||
return jdbcUrl;
|
||||
}
|
||||
|
||||
public String getDbUser() {
|
||||
return dbUser;
|
||||
}
|
||||
|
||||
public String getDbPassword() {
|
||||
return dbPassword;
|
||||
}
|
||||
|
||||
public String getDashboardApiKey() {
|
||||
return dashboardApiKey;
|
||||
}
|
||||
|
||||
public static AppConfig load() throws IOException {
|
||||
Properties properties = new Properties();
|
||||
try (InputStream inputStream = AppConfig.class.getClassLoader().getResourceAsStream("application.properties")) {
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Missing application.properties");
|
||||
}
|
||||
properties.load(inputStream);
|
||||
}
|
||||
|
||||
String jdbcUrl = required(properties, "db.url");
|
||||
String dbUser = required(properties, "db.user");
|
||||
String dbPassword = required(properties, "db.password");
|
||||
String dashboardApiKey = required(properties, "dashboard.apiKey");
|
||||
return new AppConfig(jdbcUrl, dbUser, dbPassword, dashboardApiKey);
|
||||
}
|
||||
|
||||
private static String required(Properties properties, String key) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing config key: " + key);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
package cz.kamma.processmonitor.handler;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import cz.kamma.processmonitor.config.AppConfig;
|
||||
import cz.kamma.processmonitor.model.FilterOptions;
|
||||
import cz.kamma.processmonitor.model.LastRecordTimeResponse;
|
||||
import cz.kamma.processmonitor.model.Record;
|
||||
import cz.kamma.processmonitor.model.StatsResponse;
|
||||
import cz.kamma.processmonitor.repository.ProcessRepository;
|
||||
import cz.kamma.processmonitor.service.ProcessService;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DashboardHandler implements HttpHandler {
|
||||
private final AppConfig config;
|
||||
|
||||
public DashboardHandler(AppConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
sendHtml(exchange, 405, "<h1>Method Not Allowed</h1>");
|
||||
return;
|
||||
}
|
||||
|
||||
String query = exchange.getRequestURI().getQuery();
|
||||
String apiKey = getParam(query, "apiKey");
|
||||
|
||||
if (apiKey == null || apiKey.isBlank()) {
|
||||
String html = loadResource("login.html");
|
||||
sendHtml(exchange, 200, html);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKey.equals(config.getDashboardApiKey())) {
|
||||
String html = loadResource("error.html").replace("%MESSAGE%", "Neplatný API klíč");
|
||||
sendHtml(exchange, 401, html);
|
||||
return;
|
||||
}
|
||||
|
||||
String html = loadResource("dashboard.html").replace("%API_KEY%", apiKey);
|
||||
sendHtml(exchange, 200, html);
|
||||
}
|
||||
|
||||
private String loadResource(String resourceName) throws IOException {
|
||||
try (java.io.InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourceName)) {
|
||||
if (inputStream == null) {
|
||||
try (java.io.InputStream fallbackStream = getClass().getClassLoader().getResourceAsStream(resourceName)) {
|
||||
if (fallbackStream == null) {
|
||||
throw new IOException("Resource not found: " + resourceName);
|
||||
}
|
||||
return new String(fallbackStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendHtml(HttpExchange exchange, int statusCode, String html) throws IOException {
|
||||
byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||
outputStream.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private String getParam(String query, String name) {
|
||||
if (query == null) return null;
|
||||
String[] pairs = query.split("&");
|
||||
for (String pair : pairs) {
|
||||
String[] parts = pair.split("=", 2);
|
||||
if (parts.length == 2 && parts[0].equals(name)) {
|
||||
try {
|
||||
return java.net.URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package cz.kamma.processmonitor.handler;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import cz.kamma.processmonitor.config.AppConfig;
|
||||
import cz.kamma.processmonitor.model.FilterOptions;
|
||||
import cz.kamma.processmonitor.model.LastRecordTimeResponse;
|
||||
import cz.kamma.processmonitor.model.Record;
|
||||
import cz.kamma.processmonitor.model.StatsResponse;
|
||||
import cz.kamma.processmonitor.repository.ProcessRepository;
|
||||
import cz.kamma.processmonitor.service.ProcessService;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
public class DataApiHandler implements HttpHandler {
|
||||
private static final Gson GSON = new Gson();
|
||||
private final AppConfig config;
|
||||
private final ProcessRepository repository;
|
||||
private final ProcessService processService;
|
||||
|
||||
public DataApiHandler(AppConfig config, ProcessRepository repository, ProcessService processService) {
|
||||
this.config = config;
|
||||
this.repository = repository;
|
||||
this.processService = processService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String query = exchange.getRequestURI().getQuery();
|
||||
String apiKey = getParam(query, "apiKey");
|
||||
|
||||
if (apiKey == null || apiKey.isBlank() || !apiKey.equals(config.getDashboardApiKey())) {
|
||||
sendJson(exchange, 401, "{\"error\":\"unauthorized\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String type = getParam(query, "type");
|
||||
|
||||
if ("filters".equals(type)) {
|
||||
String response = GSON.toJson(repository.getFilterOptions());
|
||||
sendJson(exchange, 200, response);
|
||||
} else if ("lastRecordTime".equals(type)) {
|
||||
String date = getParam(query, "date");
|
||||
String response = GSON.toJson(repository.getLastRecordTime(date));
|
||||
sendJson(exchange, 200, response);
|
||||
} else if ("stats".equals(type)) {
|
||||
String machine = getParam(query, "machine");
|
||||
String process = getParam(query, "process");
|
||||
String status = getParam(query, "status");
|
||||
String from = getParam(query, "from");
|
||||
String to = getParam(query, "to");
|
||||
|
||||
StatsResponse stats = repository.getStats(machine, process, status, from, to);
|
||||
|
||||
List<String> allProcessNames = repository.getAllProcessNames();
|
||||
for (Record record : stats.records) {
|
||||
record.main_name = processService.extractMainName(record.process_name, allProcessNames);
|
||||
}
|
||||
|
||||
String response = GSON.toJson(stats);
|
||||
sendJson(exchange, 200, response);
|
||||
} else {
|
||||
sendJson(exchange, 400, "{\"error\":\"invalid_type\"}");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
sendJson(exchange, 500, "{\"error\":\"internal_error\"}");
|
||||
}
|
||||
}
|
||||
|
||||
private String getParam(String query, String name) {
|
||||
if (query == null) return null;
|
||||
String[] pairs = query.split("&");
|
||||
for (String pair : pairs) {
|
||||
String[] parts = pair.split("=", 2);
|
||||
if (parts.length == 2 && parts[0].equals(name)) {
|
||||
try {
|
||||
return java.net.URLDecoder.decode(parts[1], StandardCharsets.UTF_8);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||
outputStream.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package cz.kamma.processmonitor.handler;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import cz.kamma.processmonitor.config.AppConfig;
|
||||
import cz.kamma.processmonitor.model.HeartbeatRequest;
|
||||
import cz.kamma.processmonitor.repository.ProcessRepository;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class HeartbeatHandler implements HttpHandler {
|
||||
private static final Logger LOGGER = Logger.getLogger(HeartbeatHandler.class.getName());
|
||||
private final ProcessRepository repository;
|
||||
|
||||
public HeartbeatHandler(ProcessRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try {
|
||||
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||
sendJson(exchange, 405, "{\"error\":\"method_not_allowed\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
|
||||
if (contentType == null || !contentType.toLowerCase().contains("application/json")) {
|
||||
sendJson(exchange, 415, "{\"error\":\"unsupported_media_type\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
String body = readBody(exchange.getRequestBody());
|
||||
LOGGER.info("Incoming heartbeat request: " + body);
|
||||
HeartbeatRequest request = HeartbeatRequest.parse(body);
|
||||
|
||||
int insertedRows = repository.saveHeartbeat(
|
||||
request.getMachineName(),
|
||||
request.getStatus(),
|
||||
request.getDetectedAt(),
|
||||
request.getProcesses()
|
||||
);
|
||||
|
||||
LOGGER.info("Stored heartbeat for machine " + request.getMachineName() + ", inserted rows: " + insertedRows);
|
||||
sendJson(exchange, 200, String.format("{\"ok\":true,\"inserted\":%d}", insertedRows));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
LOGGER.warning("Request validation failed: " + ex.getMessage());
|
||||
sendJson(exchange, 400, "{\"error\":\"" + escapeJson(ex.getMessage()) + "\"}");
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
LOGGER.severe("Internal server error: " + ex.getMessage());
|
||||
sendJson(exchange, 500, "{\"error\":\"internal_error\"}");
|
||||
}
|
||||
}
|
||||
|
||||
private String readBody(InputStream inputStream) throws IOException {
|
||||
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private void sendJson(HttpExchange exchange, int statusCode, String body) throws IOException {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(statusCode, bytes.length);
|
||||
try (OutputStream outputStream = exchange.getResponseBody()) {
|
||||
outputStream.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private String escapeJson(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package cz.kamma.processmonitor.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FilterOptions {
|
||||
public List<String> machines;
|
||||
public List<String> processes;
|
||||
public List<String> statuses;
|
||||
|
||||
public FilterOptions(List<String> machines, List<String> processes, List<String> statuses) {
|
||||
this.machines = machines;
|
||||
this.processes = processes;
|
||||
this.statuses = statuses;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package cz.kamma.processmonitor.model;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import java.time.Instant;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class HeartbeatRequest {
|
||||
private String machine_name;
|
||||
private String status;
|
||||
private String detected_at;
|
||||
private List<String> processes;
|
||||
|
||||
private transient String machineName;
|
||||
private transient Instant detectedAt;
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
public String getMachine_name() {
|
||||
return machine_name;
|
||||
}
|
||||
|
||||
public void setMachine_name(String machine_name) {
|
||||
this.machine_name = machine_name;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getDetected_at() {
|
||||
return detected_at;
|
||||
}
|
||||
|
||||
public void setDetected_at(String detected_at) {
|
||||
this.detected_at = detected_at;
|
||||
}
|
||||
|
||||
public List<String> getProcesses() {
|
||||
return processes;
|
||||
}
|
||||
|
||||
public void setProcesses(List<String> processes) {
|
||||
this.processes = processes;
|
||||
}
|
||||
|
||||
public String getMachineName() {
|
||||
return machineName;
|
||||
}
|
||||
|
||||
public void setMachineName(String machineName) {
|
||||
this.machineName = machineName;
|
||||
}
|
||||
|
||||
public Instant getDetectedAt() {
|
||||
return detectedAt;
|
||||
}
|
||||
|
||||
public void setDetectedAt(Instant detectedAt) {
|
||||
this.detectedAt = detectedAt;
|
||||
}
|
||||
|
||||
public static HeartbeatRequest parse(String json) {
|
||||
try {
|
||||
HeartbeatRequest request = GSON.fromJson(json, HeartbeatRequest.class);
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("Empty request body");
|
||||
}
|
||||
request.validate();
|
||||
return request;
|
||||
} catch (JsonSyntaxException ex) {
|
||||
throw new IllegalArgumentException("Invalid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
public void validate() {
|
||||
machineName = required(machine_name, "machine_name");
|
||||
status = required(status, "status");
|
||||
try {
|
||||
detectedAt = Instant.parse(required(detected_at, "detected_at"));
|
||||
} catch (DateTimeParseException ex) {
|
||||
throw new IllegalArgumentException("Invalid detected_at");
|
||||
}
|
||||
if (processes == null) {
|
||||
processes = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private String required(String value, String fieldName) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing field: " + fieldName);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package cz.kamma.processmonitor.model;
|
||||
|
||||
public class LastRecordTimeResponse {
|
||||
public String lastRecordTime;
|
||||
|
||||
public LastRecordTimeResponse(String lastRecordTime) {
|
||||
this.lastRecordTime = lastRecordTime;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package cz.kamma.processmonitor.model;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
|
||||
public class Record {
|
||||
public long id;
|
||||
public String machine_name;
|
||||
public String status;
|
||||
public String detected_at;
|
||||
public String process_name;
|
||||
public String main_name;
|
||||
|
||||
public Record(long id, String machine_name, String status, Timestamp detected_at, String process_name, String main_name) {
|
||||
this.id = id;
|
||||
this.machine_name = machine_name;
|
||||
this.status = status;
|
||||
this.detected_at = detected_at != null ? detected_at.toInstant().toString() : null;
|
||||
this.process_name = process_name;
|
||||
this.main_name = main_name;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package cz.kamma.processmonitor.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class StatsResponse {
|
||||
public List<Record> records;
|
||||
|
||||
public StatsResponse(List<Record> records) {
|
||||
this.records = records;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,170 @@
|
||||
package cz.kamma.processmonitor.repository;
|
||||
|
||||
import cz.kamma.processmonitor.config.AppConfig;
|
||||
import cz.kamma.processmonitor.model.FilterOptions;
|
||||
import cz.kamma.processmonitor.model.LastRecordTimeResponse;
|
||||
import cz.kamma.processmonitor.model.Record;
|
||||
import cz.kamma.processmonitor.model.StatsResponse;
|
||||
|
||||
import java.sql.*;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ProcessRepository {
|
||||
private final AppConfig config;
|
||||
|
||||
public ProcessRepository(AppConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
private Connection getConnection() throws SQLException {
|
||||
return DriverManager.getConnection(config.getJdbcUrl(), config.getDbUser(), config.getDbPassword());
|
||||
}
|
||||
|
||||
public int saveHeartbeat(String machineName, String status, Instant detectedAt, List<String> processes) throws SQLException {
|
||||
List<String> processList = processes.isEmpty() ? Collections.singletonList(null) : processes;
|
||||
String sql = "INSERT INTO process_heartbeat (machine_name, status, detected_at, process_name) VALUES (?, ?, ?, ?)";
|
||||
|
||||
try (Connection connection = getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||
int inserted = 0;
|
||||
Timestamp timestamp = Timestamp.from(detectedAt.atZone(ZoneId.systemDefault()).toInstant());
|
||||
for (String processName : processList) {
|
||||
statement.setString(1, machineName);
|
||||
statement.setString(2, status);
|
||||
statement.setTimestamp(3, timestamp);
|
||||
statement.setString(4, processName);
|
||||
inserted += statement.executeUpdate();
|
||||
}
|
||||
return inserted;
|
||||
}
|
||||
}
|
||||
|
||||
public FilterOptions getFilterOptions() throws SQLException {
|
||||
String sqlMachines = "SELECT DISTINCT machine_name FROM process_heartbeat ORDER BY machine_name";
|
||||
String sqlProcesses = "SELECT DISTINCT process_name FROM process_heartbeat WHERE process_name IS NOT NULL ORDER BY process_name";
|
||||
String sqlStatuses = "SELECT DISTINCT status FROM process_heartbeat ORDER BY status";
|
||||
|
||||
try (Connection connection = getConnection()) {
|
||||
List<String> machines = new ArrayList<>();
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sqlMachines);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
machines.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> processes = new ArrayList<>();
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sqlProcesses);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
processes.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> statuses = new ArrayList<>();
|
||||
try (PreparedStatement stmt = connection.prepareStatement(sqlStatuses);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
statuses.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
|
||||
return new FilterOptions(machines, processes, statuses);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getAllProcessNames() throws SQLException {
|
||||
String sql = "SELECT DISTINCT process_name FROM process_heartbeat WHERE process_name IS NOT NULL";
|
||||
List<String> processNames = new ArrayList<>();
|
||||
try (Connection connection = getConnection();
|
||||
PreparedStatement stmt = connection.prepareStatement(sql);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
processNames.add(rs.getString(1));
|
||||
}
|
||||
}
|
||||
return processNames;
|
||||
}
|
||||
|
||||
public StatsResponse getStats(String machine, String process, String status, String from, String to) throws SQLException {
|
||||
StringBuilder sql = new StringBuilder("SELECT id, machine_name, status, detected_at, process_name FROM process_heartbeat WHERE 1=1");
|
||||
List<String> params = new ArrayList<>();
|
||||
|
||||
if (machine != null && !machine.isBlank()) {
|
||||
sql.append(" AND machine_name = ?");
|
||||
params.add(machine);
|
||||
}
|
||||
if (process != null && !process.isBlank()) {
|
||||
sql.append(" AND process_name = ?");
|
||||
params.add(process);
|
||||
}
|
||||
if (status != null && !status.isBlank()) {
|
||||
sql.append(" AND status = ?");
|
||||
params.add(status);
|
||||
}
|
||||
if (from != null && !from.isBlank()) {
|
||||
sql.append(" AND detected_at >= ?");
|
||||
params.add(from);
|
||||
}
|
||||
if (to != null && !to.isBlank()) {
|
||||
sql.append(" AND detected_at <= ?");
|
||||
params.add(to);
|
||||
}
|
||||
|
||||
sql.append(" ORDER BY detected_at DESC LIMIT 10000");
|
||||
|
||||
try (Connection connection = getConnection();
|
||||
PreparedStatement stmt = connection.prepareStatement(sql.toString())) {
|
||||
|
||||
for (int i = 0; i < params.size(); i++) {
|
||||
stmt.setString(i + 1, params.get(i));
|
||||
}
|
||||
|
||||
List<Record> records = new ArrayList<>();
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
long id = rs.getLong(1);
|
||||
String mName = rs.getString(2);
|
||||
String st = rs.getString(3);
|
||||
Timestamp dt = rs.getTimestamp(4);
|
||||
String pName = rs.getString(5);
|
||||
|
||||
// We will fill main_name in the service layer or here
|
||||
// To keep repository clean, we'll return the raw data first
|
||||
// But the current design expects main_name in Record.
|
||||
// I'll add a placeholder and let the service layer fill it if needed,
|
||||
// or just return the raw fields.
|
||||
// Actually, let's just use a temporary way to store it.
|
||||
records.add(new Record(id, mName, st, dt, pName, null));
|
||||
}
|
||||
}
|
||||
return new StatsResponse(records);
|
||||
}
|
||||
}
|
||||
|
||||
public LastRecordTimeResponse getLastRecordTime(String date) throws SQLException {
|
||||
String sql = "SELECT detected_at FROM process_heartbeat WHERE DATE(detected_at) = ? ORDER BY detected_at DESC LIMIT 1";
|
||||
|
||||
try (Connection connection = getConnection();
|
||||
PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
|
||||
if (date != null && !date.isBlank()) {
|
||||
stmt.setString(1, date);
|
||||
} else {
|
||||
stmt.setString(1, java.time.LocalDate.now(ZoneId.systemDefault()).toString());
|
||||
}
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
Timestamp ts = rs.getTimestamp(1);
|
||||
return new LastRecordTimeResponse(ts.toInstant().toString());
|
||||
}
|
||||
return new LastRecordTimeResponse(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package cz.kamma.processmonitor.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class ProcessService {
|
||||
|
||||
public String extractMainName(String processName, List<String> allProcessNames) {
|
||||
if (processName == null || processName.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String name = processName;
|
||||
int dotIndex = name.lastIndexOf('.');
|
||||
if (dotIndex > 0) {
|
||||
name = name.substring(0, dotIndex);
|
||||
}
|
||||
|
||||
Set<String> uniqueNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
||||
for (String procName : allProcessNames) {
|
||||
if (procName == null || procName.isBlank()) continue;
|
||||
int dotIdx = procName.lastIndexOf('.');
|
||||
if (dotIdx > 0) {
|
||||
uniqueNames.add(procName.substring(0, dotIdx));
|
||||
} else {
|
||||
uniqueNames.add(procName);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> group = new ArrayList<>();
|
||||
for (String n : uniqueNames) {
|
||||
if (hasCommonPrefix(name, n)) {
|
||||
group.add(n);
|
||||
}
|
||||
}
|
||||
|
||||
if (group.size() == 1) {
|
||||
return group.get(0);
|
||||
}
|
||||
|
||||
group.sort((a, b) -> Integer.compare(a.length(), b.length()));
|
||||
String shortest = group.get(0);
|
||||
|
||||
while (shortest.length() > 0) {
|
||||
boolean isPrefixOfAll = true;
|
||||
for (String n : group) {
|
||||
if (!n.toLowerCase().startsWith(shortest.toLowerCase())) {
|
||||
isPrefixOfAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isPrefixOfAll) {
|
||||
return shortest;
|
||||
}
|
||||
shortest = shortest.substring(0, shortest.length() - 1);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private boolean hasCommonPrefix(String a, String b) {
|
||||
return a.toLowerCase().startsWith(b.toLowerCase())
|
||||
|| b.toLowerCase().startsWith(a.toLowerCase())
|
||||
|| getCommonPrefix(a, b).length() >= 3;
|
||||
}
|
||||
|
||||
private String getCommonPrefix(String a, String b) {
|
||||
int minLen = Math.min(a.length(), b.length());
|
||||
int i = 0;
|
||||
while (i < minLen && a.toLowerCase().charAt(i) == b.toLowerCase().charAt(i)) {
|
||||
i++;
|
||||
}
|
||||
return a.substring(0, i);
|
||||
}
|
||||
}
|
||||
@ -330,29 +330,27 @@
|
||||
loadLastRecordTime();
|
||||
const records = data.records;
|
||||
|
||||
// Agregace času podle procesů
|
||||
const processTimeMap = {};
|
||||
|
||||
// Seřadit záznamy podle procesu a času
|
||||
const sortedRecords = records.sort((a, b) =>
|
||||
new Date(a.detected_at) - new Date(b.detected_at)
|
||||
);
|
||||
|
||||
// Seskupit podle hlavního názvu a spočítat časy
|
||||
const processByName = {};
|
||||
sortedRecords.forEach(r => {
|
||||
const mainName = r.main_name || r.process_name || '(bez procesu)';
|
||||
if (!processByName[mainName]) {
|
||||
processByName[mainName] = [];
|
||||
}
|
||||
processByName[mainName].push(r);
|
||||
// Agregace času podle procesů: pro každý hlavní název se zobrazí čas nejdelšího podprocesu
|
||||
const processCounts = {};
|
||||
records.forEach(r => {
|
||||
const pName = r.process_name || '(bez procesu)';
|
||||
processCounts[pName] = (processCounts[pName] || 0) + 1;
|
||||
});
|
||||
|
||||
// Spočítat čas běhu pro každý hlavní název (45s za každý záznam)
|
||||
Object.keys(processByName).forEach(mainName => {
|
||||
const records = processByName[mainName];
|
||||
const totalSeconds = records.length * 45;
|
||||
processTimeMap[mainName] = totalSeconds / 60;
|
||||
const mainNameToMaxCount = {};
|
||||
records.forEach(r => {
|
||||
const mainName = r.main_name || r.process_name || '(bez procesu)';
|
||||
const pName = r.process_name || '(bez procesu)';
|
||||
const count = processCounts[pName];
|
||||
if (!mainNameToMaxCount[mainName] || count > mainNameToMaxCount[mainName]) {
|
||||
mainNameToMaxCount[mainName] = count;
|
||||
}
|
||||
});
|
||||
|
||||
const processTimeMap = {};
|
||||
Object.keys(mainNameToMaxCount).forEach(mainName => {
|
||||
const maxCount = mainNameToMaxCount[mainName];
|
||||
processTimeMap[mainName] = (maxCount * 45) / 60;
|
||||
});
|
||||
|
||||
if (processTimeChart) processTimeChart.destroy();
|
||||
@ -362,9 +360,13 @@
|
||||
const labels = Object.keys(processTimeMap).map(mainName => {
|
||||
const minutes = processTimeMap[mainName];
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const mins = minutes % 60;
|
||||
const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
||||
return `${mainName} (${timeStr})`;
|
||||
if (hours > 0) {
|
||||
const mins = Math.floor(minutes % 60);
|
||||
return `${mainName} (${hours}h ${mins}m)`;
|
||||
} else {
|
||||
const mins = parseFloat(minutes.toFixed(1));
|
||||
return `${mainName} (${mins}m)`;
|
||||
}
|
||||
});
|
||||
|
||||
processTimeChart = new Chart(processTimeCtx, {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user