feat: critical fixes + health API + webhook + attack reports
- Fix busy-wait loop in ServerGuardService (removed while(true)) - Fix sync blocking on async Future (use thenAccept instead of .get()) - Add startup banner with system info and config validation - Add HealthService: /api/health, /api/health/live, /api/health/ready - Add RequestLoggingFilter for HTTP request monitoring - Add CorsConfig for cross-origin support - Add WebhookService (Discord/Slack notifications) - Add AttackReportService with HTML report generation - New endpoints: /api/attacks/recent, /api/attacks/report, /api/webhook/test - Upgrade Lombok 1.18.30 to 1.18.38 for Java 25+ compatibility
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
package com.aluer.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class CorsConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsFilter corsFilter() {
|
||||||
|
CorsConfiguration corsConfig = new CorsConfiguration();
|
||||||
|
corsConfig.setAllowedOriginPatterns(List.of("*"));
|
||||||
|
corsConfig.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||||
|
corsConfig.setAllowedHeaders(List.of("*"));
|
||||||
|
corsConfig.setAllowCredentials(true);
|
||||||
|
corsConfig.setMaxAge(3600L);
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/api/**", corsConfig);
|
||||||
|
return new CorsFilter(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.aluer.notification;
|
||||||
|
|
||||||
|
import com.aluer.model.AlertEvent;
|
||||||
|
import com.aluer.model.MetricsData;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AttackReportService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AttackReportService.class);
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
private final ConcurrentLinkedDeque<Map<String, Object>> recentAttacks = new ConcurrentLinkedDeque<>();
|
||||||
|
|
||||||
|
@Value("${serverguard.report.dir:./reports}")
|
||||||
|
private String reportDir;
|
||||||
|
|
||||||
|
public void recordAttack(AlertEvent alert, String sourceIp, String details) {
|
||||||
|
Map<String, Object> record = new java.util.LinkedHashMap<>();
|
||||||
|
record.put("timestamp", System.currentTimeMillis());
|
||||||
|
record.put("type", alert.getType().name());
|
||||||
|
record.put("sourceIp", sourceIp);
|
||||||
|
record.put("confidence", alert.getConfidence());
|
||||||
|
record.put("message", alert.getMessage());
|
||||||
|
record.put("details", details);
|
||||||
|
recentAttacks.addFirst(record);
|
||||||
|
while (recentAttacks.size() > 500) {
|
||||||
|
recentAttacks.pollLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> getRecentAttacks(int limit) {
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
int count = 0;
|
||||||
|
for (Map<String, Object> attack : recentAttacks) {
|
||||||
|
if (count++ >= limit) break;
|
||||||
|
result.add(attack);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateHtmlReport() {
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
html.append("<!DOCTYPE html><html lang=\"zh-CN\"><head>");
|
||||||
|
html.append("<meta charset=\"UTF-8\">");
|
||||||
|
html.append("<title>Aluer ServerGuard Attack Report</title>");
|
||||||
|
html.append("<style>");
|
||||||
|
html.append("body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a1628;color:#e2e8f0;padding:24px;}");
|
||||||
|
html.append("h1{color:#63e7c2;border-bottom:2px solid #1a365d;padding-bottom:12px;}");
|
||||||
|
html.append("table{width:100%;border-collapse:collapse;margin-top:16px;}");
|
||||||
|
html.append("th{background:#1a365d;padding:12px;text-align:left;}");
|
||||||
|
html.append("td{padding:10px 12px;border-bottom:1px solid #1e3a5f;}");
|
||||||
|
html.append(".critical{color:#ff7f96;}.high{color:#ffc86b;}.medium{color:#60cbff;}");
|
||||||
|
html.append(".timestamp{color:#8fa7c2;font-size:0.85em;}");
|
||||||
|
html.append("</style></head><body>");
|
||||||
|
html.append("<h1>Aluer ServerGuard — Attack Report</h1>");
|
||||||
|
html.append("<p class=\"timestamp\">Generated: ").append(formatTimestamp(System.currentTimeMillis())).append("</p>");
|
||||||
|
|
||||||
|
html.append("<table><thead><tr>");
|
||||||
|
html.append("<th>Time</th><th>Type</th><th>Source IP</th><th>Confidence</th><th>Message</th>");
|
||||||
|
html.append("</tr></thead><tbody>");
|
||||||
|
|
||||||
|
for (Map<String, Object> attack : recentAttacks) {
|
||||||
|
double confidence = toDouble(attack.get("confidence"));
|
||||||
|
String cssClass = confidence > 0.8 ? "critical" : confidence > 0.5 ? "high" : "medium";
|
||||||
|
html.append("<tr class=\"").append(cssClass).append("\">");
|
||||||
|
html.append("<td>").append(formatTimestamp(toLong(attack.get("timestamp")))).append("</td>");
|
||||||
|
html.append("<td>").append(attack.get("type")).append("</td>");
|
||||||
|
html.append("<td>").append(attack.get("sourceIp")).append("</td>");
|
||||||
|
html.append("<td>").append(String.format("%.0f%%", confidence * 100)).append("</td>");
|
||||||
|
html.append("<td>").append(attack.get("message")).append("</td>");
|
||||||
|
html.append("</tr>");
|
||||||
|
}
|
||||||
|
html.append("</tbody></table></body></html>");
|
||||||
|
return html.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public File saveHtmlReport() throws IOException {
|
||||||
|
Files.createDirectories(Path.of(reportDir));
|
||||||
|
String filename = "attack-report-" + DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
|
||||||
|
.withZone(ZoneId.systemDefault()).format(Instant.now()) + ".html";
|
||||||
|
File file = new File(reportDir, filename);
|
||||||
|
try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) {
|
||||||
|
writer.write(generateHtmlReport());
|
||||||
|
}
|
||||||
|
logger.info("Attack report saved: {}", file.getAbsolutePath());
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTimestamp(long millis) {
|
||||||
|
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||||
|
.withZone(ZoneId.systemDefault())
|
||||||
|
.format(Instant.ofEpochMilli(millis));
|
||||||
|
}
|
||||||
|
|
||||||
|
private double toDouble(Object value) {
|
||||||
|
if (value instanceof Number n) return n.doubleValue();
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long toLong(Object value) {
|
||||||
|
if (value instanceof Number n) return n.longValue();
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.aluer.notification;
|
||||||
|
|
||||||
|
import com.aluer.model.AlertEvent;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class WebhookService {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(WebhookService.class);
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
|
@Value("${serverguard.webhook.discord-url:}")
|
||||||
|
private String discordWebhookUrl;
|
||||||
|
|
||||||
|
@Value("${serverguard.webhook.slack-url:}")
|
||||||
|
private String slackWebhookUrl;
|
||||||
|
|
||||||
|
@Value("${serverguard.webhook.enabled:false}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
public CompletableFuture<Boolean> sendDiscordAlert(AlertEvent alert) {
|
||||||
|
if (!enabled || discordWebhookUrl.isEmpty()) {
|
||||||
|
return CompletableFuture.completedFuture(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
JsonObject embed = new JsonObject();
|
||||||
|
embed.addProperty("title", alert.getType().getTitle());
|
||||||
|
embed.addProperty("description", alert.getMessage());
|
||||||
|
embed.addProperty("color", alert.getConfidence() > 0.8 ? 0xFF0000 :
|
||||||
|
alert.getConfidence() > 0.5 ? 0xFFA500 : 0xFFFF00);
|
||||||
|
|
||||||
|
JsonObject footer = new JsonObject();
|
||||||
|
footer.addProperty("text", "Aluer ServerGuard | " + alert.getType().name());
|
||||||
|
embed.add("footer", footer);
|
||||||
|
|
||||||
|
JsonObject payload = new JsonObject();
|
||||||
|
com.google.gson.JsonArray embeds = new com.google.gson.JsonArray();
|
||||||
|
embeds.add(embed);
|
||||||
|
payload.add("embeds", embeds);
|
||||||
|
|
||||||
|
sendWebhook(discordWebhookUrl, payload);
|
||||||
|
logger.debug("Discord webhook sent for alert: {}", alert.getType());
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Discord webhook failed: {}", e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Boolean> sendCustomMessage(String title, String message, String level) {
|
||||||
|
if (!enabled) {
|
||||||
|
return CompletableFuture.completedFuture(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
int color = "critical".equals(level) ? 0xFF0000 :
|
||||||
|
"warning".equals(level) ? 0xFFA500 : 0x00FF00;
|
||||||
|
|
||||||
|
JsonObject embed = new JsonObject();
|
||||||
|
embed.addProperty("title", title);
|
||||||
|
embed.addProperty("description", message);
|
||||||
|
embed.addProperty("color", color);
|
||||||
|
|
||||||
|
JsonObject payload = new JsonObject();
|
||||||
|
com.google.gson.JsonArray embeds = new com.google.gson.JsonArray();
|
||||||
|
embeds.add(embed);
|
||||||
|
payload.add("embeds", embeds);
|
||||||
|
|
||||||
|
boolean sent = false;
|
||||||
|
if (!discordWebhookUrl.isEmpty()) {
|
||||||
|
sent = sendWebhook(discordWebhookUrl, payload);
|
||||||
|
}
|
||||||
|
if (!slackWebhookUrl.isEmpty()) {
|
||||||
|
JsonObject slackPayload = new JsonObject();
|
||||||
|
slackPayload.addProperty("text", "*" + title + "*\n" + message);
|
||||||
|
sent |= sendWebhook(slackWebhookUrl, slackPayload);
|
||||||
|
}
|
||||||
|
return sent;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendWebhook(String webhookUrl, JsonObject payload) {
|
||||||
|
try {
|
||||||
|
URL url = URI.create(webhookUrl).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
|
||||||
|
byte[] body = gson.toJson(payload).getBytes(StandardCharsets.UTF_8);
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = conn.getResponseCode();
|
||||||
|
return status >= 200 && status < 300;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.debug("Webhook send failed: {}", e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import com.aluer.model.AlertEvent;
|
|||||||
import com.aluer.model.MetricsData;
|
import com.aluer.model.MetricsData;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.CommandLineRunner;
|
import org.springframework.boot.CommandLineRunner;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ import java.util.List;
|
|||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ServerGuardService implements CommandLineRunner {
|
public class ServerGuardService implements CommandLineRunner {
|
||||||
@@ -34,9 +36,11 @@ public class ServerGuardService implements CommandLineRunner {
|
|||||||
private final AutoExecutor autoExecutor;
|
private final AutoExecutor autoExecutor;
|
||||||
private final RconClient rconClient;
|
private final RconClient rconClient;
|
||||||
|
|
||||||
|
@Value("${server.port:8080}")
|
||||||
|
private int serverPort;
|
||||||
|
|
||||||
private final ScheduledExecutorService scheduler;
|
private final ScheduledExecutorService scheduler;
|
||||||
private volatile boolean running = true;
|
private final AtomicBoolean running = new AtomicBoolean(true);
|
||||||
private int alertCount = 0;
|
|
||||||
|
|
||||||
public ServerGuardService(
|
public ServerGuardService(
|
||||||
ServerGuardConfig config,
|
ServerGuardConfig config,
|
||||||
@@ -66,40 +70,115 @@ public class ServerGuardService implements CommandLineRunner {
|
|||||||
this.autoExecutor = autoExecutor;
|
this.autoExecutor = autoExecutor;
|
||||||
this.rconClient = rconClient;
|
this.rconClient = rconClient;
|
||||||
|
|
||||||
this.scheduler = Executors.newScheduledThreadPool(4);
|
this.scheduler = Executors.newScheduledThreadPool(4, r -> {
|
||||||
|
Thread t = new Thread(r, "serverguard-scheduler");
|
||||||
|
t.setDaemon(false);
|
||||||
|
return t;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run(String... args) throws Exception {
|
public void run(String... args) {
|
||||||
logger.info("========================================");
|
printBanner();
|
||||||
logger.info(" Aluer 服务器防护系统 启动中...");
|
validateConfiguration();
|
||||||
logger.info("========================================");
|
|
||||||
|
|
||||||
if (deepSeekClient.isEnabled()) {
|
|
||||||
logger.info(" DeepSeek AI: 已启用");
|
|
||||||
} else {
|
|
||||||
logger.info(" DeepSeek AI: 未启用 (请配置apiKey以启用)");
|
|
||||||
}
|
|
||||||
|
|
||||||
startMonitoring();
|
startMonitoring();
|
||||||
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
logger.info("正在关闭服务器防护系统...");
|
logger.info("Shutdown signal received, gracefully stopping...");
|
||||||
running = false;
|
running.set(false);
|
||||||
scheduler.shutdown();
|
scheduler.shutdown();
|
||||||
try {
|
try {
|
||||||
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
|
if (!scheduler.awaitTermination(8, TimeUnit.SECONDS)) {
|
||||||
scheduler.shutdownNow();
|
scheduler.shutdownNow();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
scheduler.shutdownNow();
|
scheduler.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
emailAlertService.shutdown();
|
emailAlertService.shutdown();
|
||||||
logger.info("服务器防护系统已停止");
|
if (rconClient.isConnected()) {
|
||||||
}));
|
rconClient.close();
|
||||||
|
}
|
||||||
|
logger.info("Aluer ServerGuard stopped");
|
||||||
|
}, "serverguard-shutdown"));
|
||||||
|
}
|
||||||
|
|
||||||
while (running) {
|
private void printBanner() {
|
||||||
Thread.sleep(1000);
|
logger.info("");
|
||||||
|
logger.info("╔══════════════════════════════════════════════════════════════╗");
|
||||||
|
logger.info("║ Aluer ServerGuard v1.0.0 ║");
|
||||||
|
logger.info("║ AI-Powered Minecraft Server Protection ║");
|
||||||
|
logger.info("╠══════════════════════════════════════════════════════════════╣");
|
||||||
|
logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version")));
|
||||||
|
logger.info("║ Available CPUs: {}{} ║",
|
||||||
|
String.format("%-32s", Runtime.getRuntime().availableProcessors()),
|
||||||
|
"");
|
||||||
|
logger.info("║ Max Memory: {}{} ║",
|
||||||
|
String.format("%-32s", (Runtime.getRuntime().maxMemory() / (1024 * 1024)) + " MB"),
|
||||||
|
"");
|
||||||
|
logger.info("╠══════════════════════════════════════════════════════════════╣");
|
||||||
|
logger.info("║ RCON: {}:{} {}{} ║",
|
||||||
|
config.getMinecraft().getRcon().getHost(),
|
||||||
|
config.getMinecraft().getRcon().getPort(),
|
||||||
|
config.getMinecraft().getRcon().isEnabled() ? "enabled" : "disabled",
|
||||||
|
" ");
|
||||||
|
logger.info("║ DeepSeek AI: {}{} ║",
|
||||||
|
deepSeekClient.isEnabled() ? "enabled (" + config.getAi().getDeepseek().getModel() + ")" : "disabled",
|
||||||
|
deepSeekClient.isEnabled() ? "" : " ");
|
||||||
|
logger.info("║ Web Console: http://0.0.0.0:{}{} ║",
|
||||||
|
serverPort,
|
||||||
|
" ");
|
||||||
|
logger.info("╠══════════════════════════════════════════════════════════════╣");
|
||||||
|
logger.info("║ Monitor: {}s interval{}{} ║",
|
||||||
|
config.getMinecraft().getCheckIntervalSeconds(),
|
||||||
|
" ",
|
||||||
|
"");
|
||||||
|
logger.info("║ Auto-Execute: {}{} ║",
|
||||||
|
config.getAi().getDeepseek().getAutoExecute().isEnabled() ? "enabled" : "disabled",
|
||||||
|
" ");
|
||||||
|
logger.info("║ Auto-Heal: {}{} ║",
|
||||||
|
config.getSecurity().getSelfHealing().isEnabled() ? "enabled" : "disabled",
|
||||||
|
" ");
|
||||||
|
logger.info("╚══════════════════════════════════════════════════════════════╝");
|
||||||
|
logger.info("");
|
||||||
|
logger.info("Type 'help' in the shell for available commands.");
|
||||||
|
logger.info("Access the web console at http://localhost:{}/", serverPort);
|
||||||
|
logger.info("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateConfiguration() {
|
||||||
|
int warnings = 0;
|
||||||
|
|
||||||
|
if (!deepSeekClient.isEnabled()) {
|
||||||
|
logger.warn("CONFIG: DeepSeek AI is disabled. Set DEEPSEEK_API_KEY to enable AI features.");
|
||||||
|
warnings++;
|
||||||
|
}
|
||||||
|
|
||||||
|
String rconPassword = config.getMinecraft().getRcon().getPassword();
|
||||||
|
if (rconPassword == null || rconPassword.isEmpty()) {
|
||||||
|
logger.warn("CONFIG: RCON password is not set. Set RCON_PASSWORD env var.");
|
||||||
|
warnings++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.getAlert().getEmail().getUsername().isEmpty()) {
|
||||||
|
if (config.getAlert().getEmail().getTo() == null || config.getAlert().getEmail().getTo().isEmpty()) {
|
||||||
|
logger.warn("CONFIG: Email alert enabled but no recipients configured.");
|
||||||
|
warnings++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.getSecurity().getHostEnforcement().isDryRun()) {
|
||||||
|
logger.info("CONFIG: Host enforcement is in dry-run mode (safe for first deployment).");
|
||||||
|
}
|
||||||
|
if (config.getSecurity().getSelfHealing().isDryRun()) {
|
||||||
|
logger.info("CONFIG: Self-healing is in dry-run mode (safe for first deployment).");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warnings > 0) {
|
||||||
|
logger.info("Configuration check complete: {} warning(s) found.", warnings);
|
||||||
|
} else {
|
||||||
|
logger.info("Configuration validated successfully.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,18 +193,18 @@ public class ServerGuardService implements CommandLineRunner {
|
|||||||
scheduler.scheduleAtFixedRate(this::runDeepSeekAnalysis, analysisInterval, analysisInterval, TimeUnit.SECONDS);
|
scheduler.scheduleAtFixedRate(this::runDeepSeekAnalysis, analysisInterval, analysisInterval, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("监控已启动, 间隔: {} 秒", intervalSeconds);
|
logger.info("Monitoring started with {}s interval", intervalSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkProcess() {
|
private void checkProcess() {
|
||||||
try {
|
try {
|
||||||
AlertEvent event = processMonitor.checkAndRestart();
|
AlertEvent event = processMonitor.checkAndRestart();
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
logger.warn("Process check alert: {}", event.getMessage());
|
logger.warn("Process alert: {}", event.getMessage());
|
||||||
handleAlert(event);
|
handleAlert(event);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error in process check: {}", e.getMessage());
|
logger.error("Process check failed: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +213,6 @@ public class ServerGuardService implements CommandLineRunner {
|
|||||||
MetricsData data = resourceMonitor.collectMetrics();
|
MetricsData data = resourceMonitor.collectMetrics();
|
||||||
|
|
||||||
deepSeekClient.addMetrics(data);
|
deepSeekClient.addMetrics(data);
|
||||||
|
|
||||||
adaptiveThreshold.addMetric(data);
|
adaptiveThreshold.addMetric(data);
|
||||||
anomalyDetector.addMetric(data);
|
anomalyDetector.addMetric(data);
|
||||||
timeSeriesPredictor.addMetric(data);
|
timeSeriesPredictor.addMetric(data);
|
||||||
@@ -177,7 +255,7 @@ public class ServerGuardService implements CommandLineRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error in metrics collection: {}", e.getMessage());
|
logger.error("Metrics collection failed: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,39 +265,40 @@ public class ServerGuardService implements CommandLineRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DeepSeekClient.AiAnalysisResult healthReport = deepSeekClient.getServerHealthReportAsync().get();
|
deepSeekClient.getServerHealthReportAsync().thenAccept(healthReport -> {
|
||||||
|
if (healthReport == null) {
|
||||||
if (healthReport != null) {
|
return;
|
||||||
logger.info("=== DeepSeek AI 健康报告 ===");
|
}
|
||||||
logger.info("状态: {}", healthReport.getSeverity());
|
logger.info("=== DeepSeek AI Health Report ===");
|
||||||
logger.info("摘要: {}", healthReport.getDescription());
|
logger.info("Status: {}", healthReport.getSeverity());
|
||||||
|
logger.info("Summary: {}", healthReport.getDescription());
|
||||||
|
|
||||||
if ("critical".equals(healthReport.getSeverity()) || "warning".equals(healthReport.getSeverity())) {
|
if ("critical".equals(healthReport.getSeverity()) || "warning".equals(healthReport.getSeverity())) {
|
||||||
for (String concern : healthReport.getRecommendedActions()) {
|
for (String action : healthReport.getRecommendedActions()) {
|
||||||
logger.warn("建议: {}", concern);
|
logger.warn("Recommendation: {}", action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("DeepSeek 分析错误: {}", e.getMessage());
|
logger.error("DeepSeek analysis failed: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleAlert(AlertEvent alert) {
|
private void handleAlert(AlertEvent alert) {
|
||||||
deepSeekClient.addAlert(alert);
|
deepSeekClient.addAlert(alert);
|
||||||
|
|
||||||
logger.warn("告警 [{}]: {} (置信度: {:.0f}%)",
|
logger.warn("Alert [{}]: {} (confidence: {:.0f}%)",
|
||||||
alert.getType(), alert.getMessage(), alert.getConfidence() * 100);
|
alert.getType(), alert.getMessage(), alert.getConfidence() * 100);
|
||||||
|
|
||||||
if (deepSeekClient.isEnabled() && config.getAi().getDeepseek().isAutoAnalyzeAlerts()) {
|
if (deepSeekClient.isEnabled() && config.getAi().getDeepseek().isAutoAnalyzeAlerts()) {
|
||||||
deepSeekClient.analyzeAlertAsync(alert).thenAccept(analysis -> {
|
deepSeekClient.analyzeAlertAsync(alert).thenAccept(analysis -> {
|
||||||
if (analysis != null) {
|
if (analysis != null) {
|
||||||
logger.info(" [DeepSeek AI 分析]");
|
logger.info(" [DeepSeek AI Analysis]");
|
||||||
logger.info(" 严重程度: {}", analysis.getSeverity());
|
logger.info(" Severity: {}", analysis.getSeverity());
|
||||||
logger.info(" 原因分析: {}", analysis.getRootCause());
|
logger.info(" Root Cause: {}", analysis.getRootCause());
|
||||||
|
|
||||||
if (!analysis.getRecommendedActions().isEmpty()) {
|
if (!analysis.getRecommendedActions().isEmpty()) {
|
||||||
logger.info(" 建议操作:");
|
logger.info(" Recommended Actions:");
|
||||||
for (String action : analysis.getRecommendedActions()) {
|
for (String action : analysis.getRecommendedActions()) {
|
||||||
logger.info(" - {}", action);
|
logger.info(" - {}", action);
|
||||||
}
|
}
|
||||||
@@ -233,10 +312,10 @@ public class ServerGuardService implements CommandLineRunner {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (alert.getRootCause() != null) {
|
if (alert.getRootCause() != null) {
|
||||||
logger.info(" 原因分析: {}", alert.getRootCause());
|
logger.info(" Root Cause: {}", alert.getRootCause());
|
||||||
}
|
}
|
||||||
if (alert.getSuggestedAction() != null) {
|
if (alert.getSuggestedAction() != null) {
|
||||||
logger.info(" 建议操作: {}", alert.getSuggestedAction());
|
logger.info(" Suggested Action: {}", alert.getSuggestedAction());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,30 +324,16 @@ public class ServerGuardService implements CommandLineRunner {
|
|||||||
if (alert.getType().name().contains("CONNECTION_FLOOD") ||
|
if (alert.getType().name().contains("CONNECTION_FLOOD") ||
|
||||||
alert.getType().name().contains("LOG_ATTACK")) {
|
alert.getType().name().contains("LOG_ATTACK")) {
|
||||||
if (alert.getConfidence() > 0.8) {
|
if (alert.getConfidence() > 0.8) {
|
||||||
logger.info("高置信度攻击告警 - 触发自动防御");
|
logger.info("High confidence attack alert — triggering auto defense");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void executeAutoAction(DeepSeekClient.AiAnalysisResult analysis, AlertEvent alert) {
|
public boolean isRunning() {
|
||||||
String autoAction = analysis.getAutoAction();
|
return running.get();
|
||||||
|
|
||||||
if (autoAction == null || autoAction.isEmpty() || autoAction.equals("无") || autoAction.equals("none")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(" [DeepSeek] 执行自动操作: {}", autoAction);
|
|
||||||
|
|
||||||
if (autoAction.contains("封禁IP") || autoAction.contains("ban")) {
|
|
||||||
logger.info(" 自动封禁IP已触发");
|
|
||||||
} else if (autoAction.contains("限制连接")) {
|
|
||||||
logger.info(" 自动连接限制已触发");
|
|
||||||
} else if (autoAction.contains("重启")) {
|
|
||||||
logger.info(" 建议重启服务器");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
running = false;
|
running.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.aluer.ai.DeepSeekClient;
|
|||||||
import com.aluer.audit.SecurityAuditService;
|
import com.aluer.audit.SecurityAuditService;
|
||||||
import com.aluer.backup.BackupService;
|
import com.aluer.backup.BackupService;
|
||||||
import com.aluer.config.ServerGuardConfig;
|
import com.aluer.config.ServerGuardConfig;
|
||||||
|
import com.aluer.notification.AttackReportService;
|
||||||
|
import com.aluer.notification.WebhookService;
|
||||||
import com.aluer.punishment.PunishmentService;
|
import com.aluer.punishment.PunishmentService;
|
||||||
import com.aluer.profiler.PerformanceProfiler;
|
import com.aluer.profiler.PerformanceProfiler;
|
||||||
import com.aluer.security.NetworkThreatFusionService;
|
import com.aluer.security.NetworkThreatFusionService;
|
||||||
@@ -28,6 +30,9 @@ public class DashboardController {
|
|||||||
private final DeepSeekClient deepSeekClient;
|
private final DeepSeekClient deepSeekClient;
|
||||||
private final ServerGuardConfig config;
|
private final ServerGuardConfig config;
|
||||||
private final NetworkThreatFusionService networkThreatFusionService;
|
private final NetworkThreatFusionService networkThreatFusionService;
|
||||||
|
private final HealthService healthService;
|
||||||
|
private final AttackReportService attackReportService;
|
||||||
|
private final WebhookService webhookService;
|
||||||
|
|
||||||
public DashboardController(
|
public DashboardController(
|
||||||
RconClient rconClient,
|
RconClient rconClient,
|
||||||
@@ -38,7 +43,10 @@ public class DashboardController {
|
|||||||
WorldManagementService worldService,
|
WorldManagementService worldService,
|
||||||
DeepSeekClient deepSeekClient,
|
DeepSeekClient deepSeekClient,
|
||||||
ServerGuardConfig config,
|
ServerGuardConfig config,
|
||||||
NetworkThreatFusionService networkThreatFusionService) {
|
NetworkThreatFusionService networkThreatFusionService,
|
||||||
|
HealthService healthService,
|
||||||
|
AttackReportService attackReportService,
|
||||||
|
WebhookService webhookService) {
|
||||||
this.rconClient = rconClient;
|
this.rconClient = rconClient;
|
||||||
this.profiler = profiler;
|
this.profiler = profiler;
|
||||||
this.backupService = backupService;
|
this.backupService = backupService;
|
||||||
@@ -48,6 +56,9 @@ public class DashboardController {
|
|||||||
this.deepSeekClient = deepSeekClient;
|
this.deepSeekClient = deepSeekClient;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.networkThreatFusionService = networkThreatFusionService;
|
this.networkThreatFusionService = networkThreatFusionService;
|
||||||
|
this.healthService = healthService;
|
||||||
|
this.attackReportService = attackReportService;
|
||||||
|
this.webhookService = webhookService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/status")
|
@GetMapping("/status")
|
||||||
@@ -166,4 +177,49 @@ public class DashboardController {
|
|||||||
status.put("enabled", true);
|
status.put("enabled", true);
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/health")
|
||||||
|
public Map<String, Object> getHealth() {
|
||||||
|
return healthService.getHealthReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/health/live")
|
||||||
|
public Map<String, Object> getLiveness() {
|
||||||
|
return healthService.getLiveness();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/health/ready")
|
||||||
|
public Map<String, Object> getReadiness() {
|
||||||
|
return healthService.getReadiness();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/attacks/recent")
|
||||||
|
public Map<String, Object> getRecentAttacks(@RequestParam(defaultValue = "20") int limit) {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("count", Math.min(limit, attackReportService.getRecentAttacks(Integer.MAX_VALUE).size()));
|
||||||
|
response.put("items", attackReportService.getRecentAttacks(limit));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/attacks/report")
|
||||||
|
public Map<String, Object> generateAttackReport() {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
try {
|
||||||
|
java.io.File report = attackReportService.saveHtmlReport();
|
||||||
|
response.put("status", "generated");
|
||||||
|
response.put("path", report.getAbsolutePath());
|
||||||
|
} catch (Exception e) {
|
||||||
|
response.put("status", "failed");
|
||||||
|
response.put("error", e.getMessage());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/webhook/test")
|
||||||
|
public Map<String, Object> testWebhook() {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
webhookService.sendCustomMessage("Aluer ServerGuard Test", "Webhook integration test successful.", "info");
|
||||||
|
response.put("status", "sent");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package com.aluer.web;
|
||||||
|
|
||||||
|
import com.aluer.ai.DeepSeekClient;
|
||||||
|
import com.aluer.config.ServerGuardConfig;
|
||||||
|
import com.aluer.service.RconClient;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
|
import java.lang.management.MemoryMXBean;
|
||||||
|
import java.lang.management.OperatingSystemMXBean;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class HealthService {
|
||||||
|
|
||||||
|
private final ServerGuardConfig config;
|
||||||
|
private final DeepSeekClient deepSeekClient;
|
||||||
|
private final RconClient rconClient;
|
||||||
|
private final OperatingSystemMXBean osBean;
|
||||||
|
private final MemoryMXBean memoryBean;
|
||||||
|
|
||||||
|
public HealthService(ServerGuardConfig config,
|
||||||
|
DeepSeekClient deepSeekClient,
|
||||||
|
RconClient rconClient) {
|
||||||
|
this.config = config;
|
||||||
|
this.deepSeekClient = deepSeekClient;
|
||||||
|
this.rconClient = rconClient;
|
||||||
|
this.osBean = ManagementFactory.getOperatingSystemMXBean();
|
||||||
|
this.memoryBean = ManagementFactory.getMemoryMXBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getHealthReport() {
|
||||||
|
Map<String, Object> report = new LinkedHashMap<>();
|
||||||
|
report.put("status", computeOverallStatus());
|
||||||
|
report.put("timestamp", System.currentTimeMillis());
|
||||||
|
report.put("uptimeSeconds", ManagementFactory.getRuntimeMXBean().getUptime() / 1000);
|
||||||
|
|
||||||
|
report.put("components", buildComponentStatus());
|
||||||
|
report.put("system", buildSystemInfo());
|
||||||
|
report.put("version", "1.0.0");
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getLiveness() {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
result.put("status", "UP");
|
||||||
|
result.put("timestamp", System.currentTimeMillis());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getReadiness() {
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
boolean rconOk = rconClient.isConnected() || config.getMinecraft().getRcon().isEnabled();
|
||||||
|
result.put("status", rconOk ? "READY" : "NOT_READY");
|
||||||
|
result.put("rcon", rconOk ? "configured" : "unavailable");
|
||||||
|
result.put("timestamp", System.currentTimeMillis());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String computeOverallStatus() {
|
||||||
|
if (!rconClient.isConnected() && config.getMinecraft().getRcon().isEnabled()) {
|
||||||
|
return "DEGRADED";
|
||||||
|
}
|
||||||
|
return "HEALTHY";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildComponentStatus() {
|
||||||
|
Map<String, Object> components = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
Map<String, Object> rcon = new LinkedHashMap<>();
|
||||||
|
rcon.put("status", rconClient.isConnected() ? "UP" : "DOWN");
|
||||||
|
rcon.put("host", config.getMinecraft().getRcon().getHost() + ":" + config.getMinecraft().getRcon().getPort());
|
||||||
|
components.put("rcon", rcon);
|
||||||
|
|
||||||
|
Map<String, Object> ai = new LinkedHashMap<>();
|
||||||
|
ai.put("status", deepSeekClient.isEnabled() ? "UP" : "DISABLED");
|
||||||
|
ai.put("model", config.getAi().getDeepseek().getModel());
|
||||||
|
components.put("deepseek-ai", ai);
|
||||||
|
|
||||||
|
Map<String, Object> email = new LinkedHashMap<>();
|
||||||
|
boolean emailConfigured = !config.getAlert().getEmail().getUsername().isEmpty();
|
||||||
|
email.put("status", emailConfigured ? "UP" : "DISABLED");
|
||||||
|
components.put("email-alert", email);
|
||||||
|
|
||||||
|
Map<String, Object> security = new LinkedHashMap<>();
|
||||||
|
security.put("status", config.getSecurity().isEnabled() ? "UP" : "DISABLED");
|
||||||
|
security.put("kernelEnabled", config.getSecurity().getKernel().isEnabled());
|
||||||
|
security.put("autonomyEnabled", config.getSecurity().getAutonomy().isEnabled());
|
||||||
|
components.put("security", security);
|
||||||
|
|
||||||
|
Map<String, Object> selfHealing = new LinkedHashMap<>();
|
||||||
|
selfHealing.put("status", config.getSecurity().getSelfHealing().isEnabled() ? "UP" : "DISABLED");
|
||||||
|
selfHealing.put("dryRun", config.getSecurity().getSelfHealing().isDryRun());
|
||||||
|
components.put("self-healing", selfHealing);
|
||||||
|
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildSystemInfo() {
|
||||||
|
Map<String, Object> system = new LinkedHashMap<>();
|
||||||
|
system.put("javaVersion", System.getProperty("java.version"));
|
||||||
|
system.put("availableProcessors", Runtime.getRuntime().availableProcessors());
|
||||||
|
|
||||||
|
long totalMemory = Runtime.getRuntime().totalMemory();
|
||||||
|
long freeMemory = Runtime.getRuntime().freeMemory();
|
||||||
|
long maxMemory = Runtime.getRuntime().maxMemory();
|
||||||
|
long usedMemory = totalMemory - freeMemory;
|
||||||
|
|
||||||
|
Map<String, Object> memory = new LinkedHashMap<>();
|
||||||
|
memory.put("usedMB", usedMemory / (1024 * 1024));
|
||||||
|
memory.put("freeMB", freeMemory / (1024 * 1024));
|
||||||
|
memory.put("totalMB", totalMemory / (1024 * 1024));
|
||||||
|
memory.put("maxMB", maxMemory / (1024 * 1024));
|
||||||
|
memory.put("usagePercent", Math.round((double) usedMemory / maxMemory * 1000) / 10.0);
|
||||||
|
system.put("memory", memory);
|
||||||
|
|
||||||
|
if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) {
|
||||||
|
system.put("systemCpuLoad", Math.round(sunOsBean.getCpuLoad() * 1000) / 10.0);
|
||||||
|
system.put("processCpuLoad", Math.round(sunOsBean.getProcessCpuLoad() * 1000) / 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
system.put("heapUsedMB", memoryBean.getHeapMemoryUsage().getUsed() / (1024 * 1024));
|
||||||
|
system.put("heapMaxMB", memoryBean.getHeapMemoryUsage().getMax() / (1024 * 1024));
|
||||||
|
|
||||||
|
return system;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.aluer.web;
|
||||||
|
|
||||||
|
import jakarta.servlet.*;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
|
||||||
|
public class RequestLoggingFilter implements Filter {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
|
||||||
|
try {
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
long duration = System.currentTimeMillis() - start;
|
||||||
|
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
||||||
|
int status = httpResponse.getStatus();
|
||||||
|
|
||||||
|
if (status >= 400 || duration > 1000) {
|
||||||
|
logger.debug("{} {} → {} ({}ms)", httpRequest.getMethod(), httpRequest.getRequestURI(), status, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status >= 500) {
|
||||||
|
logger.warn("Server error: {} {} → {} ({}ms)", httpRequest.getMethod(), httpRequest.getRequestURI(), status, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,6 +165,14 @@ serverguard:
|
|||||||
require-engine-handshake: true
|
require-engine-handshake: true
|
||||||
handshake-ttl-seconds: 30
|
handshake-ttl-seconds: 30
|
||||||
|
|
||||||
|
webhook:
|
||||||
|
enabled: false
|
||||||
|
discord-url: ${ALUER_WEBHOOK_DISCORD:}
|
||||||
|
slack-url: ${ALUER_WEBHOOK_SLACK:}
|
||||||
|
|
||||||
|
report:
|
||||||
|
dir: ./reports
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user