2026-09-12 19:22:52 +08:00
|
|
|
package com.common.service;
|
|
|
|
|
|
|
|
|
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
|
|
|
|
|
|
|
|
import java.time.LocalDate;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-09-13 06:11:12 +08:00
|
|
|
* Batch-aggregated delta shared by the after-commit memory publisher and legacy durable Outbox.
|
2026-09-12 19:22:52 +08:00
|
|
|
*
|
|
|
|
|
* <p>The event is additive/max-min only, so events from different application nodes may be
|
|
|
|
|
* published in a different order without changing the final projection. Kafka duplicates are
|
|
|
|
|
* eliminated by {@code eventId} in the projection consumer transaction.</p>
|
|
|
|
|
*/
|
|
|
|
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
|
|
|
public class StatsProjectionOutboxEvent {
|
|
|
|
|
public static final String SCHEMA = "SYSLOG_STATS_PROJECTION_V1";
|
|
|
|
|
public static final String KAFKA_KEY = "SYSLOG_STATS_PROJECTION";
|
|
|
|
|
|
|
|
|
|
private String schema = SCHEMA;
|
|
|
|
|
private String eventId = UUID.randomUUID().toString();
|
2026-09-13 06:11:12 +08:00
|
|
|
// 老版消息没有此字段,默认true,兼容清理升级前的Outbox。
|
|
|
|
|
private boolean outboxBacked = true;
|
|
|
|
|
|
|
|
|
|
public boolean isOutboxBacked() { return outboxBacked; }
|
|
|
|
|
public void setOutboxBacked(boolean outboxBacked) { this.outboxBacked = outboxBacked; }
|
2026-09-12 19:22:52 +08:00
|
|
|
private List<DeviceDelta> devices = new ArrayList<>();
|
|
|
|
|
private List<RuleDelta> rules = new ArrayList<>();
|
|
|
|
|
private List<CollectDelta> collects = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
public static StatsProjectionOutboxEvent fromAccumulator(DirectStatsAccumulator accumulator) {
|
|
|
|
|
StatsProjectionOutboxEvent event = new StatsProjectionOutboxEvent();
|
|
|
|
|
for (Map.Entry<DirectStatsAccumulator.DailyKey, DirectStatsAccumulator.DeviceDelta> entry
|
|
|
|
|
: accumulator.getDevices().entrySet()) {
|
|
|
|
|
DirectStatsAccumulator.DeviceDelta value = entry.getValue();
|
|
|
|
|
if (value.getParseDelta() != 0L || value.getNonLogDelta() != 0L) {
|
|
|
|
|
event.devices.add(DeviceDelta.counts(entry.getKey().getDate(),
|
|
|
|
|
entry.getKey().getDimensionId(), value.getParseDelta(), value.getNonLogDelta()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (Map.Entry<DirectStatsAccumulator.DailyKey, DirectStatsAccumulator.RuleDelta> entry
|
|
|
|
|
: accumulator.getRules().entrySet()) {
|
|
|
|
|
if (entry.getValue().getMaxLogTime() != null) {
|
|
|
|
|
event.rules.add(RuleDelta.max(entry.getKey().getDate(),
|
|
|
|
|
entry.getKey().getDimensionId(), entry.getValue().getMaxLogTime()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return event;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static StatsProjectionOutboxEvent fromIngress(IngressStatsProjectionWriter.Aggregation aggregation) {
|
|
|
|
|
StatsProjectionOutboxEvent event = new StatsProjectionOutboxEvent();
|
|
|
|
|
for (Map.Entry<IngressStatsProjectionWriter.DeviceKey, LocalDateTime> entry
|
|
|
|
|
: aggregation.devices.entrySet()) {
|
|
|
|
|
event.devices.add(DeviceDelta.received(entry.getKey().date,
|
|
|
|
|
entry.getKey().deviceId, entry.getValue()));
|
|
|
|
|
}
|
|
|
|
|
for (Map.Entry<Long, IngressStatsProjectionWriter.CollectTimes> entry
|
|
|
|
|
: aggregation.collects.entrySet()) {
|
|
|
|
|
event.collects.add(CollectDelta.range(entry.getKey(),
|
|
|
|
|
entry.getValue().min, entry.getValue().max));
|
|
|
|
|
}
|
|
|
|
|
return event;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void validate() {
|
|
|
|
|
if (!SCHEMA.equals(schema)) {
|
|
|
|
|
throw new IllegalArgumentException("Unsupported stats projection schema: " + schema);
|
|
|
|
|
}
|
|
|
|
|
if (eventId == null || eventId.trim().isEmpty()) {
|
|
|
|
|
throw new IllegalArgumentException("Stats projection event is missing eventId");
|
|
|
|
|
}
|
|
|
|
|
if (devices == null || rules == null || collects == null) {
|
|
|
|
|
throw new IllegalArgumentException("Stats projection event contains null collections");
|
|
|
|
|
}
|
|
|
|
|
if (devices.isEmpty() && rules.isEmpty() && collects.isEmpty()) {
|
|
|
|
|
throw new IllegalArgumentException("Stats projection event has no changes");
|
|
|
|
|
}
|
|
|
|
|
for (DeviceDelta delta : devices) delta.validate();
|
|
|
|
|
for (RuleDelta delta : rules) delta.validate();
|
|
|
|
|
for (CollectDelta delta : collects) delta.validate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isEmpty() {
|
|
|
|
|
return devices.isEmpty() && rules.isEmpty() && collects.isEmpty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getSchema() { return schema; }
|
|
|
|
|
public void setSchema(String schema) { this.schema = schema; }
|
|
|
|
|
public String getEventId() { return eventId; }
|
|
|
|
|
public void setEventId(String eventId) { this.eventId = eventId; }
|
|
|
|
|
public List<DeviceDelta> getDevices() { return devices; }
|
|
|
|
|
public void setDevices(List<DeviceDelta> devices) { this.devices = devices; }
|
|
|
|
|
public List<RuleDelta> getRules() { return rules; }
|
|
|
|
|
public void setRules(List<RuleDelta> rules) { this.rules = rules; }
|
|
|
|
|
public List<CollectDelta> getCollects() { return collects; }
|
|
|
|
|
public void setCollects(List<CollectDelta> collects) { this.collects = collects; }
|
|
|
|
|
|
|
|
|
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
|
|
|
public static class DeviceDelta {
|
|
|
|
|
private String statDate;
|
|
|
|
|
private long deviceId;
|
|
|
|
|
private long parseDelta;
|
|
|
|
|
private long nonLogDelta;
|
|
|
|
|
private String lastReceiveTime;
|
|
|
|
|
|
|
|
|
|
public DeviceDelta() {}
|
|
|
|
|
|
|
|
|
|
static DeviceDelta counts(LocalDate date, long deviceId, long parseDelta, long nonLogDelta) {
|
|
|
|
|
DeviceDelta result = new DeviceDelta();
|
|
|
|
|
result.statDate = date.toString();
|
|
|
|
|
result.deviceId = deviceId;
|
|
|
|
|
result.parseDelta = parseDelta;
|
|
|
|
|
result.nonLogDelta = nonLogDelta;
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static DeviceDelta received(LocalDate date, long deviceId, LocalDateTime receiveTime) {
|
|
|
|
|
DeviceDelta result = counts(date, deviceId, 0L, 0L);
|
|
|
|
|
result.lastReceiveTime = receiveTime.toString();
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void validate() {
|
|
|
|
|
date();
|
|
|
|
|
if (deviceId <= 0L) throw new IllegalArgumentException("Invalid device projection id");
|
|
|
|
|
if (parseDelta == 0L && nonLogDelta == 0L && receiveTime() == null) {
|
|
|
|
|
throw new IllegalArgumentException("Device projection delta is empty");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LocalDate date() { return LocalDate.parse(statDate); }
|
|
|
|
|
public LocalDateTime receiveTime() {
|
|
|
|
|
return lastReceiveTime == null || lastReceiveTime.trim().isEmpty()
|
|
|
|
|
? null : LocalDateTime.parse(lastReceiveTime);
|
|
|
|
|
}
|
|
|
|
|
public String getStatDate() { return statDate; }
|
|
|
|
|
public void setStatDate(String statDate) { this.statDate = statDate; }
|
|
|
|
|
public long getDeviceId() { return deviceId; }
|
|
|
|
|
public void setDeviceId(long deviceId) { this.deviceId = deviceId; }
|
|
|
|
|
public long getParseDelta() { return parseDelta; }
|
|
|
|
|
public void setParseDelta(long parseDelta) { this.parseDelta = parseDelta; }
|
|
|
|
|
public long getNonLogDelta() { return nonLogDelta; }
|
|
|
|
|
public void setNonLogDelta(long nonLogDelta) { this.nonLogDelta = nonLogDelta; }
|
|
|
|
|
public String getLastReceiveTime() { return lastReceiveTime; }
|
|
|
|
|
public void setLastReceiveTime(String lastReceiveTime) { this.lastReceiveTime = lastReceiveTime; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
|
|
|
public static class RuleDelta {
|
|
|
|
|
private String statDate;
|
|
|
|
|
private long normalizeRuleId;
|
|
|
|
|
private String maxLogTime;
|
|
|
|
|
|
|
|
|
|
public RuleDelta() {}
|
|
|
|
|
|
|
|
|
|
static RuleDelta max(LocalDate date, long ruleId, LocalDateTime maxLogTime) {
|
|
|
|
|
RuleDelta result = new RuleDelta();
|
|
|
|
|
result.statDate = date.toString();
|
|
|
|
|
result.normalizeRuleId = ruleId;
|
|
|
|
|
result.maxLogTime = maxLogTime.toString();
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void validate() {
|
|
|
|
|
date();
|
|
|
|
|
if (normalizeRuleId <= 0L || time() == null) {
|
|
|
|
|
throw new IllegalArgumentException("Invalid rule projection delta");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LocalDate date() { return LocalDate.parse(statDate); }
|
|
|
|
|
public LocalDateTime time() {
|
|
|
|
|
return maxLogTime == null || maxLogTime.trim().isEmpty()
|
|
|
|
|
? null : LocalDateTime.parse(maxLogTime);
|
|
|
|
|
}
|
|
|
|
|
public String getStatDate() { return statDate; }
|
|
|
|
|
public void setStatDate(String statDate) { this.statDate = statDate; }
|
|
|
|
|
public long getNormalizeRuleId() { return normalizeRuleId; }
|
|
|
|
|
public void setNormalizeRuleId(long normalizeRuleId) { this.normalizeRuleId = normalizeRuleId; }
|
|
|
|
|
public String getMaxLogTime() { return maxLogTime; }
|
|
|
|
|
public void setMaxLogTime(String maxLogTime) { this.maxLogTime = maxLogTime; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
|
|
|
public static class CollectDelta {
|
|
|
|
|
private long deviceCollectId;
|
|
|
|
|
private String firstSuccessTime;
|
|
|
|
|
private String lastSuccessTime;
|
|
|
|
|
|
|
|
|
|
public CollectDelta() {}
|
|
|
|
|
|
|
|
|
|
static CollectDelta range(long collectId, LocalDateTime first, LocalDateTime last) {
|
|
|
|
|
CollectDelta result = new CollectDelta();
|
|
|
|
|
result.deviceCollectId = collectId;
|
|
|
|
|
result.firstSuccessTime = first == null ? null : first.toString();
|
|
|
|
|
result.lastSuccessTime = last == null ? null : last.toString();
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void validate() {
|
|
|
|
|
if (deviceCollectId <= 0L || (firstTime() == null && lastTime() == null)) {
|
|
|
|
|
throw new IllegalArgumentException("Invalid collector projection delta");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LocalDateTime firstTime() {
|
|
|
|
|
return firstSuccessTime == null || firstSuccessTime.trim().isEmpty()
|
|
|
|
|
? null : LocalDateTime.parse(firstSuccessTime);
|
|
|
|
|
}
|
|
|
|
|
public LocalDateTime lastTime() {
|
|
|
|
|
return lastSuccessTime == null || lastSuccessTime.trim().isEmpty()
|
|
|
|
|
? null : LocalDateTime.parse(lastSuccessTime);
|
|
|
|
|
}
|
|
|
|
|
public long getDeviceCollectId() { return deviceCollectId; }
|
|
|
|
|
public void setDeviceCollectId(long deviceCollectId) { this.deviceCollectId = deviceCollectId; }
|
|
|
|
|
public String getFirstSuccessTime() { return firstSuccessTime; }
|
|
|
|
|
public void setFirstSuccessTime(String firstSuccessTime) { this.firstSuccessTime = firstSuccessTime; }
|
|
|
|
|
public String getLastSuccessTime() { return lastSuccessTime; }
|
|
|
|
|
public void setLastSuccessTime(String lastSuccessTime) { this.lastSuccessTime = lastSuccessTime; }
|
|
|
|
|
}
|
|
|
|
|
}
|