full refactor
This commit is contained in:
parent
1fe1137f80
commit
7d8c9be9e6
@ -1,603 +1,38 @@
|
|||||||
package cz.kamma.processmonitor;
|
package cz.kamma.processmonitor;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import cz.kamma.processmonitor.config.AppConfig;
|
||||||
import com.google.gson.JsonSyntaxException;
|
import cz.kamma.processmonitor.handler.DashboardHandler;
|
||||||
import com.sun.net.httpserver.Headers;
|
import cz.kamma.processmonitor.handler.DataApiHandler;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import cz.kamma.processmonitor.handler.HeartbeatHandler;
|
||||||
import com.sun.net.httpserver.HttpHandler;
|
import cz.kamma.processmonitor.repository.ProcessRepository;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import cz.kamma.processmonitor.service.ProcessService;
|
||||||
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
import java.net.InetSocketAddress;
|
||||||
import java.io.OutputStream;
|
import java.time.Instant;
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
public class Main {
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.DriverManager;
|
public static void main(String[] args) throws Exception {
|
||||||
import java.sql.PreparedStatement;
|
AppConfig config = AppConfig.load();
|
||||||
import java.sql.SQLException;
|
ProcessRepository repository = new ProcessRepository(config);
|
||||||
import java.sql.Timestamp;
|
ProcessService processService = new ProcessService();
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.ZoneId;
|
HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", 8080), 0);
|
||||||
import java.time.format.DateTimeParseException;
|
|
||||||
import java.util.Collections;
|
server.createContext("/hb/api", new HeartbeatHandler(repository));
|
||||||
import java.util.List;
|
server.createContext("/hb/dashboard", new DashboardHandler(config));
|
||||||
import java.util.Properties;
|
server.createContext("/hb/api/data", new DataApiHandler(config, repository, processService));
|
||||||
|
|
||||||
public class Main {
|
server.setExecutor(null);
|
||||||
private static final Gson GSON = new Gson();
|
server.start();
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
log("Listening on http://127.0.0.1:8080");
|
||||||
AppConfig config = AppConfig.load();
|
log("Dashboard: http://127.0.0.1:8080/hb/dashboard");
|
||||||
Database database = new Database(config);
|
log("API endpoint: http://127.0.0.1:8080/hb/api");
|
||||||
HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", 8080), 0);
|
}
|
||||||
server.createContext("/hb/api", new HeartbeatHandler(database));
|
|
||||||
server.createContext("/hb/dashboard", new DashboardHandler(config));
|
private static void log(String message) {
|
||||||
server.createContext("/hb/api/data", new DataApiHandler(database, config));
|
System.out.printf("[%s] %s%n", Instant.now(), message);
|
||||||
server.setExecutor(null);
|
}
|
||||||
server.start();
|
}
|
||||||
|
|
||||||
log("Listening on http://127.0.0.1:8080");
|
|
||||||
log("Dashboard: http://127.0.0.1:8080/hb/dashboard");
|
|
||||||
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();
|
loadLastRecordTime();
|
||||||
const records = data.records;
|
const records = data.records;
|
||||||
|
|
||||||
// Agregace času podle procesů
|
// Agregace času podle procesů: pro každý hlavní název se zobrazí čas nejdelšího podprocesu
|
||||||
const processTimeMap = {};
|
const processCounts = {};
|
||||||
|
records.forEach(r => {
|
||||||
// Seřadit záznamy podle procesu a času
|
const pName = r.process_name || '(bez procesu)';
|
||||||
const sortedRecords = records.sort((a, b) =>
|
processCounts[pName] = (processCounts[pName] || 0) + 1;
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Spočítat čas běhu pro každý hlavní název (45s za každý záznam)
|
const mainNameToMaxCount = {};
|
||||||
Object.keys(processByName).forEach(mainName => {
|
records.forEach(r => {
|
||||||
const records = processByName[mainName];
|
const mainName = r.main_name || r.process_name || '(bez procesu)';
|
||||||
const totalSeconds = records.length * 45;
|
const pName = r.process_name || '(bez procesu)';
|
||||||
processTimeMap[mainName] = totalSeconds / 60;
|
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();
|
if (processTimeChart) processTimeChart.destroy();
|
||||||
@ -362,9 +360,13 @@
|
|||||||
const labels = Object.keys(processTimeMap).map(mainName => {
|
const labels = Object.keys(processTimeMap).map(mainName => {
|
||||||
const minutes = processTimeMap[mainName];
|
const minutes = processTimeMap[mainName];
|
||||||
const hours = Math.floor(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
const mins = minutes % 60;
|
if (hours > 0) {
|
||||||
const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
const mins = Math.floor(minutes % 60);
|
||||||
return `${mainName} (${timeStr})`;
|
return `${mainName} (${hours}h ${mins}m)`;
|
||||||
|
} else {
|
||||||
|
const mins = parseFloat(minutes.toFixed(1));
|
||||||
|
return `${mainName} (${mins}m)`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
processTimeChart = new Chart(processTimeCtx, {
|
processTimeChart = new Chart(processTimeCtx, {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user