moonforensics

    An offline, reproducible incident forensics cockpit written in MoonBit

    incident-response
    forensics
    logs
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    4 hours ago
    Downloads
    1

    #MoonForensics

    MoonForensics 是一个 MoonBit 应用服务离线故障复盘库,以发布或配置变更后的异常调查 为主场景:在已导出的变更、应用日志和告警中,整理同一服务或实例的事件顺序, 生成可复核的时间线、候选关联组与证据报告。它不替代日志平台,不自动宣布根因。

    目标用户是需要交接事故材料的值班运维、复核变更的发布负责人和调查错误上下文的开发者。 离线流程适合生产访问受限、材料需脱敏交接,以及同一批证据需要重复复核的情况。 项目价值是固定分析步骤与证据出处,不是未经测量的排障提速或生产采用率。

    当前交付包括分析库、静态夹具和最小 CLI。JSONL 与固定格式文本经过显式映射后进入 CanonicalEvent;Profile、Processor、声明式规则和三套 AnalysisPack 已可复用。 CLI 只接收调用方提供的内容,不读取主机文件,也不声称能自动理解任意供应商日志。

    #统一抽象与关联前提

    现有 NormalizedEvent 统一时间、级别、来源和原始文本;CorrelationEvent 进一步携带 资源键与 Change / Alert / Crash / Other 角色。调用方先明确字段映射和资源身份, 核心才按同一资源、至少两个不同来源、最早事件起算的闭区间窗口构建候选组。

    配置项、服务和实例不是天然相同的资源;只有显式选择共同分析范围才能关联。 不通过名称相似或日志关键词猜测身份。缺时间的事件不参与关联;适配器拒绝空资源键, 没有资源的事件只保留在时间线中。资源与时间关系不是因果证明。

    适配结果包含稳定事件 ID、证据 ID/行号和结构化属性,隔离供应商格式差异。 通用性来自“适配层处理输入差异,分析核心复用事件契约”,而不是自动理解所有格式。

    #可解释事件关系图架构

    项目的独立复用内核是可解释事件关系图,而不是某一种日志格式的解析器:

    EvidenceRecord → Profile → CanonicalEvent → Processor → CorrelationRule → IncidentGraph

    • CanonicalEvent 分开保存事件时间、观测时间、结构化来源、资源实体、类别、事件类型、 动作、结果、级别、正文、属性和 EventProvenance。
    • Profile 用版本化配置描述嵌套对象路径、常量/可选值、事件分类、资源前缀和精确别名; Profile 身份与版本写入证据溯源,输入格式变化不会污染分析核心。
    • Processor 由按声明顺序执行的阶段组成,负责结构化标识规范化、资源别名、来源/类别/资源 过滤和时间窗口筛选;所有处理保持确定性且不修改原始内容与证据溯源。
    • CorrelationRule 已支持按类别、事件类型、动作、结果、级别、来源和资源类型声明两侧模式, 用有序时间窗口及同资源约束生成可解释关系;规则只产生关系假设,不把时间相关性冒充根因。
    • AnalysisPack 将一组经过验证的规则和适用说明封装为可复用能力,内置配置变更、进程崩溃、 请求延迟三类分析包;调用方可直接选择分析包,也可检查其规则和元数据后再运行。
    • IncidentGraph 由构建器按事件时间稳定排序节点,并保存关系边和数据质量诊断。边包含规则 ID、资源键、时间差、证据位置、匹配原因和 Observed/Hypothesis 状态;时间线、发现和报告 均从图派生。

    当前 correlate_events 和 DiagnosticRule 仍作为兼容视图;声明式规则通过 build_incident_graph 生成带证据的关系图。调用方只应选择 Profile 和规则,不应逐条手工构造关联事件。

    #独立库复用

    MoonForensics 的核心包不依赖 cmd/main。完整的跨 Schema 用例在 cross_schema_test.mbt:config-controller 的 change_id、 applied_at、service 字段和 health-monitor 的 check_id、observed_at、 target.service 字段分别由两个 JsonlMappingProfile 映射,再进入同一个 config_analysis_pack()。测试检查两个来源能在同一资源和时间窗口内形成一条关系边, 并保留各自的 Profile 版本与证据来源。

    调用方只需提供输入记录、Profile 和证据元数据;库不会读取主机文件,也不会猜测缺失字段。 新的日志系统只需增加 Profile 及适配测试,规则、资源同一性和时间窗逻辑可以继续复用。 报告 API(render_markdown_report / render_json_report)可直接消费分析图, EventProvenance 则让同一 Profile 在多批证据上的来源保持可区分。

    设计参考公开标准的分层思想,不复制实现代码: OpenTelemetry 日志模型、 Collector 组件流水线、 Sigma 关联规则、 Plaso 解析器插件 和 ECS 事件分类。 它们分别对应本项目的统一事件、处理器、规则、适配器和分类模型。

    #三个完整使用场景

    仓库中的三个场景对应 samples/incidents/ 下的固定文件,测试入口是 e2e_test.mbt 和 cross_schema_test.mbt:

    1. 配置变更后的健康故障:config-change/changes.jsonl 中的 config-001 在 09:00:00Z 将连接池从 40 调为 4,health.log 在 09:00:40Z 记录失败, config-002 回滚后于 09:02:30Z 恢复。输出应保留“变更—失败—恢复”的顺序, 但只把它标成候选关系。
    2. 应用错误与进程退出:service-crash/app.log 与 process-events.jsonl 记录同一 Checkout API 实例的错误、非零退出和重启。退出码和进程 ID 会进入证据引用, 不把日志中的错误直接解释为 OOM。
    3. 指标异常与网关超时:performance-degradation/metrics.jsonl 与 gateway.log 同时提供实例指标和请求超时记录。时间线展示异常窗口,关联边说明匹配的实例与请求, 不把某一项指标升高写成故障原因。

    这些文件中的时间、ID 和主机名都是固定的虚构值,便于重复运行测试和人工复核。

    #能力概览

    • 案件与证据模型:案件、证据文件、来源、采集时间及键值元数据。
    • 离线导入:解析 JSONL 事件流和固定格式的纯文本日志,保留原文及记录顺序。
    • 可复用映射 Profile:以版本化 Profile 将不同 JSONL 对象映射为统一事件,支持嵌套字段、 可选值、资源前缀和精确别名,并对缺失或类型错误给出证据行号。
    • 统一事件信息:标准化 RFC 3339 时间、严重级别和来源标识,为比较及排序提供稳定字段。
    • 证据完整性:为证据内容生成 SHA-256 摘要、字节大小和来源清单;可用原始内容复核清单。
    • 确定性时间线:按时间稳定排序,按来源过滤,并查询包含边界的 UTC 时间窗口。
    • 诊断与关联:规则结果同时携带证据引用和置信说明;关联相同资源、限定窗口内、来自不同来源的事件。
    • 可复用分析包:提供 config、crash、latency 三套内置包,覆盖配置变更后的故障、 进程崩溃后的健康变化,以及请求延迟后的超时/错误;每套包都公开稳定 ID、说明和规则窗口。
    • 可读报告:输出 Markdown 摘要、时间线、发现、证据索引和限制说明,也可输出完整 JSON 数据。

    关联只表达资源和时间上的关系,不等同于因果证明。报告会保留这一边界,供调查人员结合原始证据复核。

    #项目结构

    . ├── 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 # 根库包配置

    #开发环境与验证

    需要安装 MoonBit 工具链和 Git。在仓库根目录执行:

    moon fmt moon check --deny-warn moon build moon test

    测试覆盖模型、导入、标准化、完整性、时间线、规则、关联和报告黄金快照。夹具 位于 samples/incidents/,其固定时间、字段契约和场景说明见 samples/README.md。

    发布前至少执行 moon info 并检查生成的 .mbti 无意外变化;GitHub Actions 会在全新 Ubuntu 环境中安装 MoonBit 最新稳定工具链并记录完整版本,执行格式、类型、构建、测试、 接口文件和文档 CLI 示例检查。

    发布检查还应确认 git status --short 为空、moon.mod 的模块名与 GitHub 账号一致、 LICENSE 和 samples/README.md 在仓库中存在,并从干净检出重新运行上述命令。

    #CLI 工作流

    命令行入口接受内联证据内容,便于离线复现和脚本调用。ingest、analyze、case、 verify 分别覆盖导入、分析、案件级处理和完整性校验;它们都不会隐式读取当前目录文件。

    moon 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' 9d11f9a71c12d6194481f5fa5086b0eff7df05a4a228f022f55bd890009a9d16

    case 命令使用版本化 Profile 将 JSONL 映射为 CanonicalEvent,再构建案件时间线和可解释关系图:

    moon 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 crash

    内置单流 Profile 包括 flat-v1、config-v1、crash-v1、health-v1 和 latency-v1。auto-v1 会按每条记录的 profile 字段选择这些 Profile,适合在同一案件中组合不同来源;第二个可选参数 选择 none、config、crash 或 latency 分析包。映射失败会返回退出码 3,未知 Profile/分析包 返回退出码 2,成功输出 Markdown 案件报告、证据清单和关系图摘要。

    成功输出 exit code 0。参数或命令错误为 exit code 2,JSONL/输入错误为 exit code 3,证据校验失败为 exit code 4;失败路径同时返回非零进程状态。 当前入口不直接读取主机文件,调用方可先读取文件内容再将其作为参数传入。 analyze 会通过显式 JSONL 字段映射生成统一事件、时间线和候选关联组;verify 的可选摘要参数 用于复核已有清单。缺少事件必需字段或摘要不匹配时返回非零退出码。

    #取证边界

    • 解析和报告面向已提供的证据内容,不会自行连接主机或采集运行时数据。
    • 时间线排序与事件关联是确定性的;缺失或错误时间不会被推断为精确时间。
    • 规则只按声明的严重级别和文本条件匹配,不会自动学习或扩展规则。
    • 时间接近、来源不同或资源标识相同,不足以单独证明根因。
    • 清单校验依赖调用方再次提供的证据字节;校验结果不能替代可信存储或签名机制。

    #许可证

    本项目采用 MIT License,详见 LICENSE。

    CaseAnalysisError

    pub(all) suberror CaseAnalysisError {
    InvalidCase(reason~ : String)
    MissingEvidence(id~ : String)
    UnexpectedEvidence(id~ : String)
    EvidenceMismatch(id~ : String, reason~ : String)
    InvalidProcessor(reason~ : String)
    InvalidPack(rule_id~ : String, reason~ : String)
    } derive(Eq,
    Debug
    )

    Case assembly stops before producing a partial report.

    CorrelationError

    pub(all) suberror CorrelationError {
    InvalidWindow
    } derive(Eq,
    Debug
    )

    A correlation request that cannot be evaluated because its window is negative.

    CorrelationRuleError

    pub(all) suberror CorrelationRuleError {
    InvalidRule(rule_id~ : String, reason~ : String)
    } derive(Eq,
    Debug
    )

    A declarative rule cannot be evaluated safely.

    EventAdaptError

    pub(all) suberror EventAdaptError {
    MissingField(line_number~ : Int, field~ : String)
    InvalidField(line_number~ : Int, field~ : String, value~ : String)
    InvalidText(line_number~ : Int, reason~ : String)
    } derive(Eq,
    Debug
    )

    A record cannot enter the common event contract until its required fields are valid.

    JsonlParseError

    pub(all) suberror JsonlParseError {
    InvalidJson(line_number~ : Int, line~ : String)
    ExpectedObject(line_number~ : Int, line~ : String)
    } derive(Eq,
    Debug
    )

    A JSONL syntax or record-shape error with its physical source line.

    NormalizationError

    pub(all) suberror NormalizationError {
    InvalidTimestamp(String)
    } derive(Eq,
    Debug
    )

    A normalization failure with the original timestamp that could not be read.

    ProcessorError

    pub(all) suberror ProcessorError {
    InvalidWindow
    InvalidStage(String)
    } derive(Eq,
    Debug
    )

    A pipeline configuration or stage cannot be applied safely.

    ProfileCatalogError

    pub(all) suberror ProfileCatalogError {
    InvalidProfile(field~ : String, reason~ : String)
    DuplicateProfile(id~ : String, version~ : String)
    MissingProfile(id~ : String, version~ : String)
    } derive(Eq,
    Debug
    )

    A profile cannot be registered or resolved safely.

    ProfileMapError

    pub(all) suberror ProfileMapError {
    InvalidProfile(field~ : String, reason~ : String)
    MissingValue(line_number~ : Int, path~ : String)
    InvalidValue(line_number~ : Int, path~ : String, value~ : String)
    } derive(Eq,
    Debug
    )

    A profile definition or input record could not be mapped safely.

    TimelineError

    pub(all) suberror TimelineError {
    InvalidWindow
    } derive(Eq,
    Debug
    )

    A query window that cannot be evaluated because its start follows its end.

    AnalysisPack

    pub(all) struct AnalysisPack {
    pack_id : String
    title : String
    description : String
    rules : Array[CorrelationRule]
    } derive(Eq, ToJson,
    Debug
    )

    A reusable bundle of declarative rules for a focused incident pattern.

    Packs describe an analysis vocabulary without changing the canonical event model. Callers can inspect the rules, present the metadata in a UI, or run the pack through analyze_with_pack.

    CanonicalEvent

    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
    )

    A source event with stable semantics, resource identity, and provenance.

    raw_content is never replaced by normalized text. body and attributes preserve structured values for later rules and reports.

    CanonicalEvent::to_normalized_event

    fn CanonicalEvent::to_normalized_event(self : CanonicalEvent) -> NormalizedEvent

    Converts a canonical event to the legacy timeline view without losing the original body stored in raw_content.

    Case

    pub(all) struct Case {
    case_id : String
    title : String
    timezone : String
    collected_at : Timestamp
    evidence : Array[EvidenceItem]
    metadata : EvidenceMetadata
    } derive(Eq, ToJson,
    Debug
    )

    The top-level container for one incident investigation.

    CaseAnalysis

    pub(all) struct CaseAnalysis {
    events : Array[CanonicalEvent]
    graph : IncidentGraph
    report : ForensicReport
    } derive(Eq, ToJson,
    Debug
    )

    The result of assembling and analyzing one multi-source case.

    CaseEvidenceInput

    pub(all) struct CaseEvidenceInput {
    document : EvidenceDocument
    events : Array[CanonicalEvent]
    } derive(Eq,
    Debug
    )

    Canonical events and their evidence document as one case input.

    CorrelationEvent

    pub(all) struct CorrelationEvent {
    event : NormalizedEvent
    resource_id : String
    kind : CorrelationKind
    } derive(Eq, ToJson,
    Debug
    )

    A normalized event annotated with the resource and event role used for correlation.

    CorrelationGroup

    pub(all) struct CorrelationGroup {
    resource_id : String
    events : Array[CorrelationEvent]
    } derive(Eq, ToJson,
    Debug
    )

    Events from distinct sources that refer to one resource inside a time window.

    CorrelationKind

    pub(all) enum CorrelationKind {
    Change
    Alert
    Crash
    Other(String)
    } derive(Eq, ToJson,
    Debug
    )

    The observed role of an event in a cross-source correlation.

    CorrelationRule

    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
    )

    A rule that relates a matching earlier event to a matching later event.

    DiagnosticFinding

    pub(all) struct DiagnosticFinding {
    rule_id : String
    title : String
    explanation : String
    kind : FindingKind
    confidence_note : String
    evidence : Array[EvidenceReference]
    } derive(Eq, ToJson,
    Debug
    )

    One explained rule match with the supporting timeline event reference.

    DiagnosticReport

    pub(all) struct DiagnosticReport {
    findings : Array[DiagnosticFinding]
    } derive(Eq, ToJson,
    Debug
    )

    The deterministic findings produced by evaluating an ordered rule set.

    DiagnosticRule

    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
    )

    A configurable single-event rule over normalized severity and raw text.

    EventCategory

    pub(all) enum EventCategory {
    Change
    Availability
    Performance
    Process
    Other(String)
    } derive(Eq, ToJson,
    Debug
    )

    A stable event category independent from severity.

    EventEvidence

    pub(all) struct EventEvidence {
    evidence_id : String
    line_number : Int
    } derive(Eq, ToJson,
    Debug
    )

    The source location retained when an input record becomes an incident event.

    EventOutcome

    pub(all) enum EventOutcome {
    Success
    Failure
    Unknown
    Other(String)
    } derive(Eq, ToJson,
    Debug
    )

    The observed outcome of an event, when the source provides one.

    EventPattern

    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
    )

    Declarative predicates for the two sides of a temporal relation.

    EventProvenance

    pub(all) struct EventProvenance {
    evidence_id : String
    path : String
    line_number : Int?
    byte_offset : Int?
    byte_length : Int?
    sha256 : String?
    profile_id : String
    profile_version : String
    } derive(Eq, ToJson,
    Debug
    )

    Location and transformation information retained for every canonical event.

    EvidenceDocument

    pub(all) struct EvidenceDocument {
    id : String
    path : String
    source : EvidenceSource
    content : Bytes
    } derive(Eq,
    Debug
    )

    Evidence content and provenance supplied when building a manifest.

    EvidenceFormat

    pub(all) enum EvidenceFormat {
    JsonLines
    PlainText
    } derive(Eq, ToJson,
    Debug
    )

    The kind of source file represented by an evidence item.

    EvidenceItem

    pub(all) struct EvidenceItem {
    id : String
    source : EvidenceSource
    path : String
    format : EvidenceFormat
    captured_at : Timestamp?
    metadata : EvidenceMetadata
    } derive(Eq, ToJson,
    Debug
    )

    One user-provided source file and its provenance information.

    EvidenceManifest

    pub(all) struct EvidenceManifest {
    entries : Array[EvidenceManifestEntry]
    } derive(Eq, ToJson,
    Debug
    )

    A deterministic manifest for a collection of evidence documents.

    EvidenceManifestEntry

    pub(all) struct EvidenceManifestEntry {
    id : String
    path : String
    source_id : String
    size_bytes : Int
    sha256 : String
    } derive(Eq, ToJson,
    Debug
    )

    A manifest entry binds evidence identity and source to its size and SHA-256 digest.

    EvidenceMetadata

    pub(all) struct EvidenceMetadata {
    entries : Array[MetadataEntry]
    } derive(Eq, ToJson,
    Debug
    )

    Metadata shared by the case and its individual evidence items.

    EvidenceReference

    pub(all) struct EvidenceReference {
    event_index : Int
    source_id : String
    timestamp : NormalizedTimestamp?
    raw_content : String
    } derive(Eq, ToJson,
    Debug
    )

    A stable reference to an event in the timeline and its untouched raw content.

    EvidenceRelation

    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
    )

    An explainable edge between two canonical events.

    EvidenceSource

    pub(all) struct EvidenceSource {
    system : String
    component : String
    host : String?
    } derive(Eq, ToJson,
    Debug
    )

    Identifies the system component that produced an evidence item.

    FindingKind

    pub(all) enum FindingKind {
    Observation
    Hypothesis
    } derive(Eq, ToJson,
    Debug
    )

    States whether a rule result reports an observed fact or a hypothesis.

    ForensicReport

    pub(all) struct ForensicReport {
    case : Case
    summary : String
    timeline : Timeline
    findings : DiagnosticReport
    evidence_index : EvidenceManifest
    limitations : Array[String]
    } derive(Eq, ToJson,
    Debug
    )

    The complete, deterministic input used to render a case report.

    GraphDiagnostic

    pub(all) struct GraphDiagnostic {
    code : String
    message : String
    event_id : String?
    provenance : EventProvenance?
    } derive(Eq, ToJson,
    Debug
    )

    A diagnostic emitted when an event or rule cannot be fully evaluated.

    IncidentEvent

    pub(all) struct IncidentEvent {
    event_id : String
    normalized : NormalizedEvent
    resource_id : String?
    kind : CorrelationKind
    attributes : Json
    evidence : EventEvidence
    } derive(Eq, ToJson,
    Debug
    )

    A normalized event with the identity needed for cross-source analysis.

    resource_id is optional because records without an explicit resource remain useful in a timeline but must not be correlated automatically.

    IncidentEvent::to_canonical_event

    fn IncidentEvent::to_canonical_event(self : IncidentEvent) -> CanonicalEvent

    Converts a legacy adapter result into the canonical event contract.

    The bridge keeps the legacy source identifier, resource identity, attributes, raw content, and evidence line. It supplies conservative semantics for fields that the legacy mapping does not describe.

    IncidentGraph

    pub(all) struct IncidentGraph {
    nodes : Array[CanonicalEvent]
    edges : Array[EvidenceRelation]
    diagnostics : Array[GraphDiagnostic]
    } derive(Eq, ToJson,
    Debug
    )

    The deterministic graph consumed by timeline, finding, and report views.

    JsonFieldPath

    pub(all) struct JsonFieldPath {
    segments : Array[String]
    } derive(Eq,
    Debug
    )

    A deterministic path through JSON object keys.

    Array indexes, wildcards, and fuzzy key matching are intentionally excluded so a profile cannot silently change meaning when an input schema changes.

    JsonlEventMapping

    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
    )

    Explicit field mapping for JSONL records from one source shape.

    JsonlMappingProfile

    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
    )

    A versioned, reusable mapping from one JSONL schema to CanonicalEvent.

    Required values fail explicitly. Optional configured values may be absent, but a present non-string value is rejected instead of being coerced.

    JsonlRecord

    pub(all) struct JsonlRecord {
    line_number : Int
    value : Json
    } derive(Eq, ToJson,
    Debug
    )

    One JSON object parsed from a physical line in a JSONL stream.

    MetadataEntry

    pub(all) struct MetadataEntry {
    key : String
    value : String
    } derive(Eq, ToJson,
    Debug
    )

    A stable key-value metadata entry attached to a case or evidence item.

    NormalizedEvent

    pub(all) struct NormalizedEvent {
    timestamp : NormalizedTimestamp?
    severity : Severity?
    source_id : String
    raw_content : String
    } derive(Eq, ToJson,
    Debug
    )

    A normalized event that retains its source line exactly as supplied.

    NormalizedTimestamp

    pub(all) struct NormalizedTimestamp {
    epoch_day : Int
    second_of_day : Int
    nanosecond : Int
    raw : String
    } derive(Eq, ToJson,
    Debug
    )

    A timestamp represented as a UTC day and time, with the original input kept.

    PlainTextEventMapping

    pub(all) struct PlainTextEventMapping {
    system : String
    component : String
    resource_id : String?
    kind : CorrelationKind
    evidence_id : String
    } derive(Eq,
    Debug
    )

    Mapping for the fixed <timestamp> <severity> <message> text format.

    PlainTextRecord

    pub(all) struct PlainTextRecord {
    line_number : Int
    text : String
    } derive(Eq, ToJson,
    Debug
    )

    One non-empty line parsed from a plain-text log.

    ProcessorPipeline

    pub(all) struct ProcessorPipeline {
    name : String
    stages : Array[ProcessorStage]
    } derive(Eq,
    Debug
    )

    An ordered, reusable event transformation pipeline.

    ProcessorStage

    pub(all) enum ProcessorStage {
    Normalize
    AliasResource(ResourceAlias)
    FilterSource(String)
    FilterCategory(EventCategory)
    FilterResource(String)
    FilterTimeWindow(NormalizedTimestamp, NormalizedTimestamp)
    } derive(Eq,
    Debug
    )

    A deterministic transformation or filter applied to canonical events.

    ProfileCatalog

    pub struct ProfileCatalog {
    profiles : Array[JsonlMappingProfile]
    } derive(Eq,
    Debug
    )

    A validated collection of versioned JSONL mapping profiles.

    Catalog order is registration order. Resolving always requires an exact profile ID and version so a schema change cannot silently select another mapping.

    ProfileEvidence

    pub(all) struct ProfileEvidence {
    evidence_id : String
    path : String
    sha256 : String?
    } derive(Eq,
    Debug
    )

    Evidence identity shared by every event produced from one JSONL stream.

    ProfileStringValue

    pub(all) enum ProfileStringValue {
    Constant(String)
    JsonString(JsonFieldPath)
    } derive(Eq,
    Debug
    )

    A string supplied by a profile or read from an input record.

    RelationStatus

    pub(all) enum RelationStatus {
    Observed
    Hypothesis
    } derive(Eq, ToJson,
    Debug
    )

    Whether a relation is a directly observed fact or a rule-based hypothesis.

    RelationType

    pub(all) enum RelationType {
    SameResource
    TemporalBefore
    MatchedRule
    AliasOf
    AttributeMatch
    Custom(String)
    } derive(Eq, ToJson,
    Debug
    )

    The semantic type of a graph edge produced by a rule or processor.

    ResourceAlias

    pub(all) struct ResourceAlias {
    source_id : String
    canonical_id : String
    } derive(Eq,
    Debug
    )

    Exact source-to-canonical resource identity mapping.

    ResourceKind

    pub(all) enum ResourceKind {
    Service
    Instance
    Host
    Container
    Config
    Trace
    Custom(String)
    } derive(Eq, ToJson,
    Debug
    )

    The stable identity class of a resource observed by an incident event.

    ResourceMappingProfile

    pub(all) struct ResourceMappingProfile {
    kind : ResourceKind
    id : ProfileStringValue
    prefix : String
    aliases : Array[ResourceAlias]
    attributes_path : JsonFieldPath?
    } derive(Eq,
    Debug
    )

    Mapping rules for one typed resource identity.

    ResourceRef

    pub(all) struct ResourceRef {
    kind : ResourceKind
    id : String
    attributes : Json
    } derive(Eq, ToJson,
    Debug
    )

    A typed resource identity. The kind and identifier stay separate so that an instance is not accidentally treated as a service with a similar name.

    Severity

    pub(all) enum Severity {
    Trace
    Debug
    Info
    Warning
    Error
    Fatal
    Unknown(String)
    } derive(Eq, ToJson,
    Debug
    )

    A canonical severity label used by normalized events.

    SourceMappingProfile

    pub(all) struct SourceMappingProfile {
    system : ProfileStringValue
    component : ProfileStringValue
    host : ProfileStringValue?
    instrumentation_scope : ProfileStringValue?
    } derive(Eq,
    Debug
    )

    Mapping rules for the producer of a canonical event.

    TelemetrySource

    pub(all) struct TelemetrySource {
    system : String
    component : String
    host : String?
    instrumentation_scope : String?
    } derive(Eq, ToJson,
    Debug
    )

    Structured producer identity for a canonical event.

    Timeline

    pub(all) struct Timeline {
    events : Array[NormalizedEvent]
    } derive(Eq, ToJson,
    Debug
    )

    An ordered collection of normalized events for deterministic timeline queries.

    Timestamp

    pub(all) struct Timestamp {
    value : String
    } derive(Eq, ToJson,
    Debug
    )

    A timestamp preserved as the original RFC 3339 text.

    Parsing and validation belong to the ingest stage. Keeping the value intact here makes the domain model lossless and deterministic.

    adapt_jsonl_records

    fn adapt_jsonl_records(records : Array[JsonlRecord], mapping : JsonlEventMapping) -> Array[IncidentEvent] raise EventAdaptError

    Converts JSONL objects to events using an explicit field and resource mapping. Unknown JSON attributes are retained as attributes; no field names are guessed.

    adapt_jsonl_records_as_canonical

    fn adapt_jsonl_records_as_canonical(records : Array[JsonlRecord], mapping : JsonlEventMapping) -> Array[CanonicalEvent] raise EventAdaptError

    Adapts JSONL records through the canonical event contract.

    This is the migration entry point for callers that still use JsonlEventMapping. The legacy mapping remains explicit, while the result is ready for processors, declarative rules, and incident graphs.

    adapt_plain_text_records

    fn adapt_plain_text_records(records : Array[PlainTextRecord], mapping : PlainTextEventMapping) -> Array[IncidentEvent] raise EventAdaptError

    Converts fixed-format text records to events with an explicit source mapping.

    adapt_plain_text_records_as_canonical

    fn adapt_plain_text_records_as_canonical(records : Array[PlainTextRecord], mapping : PlainTextEventMapping) -> Array[CanonicalEvent] raise EventAdaptError

    Adapts fixed-format text through the canonical event contract.

    analyze_case

    fn analyze_case(case : Case, inputs : Array[CaseEvidenceInput], pipeline : ProcessorPipeline?, pack : AnalysisPack?, summary : String, limitations : Array[String]) -> CaseAnalysis raise CaseAnalysisError

    Assembles multiple in-memory evidence streams into one deterministic case.

    The case metadata and document identities are checked before mapping results enter the processor, graph, and report stages. No file-system access occurs.

    analyze_with_pack

    fn analyze_with_pack(events : Array[CanonicalEvent], pack : AnalysisPack) -> IncidentGraph raise CorrelationRuleError

    Runs a reusable pack while preserving the graph builder's deterministic ordering, evidence references, and diagnostics.

    apply_jsonl_profile

    fn apply_jsonl_profile(records : Array[JsonlRecord], profile : JsonlMappingProfile, evidence : ProfileEvidence) -> Array[CanonicalEvent] raise ProfileMapError

    Applies one validated profile to ordered JSONL records.

    The original object is retained in attributes and raw_content. Every result records the profile id/version and physical evidence line in provenance.

    build_case_jsonl_evidence

    fn build_case_jsonl_evidence(document : EvidenceDocument, records : Array[JsonlRecord], profile : JsonlMappingProfile) -> CaseEvidenceInput raise ProfileMapError

    Maps one JSONL evidence document with a profile and binds its digest to every resulting event provenance.

    build_evidence_manifest

    fn build_evidence_manifest(documents : Array[EvidenceDocument]) -> EvidenceManifest

    Builds ordered evidence metadata and SHA-256 content digests.

    Example

    test {
    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)
    }

    build_incident_graph

    fn build_incident_graph(events : Array[CanonicalEvent], rules : Array[CorrelationRule]) -> IncidentGraph raise CorrelationRuleError

    Builds a deterministic, explainable graph from canonical events and rules.

    Nodes are sorted by event time with stable input order for equal timestamps; events without timestamps are placed last. Rule edges retain their evidence references, explanation, rule ID, relation status, and resource key. Diagnostics identify data-quality limits without discarding the source event.

    build_timeline

    fn build_timeline(events : Array[NormalizedEvent]) -> Timeline

    Builds a stable timeline ordered by UTC time; events without timestamps come last. Equal timestamps retain their input order.

    Example

    test {
    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)
    }

    built_in_analysis_packs

    fn built_in_analysis_packs() -> Array[AnalysisPack]

    Returns the built-in packs in stable display order.

    canonicalize_incident_event

    fn canonicalize_incident_event(event : IncidentEvent, source : TelemetrySource, resource : ResourceRef?, category : EventCategory, event_type : String, action : String?, outcome : EventOutcome, observed_time : NormalizedTimestamp?, provenance : EventProvenance) -> CanonicalEvent

    Bridges the existing adapter result into the canonical model. Classification, resource identity, source structure, and provenance are explicit inputs so this bridge never guesses business semantics from a message.

    compare_timestamps

    fn compare_timestamps(left : NormalizedTimestamp, right : NormalizedTimestamp) -> Int

    Compares timestamps chronologically, returning a negative, zero, or positive value.

    config_analysis_pack

    fn config_analysis_pack() -> AnalysisPack

    Rules for changes followed by health or process failures.

    correlate_events

    fn correlate_events(events : Array[CorrelationEvent], window_seconds : Int) -> Array[CorrelationGroup] raise CorrelationError

    Groups same-resource events when at least two distinct sources fall within the inclusive window from the group's earliest event. Results and group members are ordered chronologically; equal timestamps retain input order. Events without a timestamp are ignored. A group records temporal/resource correlation, not causation.

    Example

    test {
    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)
    }

    correlate_incident_events

    fn correlate_incident_events(events : Array[IncidentEvent], window_seconds : Int) -> Array[CorrelationGroup] raise CorrelationError

    Correlates adapted events, excluding only events without an explicit resource.

    crash_analysis_pack

    fn crash_analysis_pack() -> AnalysisPack

    Rules for process crashes followed by service failure or recovery.

    empty_incident_graph

    fn empty_incident_graph() -> IncidentGraph

    Creates an empty graph for incremental processors.

    evaluate_correlation_rules

    fn evaluate_correlation_rules(events : Array[CanonicalEvent], rules : Array[CorrelationRule]) -> IncidentGraph raise CorrelationRuleError

    Evaluates rules in declaration order and returns an explainable incident graph.

    A relation requires an earlier matching event, a later matching event, and an inclusive temporal window. Same-resource rules require equal typed resource identities and never infer a relation when either side lacks a resource.

    evaluate_diagnostic_rules

    fn evaluate_diagnostic_rules(timeline : Timeline, rules : Array[DiagnosticRule]) -> DiagnosticReport

    Evaluates rules in declaration order and events in timeline order. Each finding cites its timeline index, normalized source, time, and raw content.

    Example

    test {
    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)
    }

    filter_timeline_by_source

    fn filter_timeline_by_source(timeline : Timeline, source_id : String) -> Timeline

    Keeps only events whose normalized source identifier matches source_id.

    Example

    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)
    }

    incident_timeline

    fn incident_timeline(events : Array[IncidentEvent]) -> Timeline

    Builds the existing timeline view from adapted events without losing event identity.

    latency_analysis_pack

    fn latency_analysis_pack() -> AnalysisPack

    Rules for latency observations followed by timeout or request errors.

    list_profiles

    fn list_profiles(catalog : ProfileCatalog) -> Array[JsonlMappingProfile]

    Returns profiles in their stable registration order.

    new_profile_catalog

    fn new_profile_catalog(profiles : Array[JsonlMappingProfile]) -> ProfileCatalog raise ProfileCatalogError

    Creates a catalog after validating every profile and its exact identity.

    normalize_event

    fn normalize_event(timestamp : String?, severity : String?, system : String, component : String, host : String?, raw_content : String) -> NormalizedEvent raise NormalizationError

    Normalizes event fields and keeps the original log content unchanged.

    normalize_severity

    fn normalize_severity(value : String) -> Severity

    Maps common severity spellings to a stable enum while retaining unknown labels.

    normalize_source_id

    fn normalize_source_id(system : String, component : String, host : String?) -> String

    Creates a stable slash-delimited source identifier from trimmed components.

    normalize_timestamp

    fn normalize_timestamp(value : String) -> NormalizedTimestamp raise NormalizationError

    Parses an RFC 3339 timestamp and converts it to a UTC comparison key.

    parse_jsonl

    fn parse_jsonl(source : String) -> Array[JsonlRecord] raise JsonlParseError

    Parses JSONL text into ordered object records while preserving line numbers. Empty and whitespace-only lines are ignored. Every non-empty line must be a complete JSON object.

    Example

    test {
    let records = parse_jsonl("{\"event_id\":\"evt-1\"}\n")
    assert_eq(records.length(), 1)
    assert_eq(records[0].line_number, 1)
    }

    parse_plain_text

    fn parse_plain_text(source : String) -> Array[PlainTextRecord]

    Parses plain-text logs into ordered records with physical line numbers. Empty and whitespace-only lines are ignored. Leading whitespace is kept, while trailing whitespace and line-ending characters are removed.

    Example

    test {
    let records = parse_plain_text("INFO service started\nservice stopped\n")
    assert_eq(records.length(), 2)
    assert_eq(records[1].line_number, 2)
    }

    process_events

    fn process_events(events : Array[CanonicalEvent], pipeline : ProcessorPipeline) -> Array[CanonicalEvent] raise ProcessorError

    Validates and runs stages in declaration order, preserving event order.

    Filter stages are inclusive and stable. A Normalize stage trims structured identifiers only; raw content, body, attributes, and provenance are retained.

    query_timeline_window

    fn query_timeline_window(timeline : Timeline, start : NormalizedTimestamp, end : NormalizedTimestamp) -> Timeline raise TimelineError

    Returns events with timestamps inside the inclusive UTC window, preserving timeline order. Events without timestamps are omitted.

    Example

    test {
    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)
    }

    register_profile

    fn register_profile(catalog : ProfileCatalog, profile : JsonlMappingProfile) -> ProfileCatalog raise ProfileCatalogError

    Registers one new profile without replacing an existing ID/version pair.

    render_json_report

    fn render_json_report(report : ForensicReport) -> String

    Renders the complete report as deterministic, two-space-indented JSON.

    Example

    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: [],
    }
    let json = render_json_report(report)
    assert_true(json.contains("\"evidence_index\""))
    assert_true(json.contains("\"limitations\""))
    }

    render_markdown_report

    fn render_markdown_report(report : ForensicReport) -> String

    Renders a Markdown case report with a timeline, findings, evidence index, and limitations. All dynamic table values are escaped so source text cannot add table cells or HTML.

    Example

    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("## 限制说明"))
    }

    resolve_profile

    fn resolve_profile(catalog : ProfileCatalog, id : String, version : String) -> JsonlMappingProfile raise ProfileCatalogError

    Resolves a profile by exact ID and version.

    validate_analysis_pack

    fn validate_analysis_pack(pack : AnalysisPack) -> Unit raise CorrelationRuleError

    Validates the metadata and rules of a reusable analysis pack.

    validate_correlation_rules

    fn validate_correlation_rules(rules : Array[CorrelationRule]) -> Unit raise CorrelationRuleError

    Validates rule identifiers, patterns, and non-negative temporal windows.

    validate_jsonl_profile

    fn validate_jsonl_profile(profile : JsonlMappingProfile) -> Unit raise ProfileMapError

    Checks a profile independently from any evidence records.

    verify_evidence_manifest

    fn verify_evidence_manifest(manifest : EvidenceManifest, documents : Array[EvidenceDocument]) -> Bool

    Checks evidence identity, source, path, byte size, and digest against a manifest.

    Example

    test {
    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]))
    }