feat(syslog): 增加 WAL 与分 IP 接入诊断

- 每分钟保存 WAL 全局状态和分 IP 接收统计,历史保留 7 天
- 记录接收、WAL 落盘、Kafka ACK、过滤、背压及丢弃数量
- 增加 wal_max_pending_bytes 和 wal_admission_unavailable 指标
- 增加 TCP pending 当前值及进程内峰值统计
- 网络异常后同一 IP 恢复接收时打印恢复日志
- 提供当前状态及按日期查询诊断历史的接口
- 连接关闭时清理 TCP pending 计数并停止无效重试
- 增加相关单元测试和现场排查文档
This commit is contained in:
王绍宏
2026-09-16 17:32:08 +08:00
parent faa9ffe2d9
commit 8f1a8b6283
30 changed files with 1722 additions and 91 deletions
@@ -146,6 +146,30 @@ public class AppProperties {
@Value("${syslog.status-log-interval-ms:60000}")
private long syslogStatusLogIntervalMs;
// ========== 接入诊断历史 ==========
/** 独立于 Kafka/数据库保存诊断快照,避免故障链路同时带走排障证据。 */
@Value("${syslog.diagnostics.enabled:true}")
private boolean syslogDiagnosticsEnabled;
/** 必须位于宿主机持久化卷;生产默认映射到 /home/syslog/logs/syslog-monitor。 */
@Value("${syslog.diagnostics.directory:${SYSLOG_DIAGNOSTICS_DIRECTORY:/data/syslog-monitor}}")
private String syslogDiagnosticsDirectory;
@Value("${syslog.diagnostics.snapshot-interval-ms:60000}")
private long syslogDiagnosticsSnapshotIntervalMs;
/** 保留今天及此前 6 个自然日。 */
@Value("${syslog.diagnostics.retention-days:7}")
private int syslogDiagnosticsRetentionDays;
/** 防止伪造 UDP 来源产生无界 IP 基数;超出的来源统一汇总到 __OTHER__。 */
@Value("${syslog.diagnostics.max-source-ips:10000}")
private int syslogDiagnosticsMaxSourceIps;
/** 网络线程只执行 offer;队列满时绝不阻塞接入线程。 */
@Value("${syslog.diagnostics.event-queue-capacity:10000}")
private int syslogDiagnosticsEventQueueCapacity;
// ========== 旧 v1 原文归档精确导入(默认关闭) ==========
@Value("${syslog.wal.legacy-import.enabled:false}")
private boolean syslogWalLegacyImportEnabled;
@@ -0,0 +1,74 @@
package com.haobang.diagnostics;
import com.haobang.wal.SyslogWalStatusProvider;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
/** 现场查询当前分 IP 计数,并按日期下载持久化诊断历史。 */
@RestController
@RequestMapping("/api/syslog/wal/diagnostics")
public class SyslogDiagnosticsController {
private final SyslogWalStatusProvider statusProvider;
private final SyslogIpMetricsRegistry ipMetrics;
private final SyslogDiagnosticsPersistence persistence;
public SyslogDiagnosticsController(SyslogWalStatusProvider statusProvider,
SyslogIpMetricsRegistry ipMetrics,
SyslogDiagnosticsPersistence persistence) {
this.statusProvider = statusProvider;
this.ipMetrics = ipMetrics;
this.persistence = persistence;
}
@GetMapping("/current")
public Map<String, Object> current(
@RequestParam(name = "source_ip", required = false) String sourceIp) {
Map<String, Object> value = new LinkedHashMap<>();
value.put("persistence", persistence.status());
value.put("global", statusProvider.status());
List<Map<String, Object>> rows = ipMetrics.currentSnapshot();
if (sourceIp != null && !sourceIp.trim().isEmpty()) {
List<Map<String, Object>> filtered = new ArrayList<>();
for (Map<String, Object> row : rows) {
if (sourceIp.trim().equals(row.get("source_ip"))) filtered.add(row);
}
rows = filtered;
}
value.put("ip_stats", rows);
return value;
}
@GetMapping("/history")
public ResponseEntity<Resource> history(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
@RequestParam(defaultValue = "snapshots") String type) {
Path file;
if ("snapshots".equals(type)) file = persistence.snapshotFile(date);
else if ("ip".equals(type)) file = persistence.ipSnapshotFile(date);
else if ("events".equals(type)) file = persistence.eventFile(date);
else return ResponseEntity.badRequest().build();
if (file == null || !Files.isRegularFile(file)) return ResponseEntity.notFound().build();
Resource resource = new FileSystemResource(file.toFile());
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/x-ndjson"))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getFileName() + "\"")
.body(resource);
}
}
@@ -0,0 +1,284 @@
package com.haobang.diagnostics;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.haobang.config.AppProperties;
import com.haobang.wal.SyslogWalStatusProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/** 将全局/分 IP 快照和网络异常事件写入独立 JSONL 文件。 */
@Component
public class SyslogDiagnosticsPersistence {
private static final Logger log = LoggerFactory.getLogger(SyslogDiagnosticsPersistence.class);
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
private static final String SNAPSHOT_PREFIX = "syslog-status-";
private static final String EVENT_PREFIX = "syslog-events-";
private static final String IP_SNAPSHOT_PREFIX = "syslog-ip-status-";
private final AppProperties properties;
private final SyslogIpMetricsRegistry ipMetrics;
private final SyslogWalStatusProvider statusProvider;
private final ObjectMapper objectMapper;
private final AtomicBoolean running = new AtomicBoolean();
private final AtomicLong snapshotWriteFailures = new AtomicLong();
private final AtomicLong eventWriteFailures = new AtomicLong();
private final AtomicLong initializationFailures = new AtomicLong();
private volatile String lastSnapshotTime;
private volatile String lastEventTime;
private volatile String lastWriteError;
private volatile LocalDate lastCleanupDate;
private Path rootDirectory;
private Path snapshotDirectory;
private Path eventDirectory;
private Path ipSnapshotDirectory;
private ScheduledExecutorService snapshotScheduler;
private Thread eventWriterThread;
public SyslogDiagnosticsPersistence(AppProperties properties,
SyslogIpMetricsRegistry ipMetrics,
SyslogWalStatusProvider statusProvider,
ObjectMapper objectMapper) {
this.properties = properties;
this.ipMetrics = ipMetrics;
this.statusProvider = statusProvider;
this.objectMapper = objectMapper.copy().deactivateDefaultTyping();
}
@PostConstruct
public void start() {
if (!properties.isSyslogDiagnosticsEnabled()) {
log.warn("Syslog 诊断历史已关闭");
return;
}
try {
validateConfiguration();
rootDirectory = Paths.get(properties.getSyslogDiagnosticsDirectory())
.toAbsolutePath().normalize();
snapshotDirectory = rootDirectory.resolve("snapshots");
eventDirectory = rootDirectory.resolve("events");
ipSnapshotDirectory = rootDirectory.resolve("ip-snapshots");
Files.createDirectories(snapshotDirectory);
Files.createDirectories(eventDirectory);
Files.createDirectories(ipSnapshotDirectory);
cleanupExpiredFiles(LocalDate.now(ZONE));
} catch (Throwable failure) {
initializationFailures.incrementAndGet();
lastWriteError = failure.toString();
log.error("Syslog 诊断历史初始化失败;接入链路继续运行,诊断持久化不可用: directory={}",
properties.getSyslogDiagnosticsDirectory(), failure);
return;
}
running.set(true);
eventWriterThread = new Thread(this::eventWriterLoop, "syslog-diagnostics-event-writer");
eventWriterThread.setDaemon(true);
eventWriterThread.start();
snapshotScheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "syslog-diagnostics-snapshot-writer");
thread.setDaemon(true);
return thread;
});
snapshotScheduler.scheduleWithFixedDelay(this::writeSnapshotSafely,
1000L, properties.getSyslogDiagnosticsSnapshotIntervalMs(), TimeUnit.MILLISECONDS);
log.info("Syslog 诊断历史已启用: directory={}, snapshotIntervalMs={}, retentionDays={}, maxSourceIps={}",
rootDirectory, properties.getSyslogDiagnosticsSnapshotIntervalMs(),
properties.getSyslogDiagnosticsRetentionDays(), properties.getSyslogDiagnosticsMaxSourceIps());
}
private void eventWriterLoop() {
Map<String, Object> pending = null;
while (running.get() || pending != null || hasQueuedEvent()) {
try {
if (pending == null) {
pending = running.get()
? ipMetrics.pollEvent(1000L, TimeUnit.MILLISECONDS)
: ipMetrics.pollEvent();
}
if (pending != null) {
writeEvent(pending);
pending = null;
}
} catch (InterruptedException interrupted) {
if (running.get()) Thread.currentThread().interrupt();
} catch (Throwable failure) {
recordFailure(false, failure);
try { Thread.sleep(1000L); }
catch (InterruptedException interrupted) {
if (running.get()) Thread.currentThread().interrupt();
}
}
}
}
private boolean hasQueuedEvent() {
return ipMetrics.getEventQueueSize() > 0;
}
void writeSnapshotSafely() {
if (!running.get()) return;
try {
ZonedDateTime now = ZonedDateTime.now(ZONE);
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("record_type", "snapshot");
snapshot.put("snapshot_time", now.toInstant().toString());
snapshot.put("instance_id", ipMetrics.getInstanceId());
snapshot.put("process_start_time", ipMetrics.getProcessStartTime().toString());
snapshot.put("device_collect_id", properties.getAppServiceDeviceCollectId());
snapshot.put("device_collect_name", properties.getAppServiceDeviceCollectName());
snapshot.put("global", statusProvider.status());
SyslogIpMetricsRegistry.SnapshotBatch batch = ipMetrics.snapshotWithDeltas();
snapshot.put("ip_stats_count", batch.getRows().size());
appendJson(snapshotFile(now.toLocalDate()), snapshot);
appendIpSnapshots(ipSnapshotFile(now.toLocalDate()), now, batch.getRows());
ipMetrics.commitSnapshot(batch);
lastSnapshotTime = now.toInstant().toString();
cleanupExpiredFiles(now.toLocalDate());
} catch (Throwable failure) {
recordFailure(true, failure);
}
}
private void writeEvent(Map<String, Object> event) throws IOException {
LocalDate date = LocalDate.now(ZONE);
appendJson(eventFile(date), event);
Object eventTime = event.get("event_time");
lastEventTime = eventTime == null ? ZonedDateTime.now(ZONE).toInstant().toString()
: String.valueOf(eventTime);
}
private void appendJson(Path path, Map<String, Object> value) throws IOException {
byte[] json = (objectMapper.writeValueAsString(value) + System.lineSeparator())
.getBytes(StandardCharsets.UTF_8);
Files.write(path, json, StandardOpenOption.CREATE, StandardOpenOption.WRITE,
StandardOpenOption.APPEND);
}
private void appendIpSnapshots(Path path, ZonedDateTime now,
List<Map<String, Object>> rows) throws IOException {
if (rows.isEmpty()) return;
StringBuilder output = new StringBuilder(rows.size() * 512);
for (Map<String, Object> row : rows) {
Map<String, Object> record = new LinkedHashMap<>();
record.put("record_type", "ip_snapshot");
record.put("snapshot_time", now.toInstant().toString());
record.put("instance_id", ipMetrics.getInstanceId());
record.put("process_start_time", ipMetrics.getProcessStartTime().toString());
record.put("device_collect_id", properties.getAppServiceDeviceCollectId());
record.put("device_collect_name", properties.getAppServiceDeviceCollectName());
record.putAll(row);
output.append(objectMapper.writeValueAsString(record)).append(System.lineSeparator());
}
Files.write(path, output.toString().getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
}
private void cleanupExpiredFiles(LocalDate today) throws IOException {
if (today.equals(lastCleanupDate)) return;
LocalDate oldestRetained = today.minusDays(properties.getSyslogDiagnosticsRetentionDays() - 1L);
cleanupDirectory(snapshotDirectory, SNAPSHOT_PREFIX, oldestRetained);
cleanupDirectory(eventDirectory, EVENT_PREFIX, oldestRetained);
cleanupDirectory(ipSnapshotDirectory, IP_SNAPSHOT_PREFIX, oldestRetained);
lastCleanupDate = today;
}
private void cleanupDirectory(Path directory, String prefix, LocalDate oldestRetained) throws IOException {
try (DirectoryStream<Path> files = Files.newDirectoryStream(directory, prefix + "*.jsonl")) {
for (Path file : files) {
LocalDate fileDate = parseDate(file.getFileName().toString(), prefix);
if (fileDate != null && fileDate.isBefore(oldestRetained)) Files.deleteIfExists(file);
}
}
}
private LocalDate parseDate(String fileName, String prefix) {
if (!fileName.startsWith(prefix) || !fileName.endsWith(".jsonl")) return null;
String date = fileName.substring(prefix.length(), fileName.length() - ".jsonl".length());
try { return LocalDate.parse(date); }
catch (RuntimeException ignored) { return null; }
}
private void recordFailure(boolean snapshot, Throwable failure) {
long count = snapshot ? snapshotWriteFailures.incrementAndGet() : eventWriteFailures.incrementAndGet();
lastWriteError = failure.toString();
if (count == 1L || count % 100L == 0L) {
log.error("Syslog 诊断{}写入失败: failures={}", snapshot ? "快照" : "事件", count, failure);
}
}
private void validateConfiguration() {
if (properties.getSyslogDiagnosticsSnapshotIntervalMs() <= 0L
|| properties.getSyslogDiagnosticsRetentionDays() <= 0
|| properties.getSyslogDiagnosticsMaxSourceIps() <= 0
|| properties.getSyslogDiagnosticsEventQueueCapacity() <= 0) {
throw new IllegalArgumentException("Syslog 诊断间隔、保留天数、IP 上限和事件队列容量必须大于 0");
}
if (properties.getSyslogDiagnosticsDirectory() == null
|| properties.getSyslogDiagnosticsDirectory().trim().isEmpty()) {
throw new IllegalArgumentException("syslog.diagnostics.directory 不能为空");
}
}
public Map<String, Object> status() {
Map<String, Object> value = new LinkedHashMap<>();
value.put("enabled", properties.isSyslogDiagnosticsEnabled());
value.put("active", running.get());
value.put("directory", rootDirectory == null ? null : rootDirectory.toString());
value.put("retention_days", properties.getSyslogDiagnosticsRetentionDays());
value.put("snapshot_interval_ms", properties.getSyslogDiagnosticsSnapshotIntervalMs());
value.put("last_snapshot_time", lastSnapshotTime);
value.put("last_event_time", lastEventTime);
value.put("snapshot_write_failures", snapshotWriteFailures.get());
value.put("event_write_failures", eventWriteFailures.get());
value.put("initialization_failures", initializationFailures.get());
value.put("event_queue_dropped", ipMetrics.getEventQueueDropped());
value.put("last_write_error", lastWriteError);
return value;
}
public Path snapshotFile(LocalDate date) {
return snapshotDirectory == null ? null
: snapshotDirectory.resolve(SNAPSHOT_PREFIX + date + ".jsonl");
}
public Path eventFile(LocalDate date) {
return eventDirectory == null ? null
: eventDirectory.resolve(EVENT_PREFIX + date + ".jsonl");
}
public Path ipSnapshotFile(LocalDate date) {
return ipSnapshotDirectory == null ? null
: ipSnapshotDirectory.resolve(IP_SNAPSHOT_PREFIX + date + ".jsonl");
}
@PreDestroy
public void shutdown() {
if (!running.compareAndSet(true, false)) return;
if (snapshotScheduler != null) snapshotScheduler.shutdownNow();
if (eventWriterThread != null) {
eventWriterThread.interrupt();
try { eventWriterThread.join(5000L); }
catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); }
}
}
}
@@ -0,0 +1,496 @@
package com.haobang.diagnostics;
import com.common.entity.DeviceDevice;
import com.haobang.config.AppProperties;
import io.netty.handler.codec.TooLongFrameException;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
* 进程内、按来源 IP 聚合的接入诊断指标。
*
* <p>所有接入热路径只做无锁计数和非阻塞队列 offer。诊断文件 IO 由
* {@link SyslogDiagnosticsPersistence} 的独立线程完成。</p>
*/
@Component
public class SyslogIpMetricsRegistry {
public static final String OVERFLOW_SOURCE_IP = "__OTHER__";
private final ConcurrentHashMap<String, IpMetrics> metrics = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, MetricValues> previousSnapshot = new ConcurrentHashMap<>();
private final IpMetrics overflow = new IpMetrics(OVERFLOW_SOURCE_IP);
private final ArrayBlockingQueue<Map<String, Object>> eventQueue;
private final LongAdder eventQueueDropped = new LongAdder();
private final AtomicLong tcpPendingFramesCurrent = new AtomicLong();
private final AtomicLong tcpPendingFramesMax = new AtomicLong();
private final int maxSourceIps;
private final int deviceCollectId;
private final String deviceCollectName;
private final String instanceId = UUID.randomUUID().toString();
private final Instant processStartTime = Instant.now();
public SyslogIpMetricsRegistry(AppProperties properties) {
this.maxSourceIps = Math.max(1, properties.getSyslogDiagnosticsMaxSourceIps());
this.deviceCollectId = properties.getAppServiceDeviceCollectId();
this.deviceCollectName = properties.getAppServiceDeviceCollectName();
this.eventQueue = new ArrayBlockingQueue<>(
Math.max(1, properties.getSyslogDiagnosticsEventQueueCapacity()));
}
/**
* 记录收到的报文;如果该 IP 此前发生过网络接入异常,返回本次恢复信息。
*
* <p>这里不判断设备是否离线,也不生成中断统计,只用于让接入层在异常后的第一条报文到达时
* 打印一次恢复日志。</p>
*/
public NetworkRecovery recordNetworkReceived(String sourceIp, int sourcePort, String protocol) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, protocol);
value.networkReceived.increment();
long receivedAt = System.currentTimeMillis();
value.lastReceiveTime.set(receivedAt);
// __OTHER__ 会合并多个来源,不能用其中一个 IP 的异常推断另一个 IP 已恢复。
if (value == overflow) return null;
long exceptionAt = value.pendingRecoveryExceptionTime.getAndSet(0L);
return exceptionAt <= 0L ? null : new NetworkRecovery(exceptionAt, receivedAt);
}
public void recordWalAppended(String sourceIp, Integer sourcePort, String protocol) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort == null ? 0 : sourcePort, protocol);
value.walAppended.increment();
}
public void recordWalBackpressure(String sourceIp, int sourcePort, String protocol) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, protocol);
value.walAdmissionBackpressure.increment();
}
public void recordWalUnavailable(String sourceIp, Integer sourcePort, String protocol) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort == null ? 0 : sourcePort, protocol);
value.walAdmissionUnavailable.increment();
}
public void recordTcpPendingAdded(String sourceIp, int sourcePort) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, "TCP");
long current = tcpPendingFramesCurrent.incrementAndGet();
updateMax(tcpPendingFramesMax, current);
long ipCurrent = value.tcpPendingFramesCurrent.incrementAndGet();
updateMax(value.tcpPendingFramesMax, ipCurrent);
}
public void recordTcpPendingRemoved(String sourceIp, int sourcePort) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, "TCP");
decrementIfPositive(tcpPendingFramesCurrent);
decrementIfPositive(value.tcpPendingFramesCurrent);
}
public void recordKafkaAck(String sourceIp, int sourcePort, String protocol, DeviceDevice device) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, protocol);
value.assignDevice(device);
value.kafkaAcked.increment();
}
public void recordInvalidDeviceDropped(String sourceIp, String protocol) {
IpMetrics value = metric(sourceIp);
value.touch(0, protocol);
value.invalidDeviceDropped.increment();
}
public void recordDeviceLookupFailed(String sourceIp, String protocol) {
IpMetrics value = metric(sourceIp);
value.touch(0, protocol);
value.deviceLookupFailed.increment();
}
public void recordOversizedDropped(String sourceIp, int sourcePort, String protocol, int chars) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, protocol);
value.oversizedDropped.increment();
enqueueEvent(event("OVERSIZED_DROPPED", sourceIp, sourcePort, protocol,
"chars=" + chars, null, value));
}
public void recordUdpDropped(String sourceIp, int sourcePort) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, "UDP");
value.udpDropped.increment();
}
public void recordTcpPause(String sourceIp, int sourcePort) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, "TCP");
value.tcpPauseTotal.increment();
}
public void recordDispatchRetry(String sourceIp, int sourcePort, String protocol) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, protocol);
value.dispatchRetry.increment();
}
public void recordTerminalDropped(String sourceIp, int sourcePort, String protocol,
DeviceDevice device, String reason) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, protocol);
value.assignDevice(device);
value.dispatchTerminalDropped.increment();
enqueueEvent(event("DISPATCH_TERMINAL_DROPPED", sourceIp, sourcePort, protocol,
abbreviate(reason, 1024), null, value));
}
public void recordNetworkException(String sourceIp, int sourcePort, String protocol,
Throwable failure) {
IpMetrics value = metric(sourceIp);
value.touch(sourcePort, protocol);
value.networkException.increment();
long exceptionAt = System.currentTimeMillis();
value.lastExceptionTime.set(exceptionAt);
// 多次连续异常只保留第一次,恢复日志才能反映从首次发现异常到重新收到数据的间隔。
if (value != overflow) value.pendingRecoveryExceptionTime.compareAndSet(0L, exceptionAt);
boolean tcp = "TCP".equalsIgnoreCase(protocol);
String type = classify(failure, tcp);
if (tcp) {
value.tcpNetworkException.increment();
if ("TOO_LONG_FRAME".equals(type)) value.tcpTooLongFrame.increment();
else if ("CONNECTION_RESET".equals(type)) value.tcpConnectionReset.increment();
else value.tcpOtherException.increment();
} else {
value.udpNetworkException.increment();
}
Throwable root = rootCause(failure);
enqueueEvent(event(type, sourceIp, sourcePort, protocol,
root == null ? null : abbreviate(root.getMessage(), 1024),
root == null ? null : root.getClass().getName(), value));
}
/** 当前累计值;读取接口调用不会改变分钟增量基线。 */
public List<Map<String, Object>> currentSnapshot() {
return snapshot(false);
}
/** 持久化线程专用;返回累计值并推进下一分钟的 delta 基线。 */
public List<Map<String, Object>> snapshotAndAdvanceDeltas() {
SnapshotBatch batch = snapshotWithDeltas();
commitSnapshot(batch);
return batch.getRows();
}
/** 先捕获一致的 delta,只有两个诊断文件都写成功后才由持久化线程提交基线。 */
public SnapshotBatch snapshotWithDeltas() {
List<IpMetrics> values = sortedMetrics();
List<Map<String, Object>> rows = new ArrayList<>(values.size());
Map<String, MetricValues> baselines = new LinkedHashMap<>();
for (IpMetrics value : values) {
MetricValues current = value.values();
rows.add(value.toMap(current, current.minus(previousSnapshot.get(value.sourceIp))));
baselines.put(value.sourceIp, current);
}
return new SnapshotBatch(rows, baselines);
}
public void commitSnapshot(SnapshotBatch batch) {
if (batch != null) previousSnapshot.putAll(batch.baselines);
}
private List<Map<String, Object>> snapshot(boolean advanceDeltas) {
List<IpMetrics> values = sortedMetrics();
List<Map<String, Object>> result = new ArrayList<>(values.size());
for (IpMetrics value : values) {
MetricValues current = value.values();
MetricValues previous = previousSnapshot.get(value.sourceIp);
result.add(value.toMap(current, advanceDeltas ? current.minus(previous) : null));
if (advanceDeltas) previousSnapshot.put(value.sourceIp, current);
}
return result;
}
private List<IpMetrics> sortedMetrics() {
List<IpMetrics> values = new ArrayList<>(metrics.values());
if (!overflow.isEmpty()) values.add(overflow);
Collections.sort(values, Comparator.comparing(value -> value.sourceIp));
return values;
}
public Map<String, Object> summary() {
Map<String, Object> value = new LinkedHashMap<>();
value.put("instance_id", instanceId);
value.put("process_start_time", processStartTime.toString());
value.put("device_collect_id", deviceCollectId);
value.put("device_collect_name", deviceCollectName);
value.put("tracked_source_ips", metrics.size());
value.put("max_source_ips", maxSourceIps);
value.put("event_queue_size", eventQueue.size());
value.put("event_queue_dropped", eventQueueDropped.sum());
value.put("network_exception_total", sum(Metric.NETWORK_EXCEPTION));
value.put("tcp_network_exception_total", sum(Metric.TCP_NETWORK_EXCEPTION));
value.put("udp_network_exception_total", sum(Metric.UDP_NETWORK_EXCEPTION));
value.put("tcp_connection_reset_total", sum(Metric.TCP_CONNECTION_RESET));
value.put("tcp_too_long_frame_total", sum(Metric.TCP_TOO_LONG_FRAME));
value.put("tcp_other_exception_total", sum(Metric.TCP_OTHER_EXCEPTION));
value.put("tcp_pending_frames_current", tcpPendingFramesCurrent.get());
value.put("tcp_pending_frames_max", tcpPendingFramesMax.get());
return value;
}
public Map<String, Object> pollEvent(long timeout, TimeUnit unit) throws InterruptedException {
return eventQueue.poll(timeout, unit);
}
public Map<String, Object> pollEvent() { return eventQueue.poll(); }
public String getInstanceId() { return instanceId; }
public Instant getProcessStartTime() { return processStartTime; }
public int getEventQueueSize() { return eventQueue.size(); }
public long getEventQueueDropped() { return eventQueueDropped.sum(); }
public long getTcpPendingFramesCurrent() { return tcpPendingFramesCurrent.get(); }
public long getTcpPendingFramesMax() { return tcpPendingFramesMax.get(); }
public static final class NetworkRecovery {
private final long exceptionAtEpochMillis;
private final long recoveredAtEpochMillis;
private NetworkRecovery(long exceptionAtEpochMillis, long recoveredAtEpochMillis) {
this.exceptionAtEpochMillis = exceptionAtEpochMillis;
this.recoveredAtEpochMillis = recoveredAtEpochMillis;
}
public Instant getExceptionAt() { return Instant.ofEpochMilli(exceptionAtEpochMillis); }
public Instant getRecoveredAt() { return Instant.ofEpochMilli(recoveredAtEpochMillis); }
public long getElapsedMillis() {
return Math.max(0L, recoveredAtEpochMillis - exceptionAtEpochMillis);
}
}
private IpMetrics metric(String sourceIp) {
String normalized = sourceIp == null || sourceIp.trim().isEmpty() ? "unknown" : sourceIp.trim();
IpMetrics existing = metrics.get(normalized);
if (existing != null) return existing;
synchronized (metrics) {
existing = metrics.get(normalized);
if (existing != null) return existing;
if (metrics.size() >= maxSourceIps) return overflow;
IpMetrics created = new IpMetrics(normalized);
metrics.put(normalized, created);
return created;
}
}
private void enqueueEvent(Map<String, Object> event) {
if (!eventQueue.offer(event)) eventQueueDropped.increment();
}
private static void updateMax(AtomicLong maximum, long candidate) {
long current = maximum.get();
while (candidate > current && !maximum.compareAndSet(current, candidate)) {
current = maximum.get();
}
}
private static void decrementIfPositive(AtomicLong value) {
long current = value.get();
while (current > 0L && !value.compareAndSet(current, current - 1L)) {
current = value.get();
}
}
private Map<String, Object> event(String eventType, String sourceIp, int sourcePort,
String protocol, String message, String exceptionClass,
IpMetrics metrics) {
Map<String, Object> value = new LinkedHashMap<>();
value.put("record_type", "event");
value.put("event_time", Instant.now().toString());
value.put("instance_id", instanceId);
value.put("device_collect_id", deviceCollectId);
value.put("device_collect_name", deviceCollectName);
value.put("event_type", eventType);
value.put("source_ip", sourceIp);
value.put("source_port", sourcePort);
value.put("protocol", protocol);
value.put("device_id", metrics.deviceId.get() == 0 ? null : metrics.deviceId.get());
value.put("device_name", metrics.deviceName);
value.put("exception_class", exceptionClass);
value.put("message", message);
return value;
}
private long sum(Metric metric) {
long total = metric.value(overflow);
for (IpMetrics value : metrics.values()) total += metric.value(value);
return total;
}
private static String classify(Throwable failure, boolean tcp) {
if (!tcp) return "UDP_NETWORK_EXCEPTION";
for (Throwable current = failure; current != null; current = current.getCause()) {
if (current instanceof TooLongFrameException) return "TOO_LONG_FRAME";
String text = String.valueOf(current.getMessage()).toLowerCase();
if (text.contains("connection reset") || text.contains("broken pipe")
|| text.contains("forcibly closed")) return "CONNECTION_RESET";
}
return "TCP_NETWORK_EXCEPTION";
}
private static Throwable rootCause(Throwable failure) {
Throwable current = failure;
while (current != null && current.getCause() != null && current.getCause() != current) {
current = current.getCause();
}
return current;
}
private static String abbreviate(String value, int limit) {
if (value == null || value.length() <= limit) return value;
return value.substring(0, limit);
}
private enum Metric {
NETWORK_EXCEPTION { long value(IpMetrics value) { return value.networkException.sum(); } },
TCP_NETWORK_EXCEPTION { long value(IpMetrics value) { return value.tcpNetworkException.sum(); } },
UDP_NETWORK_EXCEPTION { long value(IpMetrics value) { return value.udpNetworkException.sum(); } },
TCP_CONNECTION_RESET { long value(IpMetrics value) { return value.tcpConnectionReset.sum(); } },
TCP_TOO_LONG_FRAME { long value(IpMetrics value) { return value.tcpTooLongFrame.sum(); } },
TCP_OTHER_EXCEPTION { long value(IpMetrics value) { return value.tcpOtherException.sum(); } };
abstract long value(IpMetrics value);
}
public static final class SnapshotBatch {
private final List<Map<String, Object>> rows;
private final Map<String, MetricValues> baselines;
private SnapshotBatch(List<Map<String, Object>> rows,
Map<String, MetricValues> baselines) {
this.rows = rows;
this.baselines = baselines;
}
public List<Map<String, Object>> getRows() { return rows; }
}
private static final class IpMetrics {
private final String sourceIp;
private final Set<String> protocols = ConcurrentHashMap.newKeySet();
private final AtomicInteger lastSourcePort = new AtomicInteger();
private final AtomicInteger deviceId = new AtomicInteger();
private volatile String deviceName;
private final AtomicLong lastReceiveTime = new AtomicLong();
private final AtomicLong lastExceptionTime = new AtomicLong();
private final AtomicLong pendingRecoveryExceptionTime = new AtomicLong();
private final AtomicLong tcpPendingFramesCurrent = new AtomicLong();
private final AtomicLong tcpPendingFramesMax = new AtomicLong();
private final LongAdder networkReceived = new LongAdder();
private final LongAdder walAppended = new LongAdder();
private final LongAdder walAdmissionBackpressure = new LongAdder();
private final LongAdder walAdmissionUnavailable = new LongAdder();
private final LongAdder kafkaAcked = new LongAdder();
private final LongAdder invalidDeviceDropped = new LongAdder();
private final LongAdder deviceLookupFailed = new LongAdder();
private final LongAdder oversizedDropped = new LongAdder();
private final LongAdder udpDropped = new LongAdder();
private final LongAdder tcpPauseTotal = new LongAdder();
private final LongAdder networkException = new LongAdder();
private final LongAdder tcpNetworkException = new LongAdder();
private final LongAdder udpNetworkException = new LongAdder();
private final LongAdder tcpConnectionReset = new LongAdder();
private final LongAdder tcpTooLongFrame = new LongAdder();
private final LongAdder tcpOtherException = new LongAdder();
private final LongAdder dispatchTerminalDropped = new LongAdder();
private final LongAdder dispatchRetry = new LongAdder();
private IpMetrics(String sourceIp) { this.sourceIp = sourceIp; }
private void touch(int sourcePort, String protocol) {
if (sourcePort > 0) lastSourcePort.set(sourcePort);
if (protocol != null && !protocol.trim().isEmpty()) protocols.add(protocol);
}
private void assignDevice(DeviceDevice device) {
if (device == null) return;
if (device.getId() != null) deviceId.set(device.getId());
if (device.getName() != null) deviceName = device.getName();
}
private MetricValues values() {
return new MetricValues(networkReceived.sum(), walAppended.sum(),
walAdmissionBackpressure.sum(), walAdmissionUnavailable.sum(), kafkaAcked.sum(),
invalidDeviceDropped.sum(), deviceLookupFailed.sum(), oversizedDropped.sum(),
udpDropped.sum(), tcpPauseTotal.sum(), networkException.sum(),
tcpNetworkException.sum(), udpNetworkException.sum(),
tcpConnectionReset.sum(), tcpTooLongFrame.sum(), tcpOtherException.sum(),
dispatchTerminalDropped.sum(), dispatchRetry.sum());
}
private Map<String, Object> toMap(MetricValues total, MetricValues delta) {
Map<String, Object> value = new LinkedHashMap<>();
value.put("source_ip", sourceIp);
value.put("source_port", lastSourcePort.get());
value.put("protocols", new ArrayList<>(protocols));
value.put("device_id", deviceId.get() == 0 ? null : deviceId.get());
value.put("device_name", deviceName);
value.put("last_receive_time", instant(lastReceiveTime.get()));
value.put("last_exception_time", instant(lastExceptionTime.get()));
value.put("tcp_pending_frames_current", tcpPendingFramesCurrent.get());
value.put("tcp_pending_frames_max", tcpPendingFramesMax.get());
value.putAll(total.toMap());
if (delta != null) value.put("delta", delta.toMap());
return value;
}
private boolean isEmpty() { return values().isZero(); }
private static String instant(long epochMillis) {
return epochMillis <= 0L ? null : Instant.ofEpochMilli(epochMillis).toString();
}
}
private static final class MetricValues {
private final long[] values;
private MetricValues(long... values) { this.values = values; }
private MetricValues minus(MetricValues previous) {
long[] result = new long[values.length];
for (int index = 0; index < values.length; index++) {
long old = previous == null ? 0L : previous.values[index];
result[index] = Math.max(0L, values[index] - old);
}
return new MetricValues(result);
}
private boolean isZero() {
for (long value : values) if (value != 0L) return false;
return true;
}
private Map<String, Object> toMap() {
String[] names = {"network_received", "wal_appended", "wal_admission_backpressure",
"wal_admission_unavailable", "kafka_acked",
"invalid_device_dropped", "device_lookup_failed", "oversized_dropped",
"udp_dropped", "tcp_pause_total", "network_exception_total",
"tcp_network_exception_total", "udp_network_exception_total",
"tcp_connection_reset_total", "tcp_too_long_frame_total",
"tcp_other_exception_total", "dispatch_terminal_dropped", "dispatch_retry"};
Map<String, Object> result = new LinkedHashMap<>();
for (int index = 0; index < names.length; index++) result.put(names[index], values[index]);
return result;
}
}
}
@@ -1,6 +1,7 @@
package com.haobang.dispatch;
import com.haobang.config.AppProperties;
import com.haobang.diagnostics.SyslogIpMetricsRegistry;
import com.haobang.wal.SyslogWalRecord;
import com.haobang.wal.SyslogWalService;
import com.kafka.kafkaProducer;
@@ -8,6 +9,7 @@ import com.netty.SyslogMessageHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@@ -51,6 +53,7 @@ public class SyslogWalDispatcher {
private final LongAdder retryCount = new LongAdder();
private final LongAdder largeDispatched = new LongAdder();
private final AtomicLong inflight = new AtomicLong();
private SyslogIpMetricsRegistry ipMetrics;
private Semaphore permits;
private ByteBudget byteBudget;
@@ -71,6 +74,11 @@ public class SyslogWalDispatcher {
this.properties = properties;
}
@Autowired(required = false)
void setIpMetrics(SyslogIpMetricsRegistry ipMetrics) {
this.ipMetrics = ipMetrics;
}
@PostConstruct
public void start() throws Exception {
validateConfiguration();
@@ -157,6 +165,10 @@ public class SyslogWalDispatcher {
walService.recordTerminal(record, resolution.getType().name(), resolution.getReason());
} else {
terminalDropped.increment();
if (ipMetrics != null) {
ipMetrics.recordTerminalDropped(record.getSourceIp(), record.getSourcePort(),
record.getProtocol(), resolution.getDevice(), resolution.getReason());
}
}
terminalFiltered.increment();
markComplete(record);
@@ -200,6 +212,10 @@ public class SyslogWalDispatcher {
.whenComplete((delivery, failure) -> {
if (failure == null) {
kafkaAcked.increment();
if (ipMetrics != null) {
ipMetrics.recordKafkaAck(record.getSourceIp(), record.getSourcePort(),
record.getProtocol(), resolution.getDevice());
}
if (large) largeDispatched.increment();
markComplete(record);
} else {
@@ -214,6 +230,9 @@ public class SyslogWalDispatcher {
private void scheduleRetry(SyslogWalRecord record, int attempt, String stage, Throwable failure) {
retryCount.increment();
if (ipMetrics != null) {
ipMetrics.recordDispatchRetry(record.getSourceIp(), record.getSourcePort(), record.getProtocol());
}
long retries = retryCount.sum();
if (attempt == 1 || retries % 100 == 0) {
log.warn("WAL 事件投递失败,将保留原 event_id 重试: eventId={}, sequence={}, stage={}, "
@@ -261,18 +280,21 @@ public class SyslogWalDispatcher {
private void logStatus() {
if (!running.get()) return;
log.info("Syslog WAL状态: networkReceived={}, appended={}, admissionOversized={}, walPendingApprox={}, walPendingBytes={}, walQueue={}/{}, durableSequence={}, "
log.info("Syslog WAL状态: networkReceived={}, appended={}, admissionOversized={}, admissionUnavailable={}, "
+ "walPendingApprox={}, walPendingBytes={}/{}, walQueue={}/{}, durableSequence={}, "
+ "checkpointSequence={}, kafkaInflight={}, kafkaInflightBytes={}, kafkaAcked={}, largeDispatched={}, "
+ "terminalRecordEnabled={}, terminalFiltered={}, terminalDropped={}, "
+ "retry={}, tcpPauseTotal={}, udpDropped={}, deviceCacheSize={}, deviceCacheHits={}, "
+ "retry={}, tcpPauseTotal={}, tcpPendingFrames={}/{}, udpDropped={}, deviceCacheSize={}, deviceCacheHits={}, "
+ "deviceCacheMisses={}",
SyslogMessageHandler.getNetworkReceivedCount(), walService.getAppendedCount(),
walService.getOversizedCount(), walService.getPendingRecordsApprox(), walService.getPendingAppendBytes(),
walService.getOversizedCount(), walService.getUnavailableCount(),
walService.getPendingRecordsApprox(), walService.getPendingAppendBytes(), walService.getMaxPendingBytes(),
walService.getQueueSize(), walService.getQueueCapacity(), walService.getDurableSequence(),
walService.getCheckpointSequence(), inflight.get(), byteBudget == null ? 0L : byteBudget.used(),
kafkaAcked.sum(), largeDispatched.sum(), properties.isSyslogDispatchTerminalRecordEnabled(),
terminalFiltered.sum(), terminalDropped.sum(),
retryCount.sum(), SyslogMessageHandler.getTcpPauseCount(),
SyslogMessageHandler.getTcpPendingFramesCurrent(), SyslogMessageHandler.getTcpPendingFramesMax(),
SyslogMessageHandler.getUdpDroppedCount(), deviceResolver.getCacheSize(),
deviceResolver.getHitCount(), deviceResolver.getMissCount());
}
@@ -3,6 +3,7 @@ package com.haobang.wal;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.haobang.config.AppProperties;
import com.haobang.diagnostics.SyslogIpMetricsRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -70,12 +71,14 @@ public class SyslogWalService {
private final ObjectMapper objectMapper;
private final Charset syslogCharset;
private final ForceOperation forceOperation;
private SyslogIpMetricsRegistry ipMetrics;
private final PointTimeAllocator pointTimeAllocator = new PointTimeAllocator();
private final AtomicBoolean initialized = new AtomicBoolean();
private final AtomicLong nextSequence = new AtomicLong();
private final AtomicLong durableSequence = new AtomicLong();
private final LongAdder appended = new LongAdder();
private final LongAdder admissionBackpressure = new LongAdder();
private final LongAdder admissionUnavailable = new LongAdder();
private final LongAdder admissionOversized = new LongAdder();
private final AtomicLong pendingAppendBytes = new AtomicLong();
private final LongAdder quarantined = new LongAdder();
@@ -147,6 +150,12 @@ public class SyslogWalService {
this.forceOperation = forceOperation;
}
/** 测试可不注册诊断 Bean;生产环境由 Spring 注入。 */
@Autowired(required = false)
void setIpMetrics(SyslogIpMetricsRegistry ipMetrics) {
this.ipMetrics = ipMetrics;
}
@PostConstruct
public void initialize() {
if (!initialized.compareAndSet(false, true)) return;
@@ -232,6 +241,11 @@ public class SyslogWalService {
}
refreshDiskState(false);
if (!running || fatalFailure != null || diskState == DiskState.CRITICAL) {
admissionUnavailable.increment();
if (ipMetrics != null) {
ipMetrics.recordWalUnavailable(event.getSourceIp(), event.getSourcePort(),
event.getTransport());
}
return AppendAttempt.rejected(AdmissionStatus.UNAVAILABLE);
}
int rawBytes = event.getRawMessage().getBytes(event.getCharset()).length;
@@ -250,7 +264,16 @@ public class SyslogWalService {
admissionBackpressure.increment();
return AppendAttempt.rejected(AdmissionStatus.BACKPRESSURE);
}
return AppendAttempt.accepted(pending.getDurableFuture());
AppendAttempt accepted = AppendAttempt.accepted(pending.getDurableFuture());
if (ipMetrics != null) {
accepted.getDurableStage().whenComplete((receipt, failure) -> {
if (failure == null) {
ipMetrics.recordWalAppended(event.getSourceIp(), event.getSourcePort(),
event.getTransport());
}
});
}
return accepted;
}
public boolean shouldPauseTcp() {
@@ -281,8 +304,10 @@ public class SyslogWalService {
public int getQueueCapacity() { return properties.getSyslogWalQueueCapacity(); }
public long getAppendedCount() { return appended.sum(); }
public long getBackpressureCount() { return admissionBackpressure.sum(); }
public long getUnavailableCount() { return admissionUnavailable.sum(); }
public long getOversizedCount() { return admissionOversized.sum(); }
public long getPendingAppendBytes() { return pendingAppendBytes.get(); }
public long getMaxPendingBytes() { return properties.getSyslogWalMaxPendingBytes(); }
public long getQuarantinedCount() { return quarantined.sum(); }
public long getTerminalRecordedCount() { return terminalRecorded.sum(); }
public long getIngressPointTimeAssignedCount() { return pointTimeAllocator.getAssignedCount(); }
@@ -1,74 +1,23 @@
package com.haobang.wal;
import com.haobang.dispatch.DeviceContextResolver;
import com.haobang.dispatch.SyslogWalDispatcher;
import com.netty.SyslogMessageHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
/** 运维只读状态接口;不提供跳过、清空或人工推进游标等危险操作。 */
@RestController
@RequestMapping("/api/syslog/wal")
public class SyslogWalStatusController {
private final SyslogWalService wal;
private final SyslogWalDispatcher dispatcher;
private final DeviceContextResolver resolver;
private final SyslogWalStatusProvider statusProvider;
public SyslogWalStatusController(SyslogWalService wal, SyslogWalDispatcher dispatcher,
DeviceContextResolver resolver) {
this.wal = wal;
this.dispatcher = dispatcher;
this.resolver = resolver;
public SyslogWalStatusController(SyslogWalStatusProvider statusProvider) {
this.statusProvider = statusProvider;
}
@GetMapping("/status")
public Map<String, Object> status() {
Map<String, Object> value = new LinkedHashMap<>();
value.put("network_received", SyslogMessageHandler.getNetworkReceivedCount());
value.put("network_oversized_dropped", SyslogMessageHandler.getOversizedDroppedCount());
value.put("network_invalid_device_dropped", SyslogMessageHandler.getInvalidDeviceDroppedCount());
value.put("network_device_lookup_failed", SyslogMessageHandler.getDeviceLookupFailedCount());
value.put("wal_appended", wal.getAppendedCount());
value.put("wal_queue_size", wal.getQueueSize());
value.put("wal_queue_capacity", wal.getQueueCapacity());
value.put("wal_pending_bytes", wal.getPendingAppendBytes());
value.put("wal_durable_sequence", wal.getDurableSequence());
value.put("wal_checkpoint_sequence", wal.getCheckpointSequence());
value.put("wal_pending_approx", wal.getPendingRecordsApprox());
value.put("wal_admission_backpressure", wal.getBackpressureCount());
value.put("wal_admission_oversized", wal.getOversizedCount());
value.put("wal_terminal_recorded", wal.getTerminalRecordedCount());
value.put("wal_quarantined", wal.getQuarantinedCount());
Map<String, Long> pointTimeAssigned = new LinkedHashMap<>();
pointTimeAssigned.put("ingress", wal.getIngressPointTimeAssignedCount());
value.put("point_time_assigned_total", pointTimeAssigned);
value.put("wal_disk_critical", wal.isDiskCritical());
value.put("wal_disk_state", wal.getDiskState());
value.put("wal_disk_total_bytes", wal.getDiskTotalBytes());
value.put("wal_disk_usable_bytes", wal.getDiskUsableBytes());
value.put("wal_disk_stop_threshold_bytes", wal.getDiskStopThresholdBytes());
value.put("wal_fatal_error", wal.getFatalFailure() == null
? null : wal.getFatalFailure().toString());
value.put("dispatch_inflight", dispatcher.getInflight());
value.put("dispatch_inflight_bytes", dispatcher.getInflightBytes());
value.put("kafka_acked", dispatcher.getKafkaAcked());
value.put("kafka_large_dispatched", dispatcher.getLargeDispatched());
value.put("dispatch_terminal_record_enabled", dispatcher.isTerminalRecordEnabled());
value.put("dispatch_terminal_filtered", dispatcher.getTerminalFiltered());
value.put("dispatch_terminal_dropped", dispatcher.getTerminalDropped());
value.put("dispatch_retry", dispatcher.getRetryCount());
value.put("tcp_pause_total", SyslogMessageHandler.getTcpPauseCount());
value.put("udp_dropped_total", SyslogMessageHandler.getUdpDroppedCount());
value.put("device_cache_size", resolver.getCacheSize());
value.put("device_cache_hits", resolver.getHitCount());
value.put("device_cache_misses", resolver.getMissCount());
value.put("device_ingress_snapshot_ready", resolver.isIngressSnapshotReady());
value.put("device_ingress_snapshot_size", resolver.getIngressSnapshotSize());
value.put("device_ingress_snapshot_refresh_failed", resolver.getIngressSnapshotRefreshFailedCount());
return value;
return statusProvider.status();
}
}
@@ -0,0 +1,78 @@
package com.haobang.wal;
import com.haobang.diagnostics.SyslogIpMetricsRegistry;
import com.haobang.dispatch.DeviceContextResolver;
import com.haobang.dispatch.SyslogWalDispatcher;
import com.netty.SyslogMessageHandler;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
/** 生成 HTTP 状态与持久化快照共用的单一口径。 */
@Component
public class SyslogWalStatusProvider {
private final SyslogWalService wal;
private final SyslogWalDispatcher dispatcher;
private final DeviceContextResolver resolver;
private final SyslogIpMetricsRegistry ipMetrics;
public SyslogWalStatusProvider(SyslogWalService wal, SyslogWalDispatcher dispatcher,
DeviceContextResolver resolver, SyslogIpMetricsRegistry ipMetrics) {
this.wal = wal;
this.dispatcher = dispatcher;
this.resolver = resolver;
this.ipMetrics = ipMetrics;
}
public Map<String, Object> status() {
Map<String, Object> value = new LinkedHashMap<>();
value.put("network_received", SyslogMessageHandler.getNetworkReceivedCount());
value.put("network_oversized_dropped", SyslogMessageHandler.getOversizedDroppedCount());
value.put("network_invalid_device_dropped", SyslogMessageHandler.getInvalidDeviceDroppedCount());
value.put("network_device_lookup_failed", SyslogMessageHandler.getDeviceLookupFailedCount());
value.put("wal_appended", wal.getAppendedCount());
value.put("wal_queue_size", wal.getQueueSize());
value.put("wal_queue_capacity", wal.getQueueCapacity());
value.put("wal_pending_bytes", wal.getPendingAppendBytes());
value.put("wal_max_pending_bytes", wal.getMaxPendingBytes());
value.put("wal_durable_sequence", wal.getDurableSequence());
value.put("wal_checkpoint_sequence", wal.getCheckpointSequence());
value.put("wal_pending_approx", wal.getPendingRecordsApprox());
value.put("wal_admission_backpressure", wal.getBackpressureCount());
value.put("wal_admission_unavailable", wal.getUnavailableCount());
value.put("wal_admission_oversized", wal.getOversizedCount());
value.put("wal_terminal_recorded", wal.getTerminalRecordedCount());
value.put("wal_quarantined", wal.getQuarantinedCount());
Map<String, Long> pointTimeAssigned = new LinkedHashMap<>();
pointTimeAssigned.put("ingress", wal.getIngressPointTimeAssignedCount());
value.put("point_time_assigned_total", pointTimeAssigned);
value.put("wal_disk_critical", wal.isDiskCritical());
value.put("wal_disk_state", wal.getDiskState());
value.put("wal_disk_total_bytes", wal.getDiskTotalBytes());
value.put("wal_disk_usable_bytes", wal.getDiskUsableBytes());
value.put("wal_disk_stop_threshold_bytes", wal.getDiskStopThresholdBytes());
value.put("wal_fatal_error", wal.getFatalFailure() == null
? null : wal.getFatalFailure().toString());
value.put("dispatch_inflight", dispatcher.getInflight());
value.put("dispatch_inflight_bytes", dispatcher.getInflightBytes());
value.put("kafka_acked", dispatcher.getKafkaAcked());
value.put("kafka_large_dispatched", dispatcher.getLargeDispatched());
value.put("dispatch_terminal_record_enabled", dispatcher.isTerminalRecordEnabled());
value.put("dispatch_terminal_filtered", dispatcher.getTerminalFiltered());
value.put("dispatch_terminal_dropped", dispatcher.getTerminalDropped());
value.put("dispatch_retry", dispatcher.getRetryCount());
value.put("tcp_pause_total", SyslogMessageHandler.getTcpPauseCount());
value.put("tcp_pending_frames_current", SyslogMessageHandler.getTcpPendingFramesCurrent());
value.put("tcp_pending_frames_max", SyslogMessageHandler.getTcpPendingFramesMax());
value.put("udp_dropped_total", SyslogMessageHandler.getUdpDroppedCount());
value.put("device_cache_size", resolver.getCacheSize());
value.put("device_cache_hits", resolver.getHitCount());
value.put("device_cache_misses", resolver.getMissCount());
value.put("device_ingress_snapshot_ready", resolver.isIngressSnapshotReady());
value.put("device_ingress_snapshot_size", resolver.getIngressSnapshotSize());
value.put("device_ingress_snapshot_refresh_failed", resolver.getIngressSnapshotRefreshFailedCount());
value.put("diagnostics", ipMetrics.summary());
return value;
}
}
@@ -1,12 +1,14 @@
package com.netty;
import com.haobang.dispatch.DeviceContextResolver;
import com.haobang.diagnostics.SyslogIpMetricsRegistry;
import com.haobang.wal.SyslogIngressEvent;
import com.haobang.wal.SyslogWalService;
import com.kafka.kafkaProducer;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.DatagramChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,6 +21,7 @@ import java.time.ZoneId;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
@@ -37,6 +40,8 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
private static final LongAdder OVERSIZED_DROPPED = new LongAdder();
private static final LongAdder INVALID_DEVICE_DROPPED = new LongAdder();
private static final LongAdder DEVICE_LOOKUP_FAILED = new LongAdder();
private static final AtomicLong TCP_PENDING_FRAMES_CURRENT = new AtomicLong();
private static final AtomicLong TCP_PENDING_FRAMES_MAX = new AtomicLong();
/** 每个 TCP Channel 独立 Handler,pending 只保存关闭 AUTO_READ 前已进入 Pipeline 的帧。 */
private final Deque<SyslogIngressEvent> pendingTcp = new ArrayDeque<>();
@@ -44,15 +49,22 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
private final SyslogWalService walService;
private final DeviceContextResolver deviceResolver;
private final boolean dropInvalidBeforeWal;
private final SyslogIpMetricsRegistry ipMetrics;
private boolean retryScheduled;
private boolean autoReadPaused;
SyslogMessageHandler(Charset syslogCharset, SyslogWalService walService) {
this(syslogCharset, walService, null, false);
this(syslogCharset, walService, null, false, null);
}
SyslogMessageHandler(Charset syslogCharset, SyslogWalService walService,
DeviceContextResolver deviceResolver, boolean dropInvalidBeforeWal) {
this(syslogCharset, walService, deviceResolver, dropInvalidBeforeWal, null);
}
SyslogMessageHandler(Charset syslogCharset, SyslogWalService walService,
DeviceContextResolver deviceResolver, boolean dropInvalidBeforeWal,
SyslogIpMetricsRegistry ipMetrics) {
this.syslogCharset = java.util.Objects.requireNonNull(syslogCharset, "syslogCharset");
this.walService = java.util.Objects.requireNonNull(walService, "walService");
if (dropInvalidBeforeWal && deviceResolver == null) {
@@ -60,6 +72,7 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
}
this.deviceResolver = deviceResolver;
this.dropInvalidBeforeWal = dropInvalidBeforeWal;
this.ipMetrics = ipMetrics;
}
@Override
@@ -93,6 +106,16 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
}
NETWORK_RECEIVED.increment();
if (ipMetrics != null) {
SyslogIpMetricsRegistry.NetworkRecovery recovery =
ipMetrics.recordNetworkReceived(sourceIp, sourcePort, protocol);
if (recovery != null) {
log.info("Syslog 网络接入异常后已恢复接收: sourceIp={}, sourcePort={}, protocol={}, "
+ "exceptionTime={}, recoveryTime={}, elapsedMs={}",
sourceIp, sourcePort, protocol, recovery.getExceptionAt(),
recovery.getRecoveredAt(), recovery.getElapsedMillis());
}
}
if (dropInvalidBeforeWal && shouldDropBeforeWal(sourceIp, protocol)) return;
Instant receivedInstant = Instant.now();
@@ -114,7 +137,11 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
recordOversized(event, "UDP");
return;
}
if (result == SyslogWalService.AdmissionResult.BACKPRESSURE && ipMetrics != null) {
ipMetrics.recordWalBackpressure(event.getSourceIp(), event.getSourcePort(), "UDP");
}
UDP_DROPPED.increment();
if (ipMetrics != null) ipMetrics.recordUdpDropped(event.getSourceIp(), event.getSourcePort());
long dropped = UDP_DROPPED.sum();
if (dropped == 1 || dropped % 100 == 0) {
log.error("UDP Syslog 无法进入 WAL;UDP 不支持背压,已发生明确丢弃: eventId={}, sourceIp={}, "
@@ -126,7 +153,7 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
private void acceptTcp(ChannelHandlerContext ctx, SyslogWalService wal, SyslogIngressEvent event) {
// pending 非空时必须维持同一 TCP Channel 的原始顺序,不能让新帧越过旧帧。
if (!pendingTcp.isEmpty()) {
pendingTcp.addLast(event);
addPendingTcp(event);
pauseAndSchedule(ctx, wal);
return;
}
@@ -137,7 +164,10 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
return;
}
if (result != SyslogWalService.AdmissionResult.ACCEPTED) {
pendingTcp.addLast(event);
if (result == SyslogWalService.AdmissionResult.BACKPRESSURE && ipMetrics != null) {
ipMetrics.recordWalBackpressure(event.getSourceIp(), event.getSourcePort(), "TCP");
}
addPendingTcp(event);
pauseAndSchedule(ctx, wal);
return;
}
@@ -149,6 +179,12 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
ctx.channel().config().setAutoRead(false);
autoReadPaused = true;
TCP_PAUSE_COUNT.increment();
if (ipMetrics != null) {
InetSocketAddress remote = remoteAddress(ctx);
ipMetrics.recordTcpPause(remote == null ? String.valueOf(ctx.channel().remoteAddress())
: remote.getAddress().getHostAddress(),
remote == null ? 0 : remote.getPort());
}
log.warn("TCP Syslog 已按 WAL 接入保护暂停读取: channel={}, pendingFrames={}, walQueue={}/{}, "
+ "diskState={}, pauseTotal={}",
ctx.channel().remoteAddress(), pendingTcp.size(), wal.getQueueSize(), wal.getQueueCapacity(),
@@ -158,7 +194,7 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
}
private void scheduleDrain(ChannelHandlerContext ctx, SyslogWalService wal) {
if (retryScheduled) return;
if (retryScheduled || !ctx.channel().isActive()) return;
retryScheduled = true;
ctx.executor().schedule(() -> {
retryScheduled = false;
@@ -167,19 +203,27 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
}
private void drainPending(ChannelHandlerContext ctx, SyslogWalService wal) {
if (!ctx.channel().isActive()) {
clearPendingTcp();
autoReadPaused = false;
return;
}
while (!pendingTcp.isEmpty()) {
SyslogIngressEvent event = pendingTcp.peekFirst();
SyslogWalService.AdmissionResult result = wal.tryAppend(event);
if (result == SyslogWalService.AdmissionResult.OVERSIZED) {
pendingTcp.removeFirst();
removeFirstPendingTcp();
recordOversized(event, "TCP");
continue;
}
if (result != SyslogWalService.AdmissionResult.ACCEPTED) {
if (result == SyslogWalService.AdmissionResult.BACKPRESSURE && ipMetrics != null) {
ipMetrics.recordWalBackpressure(event.getSourceIp(), event.getSourcePort(), "TCP");
}
scheduleDrain(ctx, wal);
return;
}
pendingTcp.removeFirst();
removeFirstPendingTcp();
}
if (wal.canResumeTcp()) {
if (autoReadPaused && ctx.channel().isActive()) {
@@ -196,33 +240,103 @@ public class SyslogMessageHandler extends SimpleChannelInboundHandler<Object> {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (ipMetrics != null) {
InetSocketAddress remote = remoteAddress(ctx);
String protocol = ctx.channel() instanceof DatagramChannel ? "UDP" : "TCP";
ipMetrics.recordNetworkException(
remote == null ? String.valueOf(ctx.channel().remoteAddress())
: remote.getAddress().getHostAddress(),
remote == null ? 0 : remote.getPort(), protocol, cause);
}
log.error("Syslog 网络接入异常: channel={}", ctx.channel().remoteAddress(), cause);
ctx.close();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
clearPendingTcp();
super.channelInactive(ctx);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
clearPendingTcp();
super.handlerRemoved(ctx);
}
public static long getNetworkReceivedCount() { return NETWORK_RECEIVED.sum(); }
public static long getTcpPauseCount() { return TCP_PAUSE_COUNT.sum(); }
public static long getUdpDroppedCount() { return UDP_DROPPED.sum(); }
public static long getOversizedDroppedCount() { return OVERSIZED_DROPPED.sum(); }
public static long getInvalidDeviceDroppedCount() { return INVALID_DEVICE_DROPPED.sum(); }
public static long getDeviceLookupFailedCount() { return DEVICE_LOOKUP_FAILED.sum(); }
public static long getTcpPendingFramesCurrent() { return TCP_PENDING_FRAMES_CURRENT.get(); }
public static long getTcpPendingFramesMax() { return TCP_PENDING_FRAMES_MAX.get(); }
private void addPendingTcp(SyslogIngressEvent event) {
pendingTcp.addLast(event);
long current = TCP_PENDING_FRAMES_CURRENT.incrementAndGet();
updateMax(TCP_PENDING_FRAMES_MAX, current);
if (ipMetrics != null) {
ipMetrics.recordTcpPendingAdded(event.getSourceIp(), event.getSourcePort());
}
}
private SyslogIngressEvent removeFirstPendingTcp() {
SyslogIngressEvent event = pendingTcp.pollFirst();
if (event == null) return null;
decrementIfPositive(TCP_PENDING_FRAMES_CURRENT);
if (ipMetrics != null) {
ipMetrics.recordTcpPendingRemoved(event.getSourceIp(), event.getSourcePort());
}
return event;
}
private void clearPendingTcp() {
while (!pendingTcp.isEmpty()) removeFirstPendingTcp();
}
private static void updateMax(AtomicLong maximum, long candidate) {
long current = maximum.get();
while (candidate > current && !maximum.compareAndSet(current, candidate)) {
current = maximum.get();
}
}
private static void decrementIfPositive(AtomicLong value) {
long current = value.get();
while (current > 0L && !value.compareAndSet(current, current - 1L)) {
current = value.get();
}
}
private boolean shouldDropBeforeWal(String sourceIp, String protocol) {
DeviceContextResolver.IngressStatus status = deviceResolver.resolveIngress(sourceIp, protocol);
if (status == DeviceContextResolver.IngressStatus.REGISTERED) return false;
if (status == DeviceContextResolver.IngressStatus.INVALID) {
INVALID_DEVICE_DROPPED.increment();
if (ipMetrics != null) ipMetrics.recordInvalidDeviceDropped(sourceIp, protocol);
return true;
}
// 后台快照暂不可用时不访问数据库、不阻塞网络线程;保守进入 WAL 等待 Dispatcher 判断。
DEVICE_LOOKUP_FAILED.increment();
if (ipMetrics != null) ipMetrics.recordDeviceLookupFailed(sourceIp, protocol);
return false;
}
private void recordOversized(SyslogIngressEvent event, String protocol) {
OVERSIZED_DROPPED.increment();
if (ipMetrics != null) {
ipMetrics.recordOversizedDropped(event.getSourceIp(), event.getSourcePort(), protocol,
event.getRawMessage().length());
}
log.error("Syslog 超过单条消息硬上限,已拒绝且不记录原文: eventId={}, protocol={}, sourceIp={}, chars={}, rejectedTotal={}",
event.getEventId(), protocol, event.getSourceIp(), event.getRawMessage().length(),
OVERSIZED_DROPPED.sum());
}
private InetSocketAddress remoteAddress(ChannelHandlerContext ctx) {
SocketAddress address = ctx.channel().remoteAddress();
return address instanceof InetSocketAddress ? (InetSocketAddress) address : null;
}
}
@@ -2,6 +2,7 @@ package com.netty;
import com.haobang.config.AppProperties;
import com.haobang.dispatch.DeviceContextResolver;
import com.haobang.diagnostics.SyslogIpMetricsRegistry;
import com.haobang.util.SpringContextUtil;
import com.haobang.wal.SyslogWalService;
import io.netty.bootstrap.Bootstrap;
@@ -43,6 +44,7 @@ public class SyslogServer {
AppProperties properties = SpringContextUtil.getBean(AppProperties.class);
SyslogWalService walService = SpringContextUtil.getBean(SyslogWalService.class);
DeviceContextResolver deviceResolver = SpringContextUtil.getBean(DeviceContextResolver.class);
SyslogIpMetricsRegistry ipMetrics = SpringContextUtil.getBean(SyslogIpMetricsRegistry.class);
Charset charset = walService.getSyslogCharset();
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
@@ -53,7 +55,7 @@ public class SyslogServer {
.channel(NioServerSocketChannel.class)
.childHandler(new SyslogTcpChannelInitializer(
properties.getSyslogMaxFrameLength(), charset, walService, deviceResolver,
!properties.isSyslogDispatchTerminalRecordEnabled()))
!properties.isSyslogDispatchTerminalRecordEnabled(), ipMetrics))
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
@@ -71,6 +73,7 @@ public class SyslogServer {
AppProperties properties = SpringContextUtil.getBean(AppProperties.class);
SyslogWalService walService = SpringContextUtil.getBean(SyslogWalService.class);
DeviceContextResolver deviceResolver = SpringContextUtil.getBean(DeviceContextResolver.class);
SyslogIpMetricsRegistry ipMetrics = SpringContextUtil.getBean(SyslogIpMetricsRegistry.class);
Charset charset = walService.getSyslogCharset();
EventLoopGroup group = new NioEventLoopGroup();
@@ -79,7 +82,7 @@ public class SyslogServer {
b.group(group)
.channel(NioDatagramChannel.class)
.handler(new SyslogUdpChannelInitializer(charset, walService, deviceResolver,
!properties.isSyslogDispatchTerminalRecordEnabled()))
!properties.isSyslogDispatchTerminalRecordEnabled(), ipMetrics))
.option(ChannelOption.SO_BROADCAST, true);
ChannelFuture f = b.bind(udpPort).sync();
@@ -17,6 +17,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import com.haobang.config.AppProperties;
import com.haobang.dispatch.DeviceContextResolver;
import com.haobang.diagnostics.SyslogIpMetricsRegistry;
import com.haobang.wal.SyslogWalService;
import java.nio.charset.Charset;
@@ -29,6 +30,7 @@ public class SyslogServerBoth {
private final AppProperties properties;
private final SyslogWalService walService;
private final DeviceContextResolver deviceResolver;
private final SyslogIpMetricsRegistry ipMetrics;
private final Charset syslogCharset;
private final int tcpPort;
private final int udpPort;
@@ -44,10 +46,12 @@ public class SyslogServerBoth {
private final ExecutorService serverExecutor = Executors.newFixedThreadPool(2);
public SyslogServerBoth(AppProperties properties, SyslogWalService walService,
DeviceContextResolver deviceResolver, Charset syslogCharset) {
DeviceContextResolver deviceResolver, SyslogIpMetricsRegistry ipMetrics,
Charset syslogCharset) {
this.properties = java.util.Objects.requireNonNull(properties, "properties");
this.walService = java.util.Objects.requireNonNull(walService, "walService");
this.deviceResolver = java.util.Objects.requireNonNull(deviceResolver, "deviceResolver");
this.ipMetrics = java.util.Objects.requireNonNull(ipMetrics, "ipMetrics");
this.syslogCharset = java.util.Objects.requireNonNull(syslogCharset, "syslogCharset");
this.tcpPort = properties.getSyslogTcpPort();
this.udpPort = properties.getSyslogUdpPort();
@@ -118,7 +122,7 @@ public class SyslogServerBoth {
.channel(NioServerSocketChannel.class)
.childHandler(new SyslogTcpChannelInitializer(
properties.getSyslogMaxFrameLength(), syslogCharset, walService, deviceResolver,
!properties.isSyslogDispatchTerminalRecordEnabled()))
!properties.isSyslogDispatchTerminalRecordEnabled(), ipMetrics))
// [SYSLOG-ALARM][NINGRESS][已修复] backlog/接收缓冲配置化;实际生效上限仍受操作系统 somaxconn/rmem_max 约束。
.option(ChannelOption.SO_BACKLOG, properties.getSyslogTcpBacklog())
.childOption(ChannelOption.SO_KEEPALIVE, true)
@@ -141,7 +145,7 @@ public class SyslogServerBoth {
b.group(udpGroup)
.channel(NioDatagramChannel.class)
.handler(new SyslogUdpChannelInitializer(syslogCharset, walService, deviceResolver,
!properties.isSyslogDispatchTerminalRecordEnabled()))
!properties.isSyslogDispatchTerminalRecordEnabled(), ipMetrics))
// [SYSLOG-ALARM][NINGRESS][已修复] 显式配置 UDP 接收缓冲;UDP 仍无端到端确认,强可靠设备应改用 TCP/TLS 或 RELP。
.option(ChannelOption.SO_BROADCAST, true)
.option(ChannelOption.SO_RCVBUF, properties.getSyslogUdpReceiveBufferBytes());
@@ -2,6 +2,7 @@ package com.netty;
import com.haobang.config.AppProperties;
import com.haobang.dispatch.DeviceContextResolver;
import com.haobang.diagnostics.SyslogIpMetricsRegistry;
import com.haobang.dispatch.SyslogWalDispatcher;
import com.haobang.wal.SyslogWalService;
import org.slf4j.Logger;
@@ -26,17 +27,20 @@ public class SyslogServerLifecycle implements SmartLifecycle {
private final AppProperties properties;
private final SyslogWalService walService;
private final DeviceContextResolver deviceResolver;
private final SyslogIpMetricsRegistry ipMetrics;
@SuppressWarnings("unused")
private final SyslogWalDispatcher dispatcher;
private final AtomicBoolean running = new AtomicBoolean();
private SyslogServerBoth server;
public SyslogServerLifecycle(AppProperties properties, SyslogWalService walService,
SyslogWalDispatcher dispatcher, DeviceContextResolver deviceResolver) {
SyslogWalDispatcher dispatcher, DeviceContextResolver deviceResolver,
SyslogIpMetricsRegistry ipMetrics) {
this.properties = properties;
this.walService = walService;
this.dispatcher = dispatcher;
this.deviceResolver = deviceResolver;
this.ipMetrics = ipMetrics;
}
@Override
@@ -44,7 +48,7 @@ public class SyslogServerLifecycle implements SmartLifecycle {
if (!running.compareAndSet(false, true)) return;
try {
Charset charset = walService.getSyslogCharset();
server = new SyslogServerBoth(properties, walService, deviceResolver, charset);
server = new SyslogServerBoth(properties, walService, deviceResolver, ipMetrics, charset);
server.start();
log.info("Syslog TCP/UDP 端口已在 WAL 和 Dispatcher 就绪后开放: charset={}", charset.name());
} catch (Exception failure) {
@@ -2,6 +2,7 @@ package com.netty;
import com.haobang.dispatch.DeviceContextResolver;
import com.haobang.diagnostics.SyslogIpMetricsRegistry;
import com.haobang.wal.SyslogWalService;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
@@ -21,15 +22,18 @@ public class SyslogTcpChannelInitializer extends ChannelInitializer<SocketChanne
private final SyslogWalService walService;
private final DeviceContextResolver deviceResolver;
private final boolean dropInvalidBeforeWal;
private final SyslogIpMetricsRegistry ipMetrics;
SyslogTcpChannelInitializer(int maxFrameLength, Charset syslogCharset, SyslogWalService walService,
DeviceContextResolver deviceResolver, boolean dropInvalidBeforeWal) {
DeviceContextResolver deviceResolver, boolean dropInvalidBeforeWal,
SyslogIpMetricsRegistry ipMetrics) {
if (maxFrameLength <= 0) throw new IllegalArgumentException("maxFrameLength must be positive");
this.maxFrameLength = maxFrameLength;
this.syslogCharset = java.util.Objects.requireNonNull(syslogCharset, "syslogCharset");
this.walService = java.util.Objects.requireNonNull(walService, "walService");
this.deviceResolver = java.util.Objects.requireNonNull(deviceResolver, "deviceResolver");
this.dropInvalidBeforeWal = dropInvalidBeforeWal;
this.ipMetrics = java.util.Objects.requireNonNull(ipMetrics, "ipMetrics");
}
@Override
@@ -47,6 +51,6 @@ public class SyslogTcpChannelInitializer extends ChannelInitializer<SocketChanne
// 添加业务处理器
pipeline.addLast("handler", new SyslogMessageHandler(
syslogCharset, walService, deviceResolver, dropInvalidBeforeWal));
syslogCharset, walService, deviceResolver, dropInvalidBeforeWal, ipMetrics));
}
}
@@ -1,6 +1,7 @@
package com.netty;
import com.haobang.dispatch.DeviceContextResolver;
import com.haobang.diagnostics.SyslogIpMetricsRegistry;
import com.haobang.wal.SyslogWalService;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
@@ -17,13 +18,16 @@ public class SyslogUdpChannelInitializer extends ChannelInitializer<DatagramChan
private final SyslogWalService walService;
private final DeviceContextResolver deviceResolver;
private final boolean dropInvalidBeforeWal;
private final SyslogIpMetricsRegistry ipMetrics;
SyslogUdpChannelInitializer(Charset syslogCharset, SyslogWalService walService,
DeviceContextResolver deviceResolver, boolean dropInvalidBeforeWal) {
DeviceContextResolver deviceResolver, boolean dropInvalidBeforeWal,
SyslogIpMetricsRegistry ipMetrics) {
this.syslogCharset = java.util.Objects.requireNonNull(syslogCharset, "syslogCharset");
this.walService = java.util.Objects.requireNonNull(walService, "walService");
this.deviceResolver = java.util.Objects.requireNonNull(deviceResolver, "deviceResolver");
this.dropInvalidBeforeWal = dropInvalidBeforeWal;
this.ipMetrics = java.util.Objects.requireNonNull(ipMetrics, "ipMetrics");
}
@Override
@@ -36,6 +40,6 @@ public class SyslogUdpChannelInitializer extends ChannelInitializer<DatagramChan
// 添加业务处理器
pipeline.addLast("handler", new SyslogMessageHandler(
syslogCharset, walService, deviceResolver, dropInvalidBeforeWal));
syslogCharset, walService, deviceResolver, dropInvalidBeforeWal, ipMetrics));
}
}