An offline, reproducible incident forensics cockpit written in MoonBit
EvidenceRecord → Profile → CanonicalEvent → Processor → CorrelationRule → IncidentGraph.
├── cmd/main/ # 最小 CLI,贯通 JSONL 分析与摘要校验
├── samples/incidents/ # 固定时间的脱敏故障夹具
├── canonical_event.mbt # 统一事件、资源、证据来源和关系边模型
├── event_adapter.mbt # JSONL/文本到事件的显式适配
├── correlation.mbt # 跨来源事件关联
├── integrity.mbt # 证据摘要及清单校验
├── ingest_jsonl.mbt # JSONL 导入
├── ingest_plain_text.mbt # 纯文本日志导入
├── mapping_profile.mbt # 版本化 JSONL 映射 Profile
├── processor.mbt # CanonicalEvent 转换与筛选管线
├── correlation_rule.mbt # 声明式时序关联规则与事件图边
├── graph.mbt # 稳定节点、关系边和可解释诊断的图构建器
├── packs.mbt # 配置、崩溃和延迟分析包
├── normalize.mbt # 时间、级别和来源标准化
├── report.mbt # Markdown 与 JSON 报告
├── rules.mbt # 诊断规则和证据引用
├── timeline.mbt # 稳定时间线与窗口查询
├── moon.mod # MoonBit 模块元数据
└── moon.pkg # 根库包配置moon fmt
moon check --deny-warn
moon build
moon testmoon run cmd/main -- ingest '{"event_id":"evt-1","severity":"INFO"}'
moon run cmd/main -- analyze '{"event_id":"evt-1","timestamp":"2026-03-08T09:00:00Z","source":"cli","severity":"INFO","resource":"demo-service"}'
moon run cmd/main -- verify 'evidence bytes'
moon run cmd/main -- verify 'evidence bytes' 9d11f9a71c12d6194481f5fa5086b0eff7df05a4a228f022f55bd890009a9d16moon run cmd/main -- case '<jsonl-source>'
moon run cmd/main -- case '<jsonl-source>' auto-v1 config
moon run cmd/main -- analyze '<jsonl-source>' auto-v1 crashpub(all) struct AnalysisPack {
pack_id : String
title : String
description : String
rules : Array[CorrelationRule]
} derive(Eq, ToJson, Debug)pub(all) struct CanonicalEvent {
event_id : String
event_time : NormalizedTimestamp?
observed_time : NormalizedTimestamp?
source : TelemetrySource
resource : ResourceRef?
category : EventCategory
event_type : String
action : String?
outcome : EventOutcome
severity : Severity?
body : Json
raw_content : String
attributes : Json
provenance : EventProvenance
} derive(Eq, ToJson, Debug)pub(all) struct Case {
case_id : String
title : String
timezone : String
collected_at : Timestamp
evidence : Array[EvidenceItem]
metadata : EvidenceMetadata
} derive(Eq, ToJson, Debug)pub(all) struct CaseAnalysis {
events : Array[CanonicalEvent]
graph : IncidentGraph
report : ForensicReport
} derive(Eq, ToJson, Debug)pub(all) struct CaseEvidenceInput {
document : EvidenceDocument
events : Array[CanonicalEvent]
} derive(Eq, Debug)pub(all) struct CorrelationEvent {
event : NormalizedEvent
resource_id : String
kind : CorrelationKind
} derive(Eq, ToJson, Debug)pub(all) struct CorrelationGroup {
resource_id : String
events : Array[CorrelationEvent]
} derive(Eq, ToJson, Debug)pub(all) struct CorrelationRule {
rule_id : String
from : EventPattern
to : EventPattern
within_seconds : Int
same_resource : Bool
relation_type : RelationType
explanation : String
} derive(Eq, ToJson, Debug)pub(all) struct DiagnosticFinding {
rule_id : String
title : String
explanation : String
kind : FindingKind
confidence_note : String
evidence : Array[EvidenceReference]
} derive(Eq, ToJson, Debug)pub(all) struct DiagnosticRule {
rule_id : String
title : String
explanation : String
kind : FindingKind
confidence_note : String
severity : Severity?
text_contains : String?
} derive(Eq, ToJson, Debug)pub(all) struct EventPattern {
category : EventCategory?
event_type : String?
action : String?
outcome : EventOutcome?
severity : Severity?
source_system : String?
source_component : String?
resource_kind : ResourceKind?
} derive(Eq, ToJson, Debug)pub(all) struct EvidenceDocument {
id : String
path : String
source : EvidenceSource
content : Bytes
} derive(Eq, Debug)pub(all) struct EvidenceItem {
id : String
source : EvidenceSource
path : String
format : EvidenceFormat
captured_at : Timestamp?
metadata : EvidenceMetadata
} derive(Eq, ToJson, Debug)pub(all) struct EvidenceManifest {
entries : Array[EvidenceManifestEntry]
} derive(Eq, ToJson, Debug)pub(all) struct EvidenceReference {
event_index : Int
source_id : String
timestamp : NormalizedTimestamp?
raw_content : String
} derive(Eq, ToJson, Debug)pub(all) struct EvidenceRelation {
relation_id : String
relation_type : RelationType
from_event_id : String
to_event_id : String
rule_id : String?
resource_key : ResourceRef?
delta_seconds : Int?
evidence_refs : Array[EventProvenance]
explanation : String
status : RelationStatus
} derive(Eq, ToJson, Debug)pub(all) struct ForensicReport {
case : Case
summary : String
timeline : Timeline
findings : DiagnosticReport
evidence_index : EvidenceManifest
limitations : Array[String]
} derive(Eq, ToJson, Debug)pub(all) struct GraphDiagnostic {
code : String
message : String
event_id : String?
provenance : EventProvenance?
} derive(Eq, ToJson, Debug)pub(all) struct IncidentEvent {
event_id : String
normalized : NormalizedEvent
resource_id : String?
kind : CorrelationKind
attributes : Json
evidence : EventEvidence
} derive(Eq, ToJson, Debug)pub(all) struct IncidentGraph {
nodes : Array[CanonicalEvent]
edges : Array[EvidenceRelation]
diagnostics : Array[GraphDiagnostic]
} derive(Eq, ToJson, Debug)pub(all) struct JsonlEventMapping {
system : String
source_field : String
event_id_field : String
timestamp_field : String
severity_field : String
resource_field : String?
resource_override : String?
kind : CorrelationKind
evidence_id : String
} derive(Eq, Debug)pub(all) struct JsonlMappingProfile {
id : String
version : String
source : SourceMappingProfile
event_id : ProfileStringValue
event_time : JsonFieldPath?
observed_time : JsonFieldPath?
severity : JsonFieldPath?
resource : ResourceMappingProfile?
category : EventCategory
event_type : ProfileStringValue
action : ProfileStringValue?
outcome : EventOutcome
body_path : JsonFieldPath?
} derive(Eq, Debug)pub(all) struct NormalizedEvent {
timestamp : NormalizedTimestamp?
severity : Severity?
source_id : String
raw_content : String
} derive(Eq, ToJson, Debug)pub(all) struct PlainTextEventMapping {
system : String
component : String
resource_id : String?
kind : CorrelationKind
evidence_id : String
} derive(Eq, Debug)pub(all) enum ProcessorStage {
Normalize
AliasResource(ResourceAlias)
FilterSource(String)
FilterCategory(EventCategory)
FilterResource(String)
FilterTimeWindow(NormalizedTimestamp, NormalizedTimestamp)
} derive(Eq, Debug)pub(all) struct ResourceMappingProfile {
kind : ResourceKind
id : ProfileStringValue
prefix : String
aliases : Array[ResourceAlias]
attributes_path : JsonFieldPath?
} derive(Eq, Debug)pub(all) struct ResourceRef {
kind : ResourceKind
id : String
attributes : Json
} derive(Eq, ToJson, Debug)pub(all) struct SourceMappingProfile {
system : ProfileStringValue
component : ProfileStringValue
host : ProfileStringValue?
instrumentation_scope : ProfileStringValue?
} derive(Eq, Debug)fn adapt_jsonl_records(records : Array[JsonlRecord], mapping : JsonlEventMapping) -> Array[IncidentEvent] raise EventAdaptErrorfn adapt_jsonl_records_as_canonical(records : Array[JsonlRecord], mapping : JsonlEventMapping) -> Array[CanonicalEvent] raise EventAdaptErrorfn adapt_plain_text_records(records : Array[PlainTextRecord], mapping : PlainTextEventMapping) -> Array[IncidentEvent] raise EventAdaptErrorfn adapt_plain_text_records_as_canonical(records : Array[PlainTextRecord], mapping : PlainTextEventMapping) -> Array[CanonicalEvent] raise EventAdaptErrorfn analyze_case(case : Case, inputs : Array[CaseEvidenceInput], pipeline : ProcessorPipeline?, pack : AnalysisPack?, summary : String, limitations : Array[String]) -> CaseAnalysis raise CaseAnalysisErrorfn analyze_with_pack(events : Array[CanonicalEvent], pack : AnalysisPack) -> IncidentGraph raise CorrelationRuleErrorfn apply_jsonl_profile(records : Array[JsonlRecord], profile : JsonlMappingProfile, evidence : ProfileEvidence) -> Array[CanonicalEvent] raise ProfileMapErrorfn build_case_jsonl_evidence(document : EvidenceDocument, records : Array[JsonlRecord], profile : JsonlMappingProfile) -> CaseEvidenceInput raise ProfileMapErrortest {
let source = EvidenceSource::{
system: "host",
component: "service",
host: None,
}
let document = EvidenceDocument::{
id: "e-1",
path: "service.log",
source,
content: b"abc",
}
let manifest = build_evidence_manifest([document])
assert_eq(manifest.entries[0].size_bytes, 3)
}fn build_incident_graph(events : Array[CanonicalEvent], rules : Array[CorrelationRule]) -> IncidentGraph raise CorrelationRuleErrortest {
let event = NormalizedEvent::{
timestamp: None,
severity: None,
source_id: "svc/api",
raw_content: "timestamp unavailable",
}
let timeline = build_timeline([event])
assert_eq(timeline.events.length(), 1)
}fn canonicalize_incident_event(event : IncidentEvent, source : TelemetrySource, resource : ResourceRef?, category : EventCategory, event_type : String, action : String?, outcome : EventOutcome, observed_time : NormalizedTimestamp?, provenance : EventProvenance) -> CanonicalEventfn correlate_events(events : Array[CorrelationEvent], window_seconds : Int) -> Array[CorrelationGroup] raise CorrelationErrortest {
let time = NormalizedTimestamp::{
epoch_day: 1,
second_of_day: 10,
nanosecond: 0,
raw: "start",
}
let base = NormalizedEvent::{
timestamp: Some(time),
severity: None,
source_id: "config/controller",
raw_content: "pool size changed",
}
let health = NormalizedEvent::{
timestamp: Some(time),
severity: None,
source_id: "health/checker",
raw_content: "health check failed",
}
let groups = correlate_events(
[
CorrelationEvent::{
event: base,
resource_id: "orders-api",
kind: CorrelationKind::Change,
},
CorrelationEvent::{
event: health,
resource_id: "orders-api",
kind: CorrelationKind::Alert,
},
],
30,
)
assert_eq(groups.length(), 1)
assert_eq(groups[0].events.length(), 2)
}fn correlate_incident_events(events : Array[IncidentEvent], window_seconds : Int) -> Array[CorrelationGroup] raise CorrelationErrorfn evaluate_correlation_rules(events : Array[CanonicalEvent], rules : Array[CorrelationRule]) -> IncidentGraph raise CorrelationRuleErrorfn evaluate_diagnostic_rules(timeline : Timeline, rules : Array[DiagnosticRule]) -> DiagnosticReporttest {
let event = NormalizedEvent::{
timestamp: None,
severity: Some(Severity::Error),
source_id: "svc/api",
raw_content: "database connection failed",
}
let timeline = build_timeline([event])
let rule = DiagnosticRule::{
rule_id: "db-error",
title: "Database error observed",
explanation: "The log reports a database connection failure.",
kind: FindingKind::Observation,
confidence_note: "Directly present in the source log.",
severity: Some(Severity::Error),
text_contains: Some("database connection"),
}
let report = evaluate_diagnostic_rules(timeline, [rule])
assert_eq(report.findings.length(), 1)
}test {
let event = NormalizedEvent::{
timestamp: None,
severity: None,
source_id: "svc/api",
raw_content: "entry",
}
let timeline = build_timeline([event])
let filtered = filter_timeline_by_source(timeline, "svc/api")
assert_eq(filtered.events.length(), 1)
}fn new_profile_catalog(profiles : Array[JsonlMappingProfile]) -> ProfileCatalog raise ProfileCatalogErrorfn normalize_event(timestamp : String?, severity : String?, system : String, component : String, host : String?, raw_content : String) -> NormalizedEvent raise NormalizationErrorfn normalize_source_id(system : String, component : String, host : String?) -> Stringtest {
let records = parse_jsonl("{\"event_id\":\"evt-1\"}\n")
assert_eq(records.length(), 1)
assert_eq(records[0].line_number, 1)
}test {
let records = parse_plain_text("INFO service started\nservice stopped\n")
assert_eq(records.length(), 2)
assert_eq(records[1].line_number, 2)
}fn process_events(events : Array[CanonicalEvent], pipeline : ProcessorPipeline) -> Array[CanonicalEvent] raise ProcessorErrorfn query_timeline_window(timeline : Timeline, start : NormalizedTimestamp, end : NormalizedTimestamp) -> Timeline raise TimelineErrortest {
let time = NormalizedTimestamp::{
epoch_day: 1,
second_of_day: 10,
nanosecond: 0,
raw: "start",
}
let event = NormalizedEvent::{
timestamp: Some(time),
severity: None,
source_id: "svc/api",
raw_content: "entry",
}
let timeline = build_timeline([event])
let result = query_timeline_window(timeline, time, time)
assert_eq(result.events.length(), 1)
}fn register_profile(catalog : ProfileCatalog, profile : JsonlMappingProfile) -> ProfileCatalog raise ProfileCatalogErrortest {
let timeline = build_timeline([])
let report = ForensicReport::{
case: Case::{
case_id: "case-1",
title: "Example incident",
timezone: "UTC",
collected_at: Timestamp::{ value: "2026-01-01T00:00:00Z", },
evidence: [],
metadata: EvidenceMetadata::{ entries: [], },
},
summary: "A reproducible sample case.",
timeline,
findings: DiagnosticReport::{ findings: [], },
evidence_index: EvidenceManifest::{ entries: [], },
limitations: [],
}
let json = render_json_report(report)
assert_true(json.contains("\"evidence_index\""))
assert_true(json.contains("\"limitations\""))
}test {
let timeline = build_timeline([])
let report = ForensicReport::{
case: Case::{
case_id: "case-1",
title: "Example incident",
timezone: "UTC",
collected_at: Timestamp::{ value: "2026-01-01T00:00:00Z", },
evidence: [],
metadata: EvidenceMetadata::{ entries: [], },
},
summary: "A reproducible sample case.",
timeline,
findings: DiagnosticReport::{ findings: [], },
evidence_index: EvidenceManifest::{ entries: [], },
limitations: ["No causal conclusion is asserted."],
}
let markdown = render_markdown_report(report)
assert_true(markdown.contains("## 摘要"))
assert_true(markdown.contains("## 限制说明"))
}fn resolve_profile(catalog : ProfileCatalog, id : String, version : String) -> JsonlMappingProfile raise ProfileCatalogErrorfn verify_evidence_manifest(manifest : EvidenceManifest, documents : Array[EvidenceDocument]) -> Booltest {
let source = EvidenceSource::{
system: "host",
component: "service",
host: None,
}
let document = EvidenceDocument::{
id: "e-1",
path: "service.log",
source,
content: b"abc",
}
let manifest = build_evidence_manifest([document])
let changed = EvidenceDocument::{
id: "e-1",
path: "service.log",
source,
content: b"abd",
}
assert_false(verify_evidence_manifest(manifest, [changed]))
}Install
Download zipAn offline, reproducible incident forensics cockpit written in MoonBit