using System.Diagnostics; using Microsoft.Extensions.Logging; using StellaOps.Findings.Ledger.Domain; using StellaOps.Findings.Ledger.Infrastructure.Exports; namespace StellaOps.Findings.Ledger.Observability; /// /// Emits structured timeline events for ledger operations. /// Currently materialised as structured logs; can be swapped for event sink later. /// internal static class LedgerTimeline { private static readonly EventId LedgerAppended = new(6101, "ledger.event.appended"); private static readonly EventId ProjectionUpdated = new(6201, "ledger.projection.updated"); private static readonly EventId OrchestratorExport = new(6301, "ledger.export.recorded"); private static readonly EventId AirgapImport = new(6401, "ledger.airgap.imported"); private static readonly EventId EvidenceSnapshotLinkedEvent = new(6501, "ledger.evidence.snapshot_linked"); private static readonly EventId AirgapTimelineImpactEvent = new(6601, "ledger.airgap.timeline_impact"); private static readonly EventId AttestationPointerLinkedEvent = new(6701, "ledger.attestation.pointer_linked"); private static readonly EventId SnapshotCreatedEvent = new(6801, "ledger.snapshot.created"); private static readonly EventId SnapshotDeletedEvent = new(6802, "ledger.snapshot.deleted"); private static readonly EventId TimeTravelQueryEvent = new(6803, "ledger.timetravel.query"); private static readonly EventId ReplayCompletedEvent = new(6804, "ledger.replay.completed"); private static readonly EventId DiffComputedEvent = new(6805, "ledger.diff.computed"); public static void EmitLedgerAppended(ILogger logger, LedgerEventRecord record, string? evidenceBundleRef = null) { if (logger is null) { return; } var traceId = Activity.Current?.TraceId.ToHexString() ?? string.Empty; logger.LogInformation( LedgerAppended, "timeline ledger.event.appended tenant={Tenant} chain={ChainId} seq={Sequence} event={EventId} type={EventType} policy={PolicyVersion} finding={FindingId} trace={TraceId} evidence_ref={EvidenceRef}", record.TenantId, record.ChainId, record.SequenceNumber, record.EventId, record.EventType, record.PolicyVersion, record.FindingId, traceId, evidenceBundleRef ?? record.EvidenceBundleReference ?? string.Empty); } public static void EmitProjectionUpdated( ILogger logger, LedgerEventRecord record, string? evaluationStatus, string? evidenceBundleRef = null) { if (logger is null) { return; } var traceId = Activity.Current?.TraceId.ToHexString() ?? string.Empty; logger.LogInformation( ProjectionUpdated, "timeline ledger.projection.updated tenant={Tenant} chain={ChainId} seq={Sequence} event={EventId} policy={PolicyVersion} finding={FindingId} status={Status} trace={TraceId} evidence_ref={EvidenceRef}", record.TenantId, record.ChainId, record.SequenceNumber, record.EventId, record.PolicyVersion, record.FindingId, evaluationStatus ?? string.Empty, traceId, evidenceBundleRef ?? record.EvidenceBundleReference ?? string.Empty); } public static void EmitOrchestratorExport(ILogger logger, OrchestratorExportRecord record) { if (logger is null) { return; } logger.LogInformation( OrchestratorExport, "timeline ledger.export.recorded tenant={Tenant} run={RunId} artifact={ArtifactHash} policy={PolicyHash} status={Status} merkle_root={MerkleRoot}", record.TenantId, record.RunId, record.ArtifactHash, record.PolicyHash, record.Status, record.MerkleRoot); } public static void EmitAirgapImport(ILogger logger, string tenantId, string bundleId, string merkleRoot, Guid? ledgerEventId) { if (logger is null) { return; } logger.LogInformation( AirgapImport, "timeline ledger.airgap.imported tenant={Tenant} bundle={BundleId} merkle_root={MerkleRoot} ledger_event={LedgerEvent}", tenantId, bundleId, merkleRoot, ledgerEventId?.ToString() ?? string.Empty); } public static void EmitEvidenceSnapshotLinked(ILogger logger, string tenantId, string findingId, string bundleUri, string dsseDigest) { if (logger is null) { return; } logger.LogInformation( EvidenceSnapshotLinkedEvent, "timeline ledger.evidence.snapshot_linked tenant={Tenant} finding={FindingId} bundle_uri={BundleUri} dsse_digest={DsseDigest}", tenantId, findingId, bundleUri, dsseDigest); } public static void EmitAirgapTimelineImpact( ILogger logger, string tenantId, string bundleId, int newFindings, int resolvedFindings, int criticalDelta, DateTimeOffset timeAnchor, bool sealedMode) { if (logger is null) { return; } logger.LogInformation( AirgapTimelineImpactEvent, "timeline ledger.airgap.timeline_impact tenant={Tenant} bundle={BundleId} new_findings={NewFindings} resolved_findings={ResolvedFindings} critical_delta={CriticalDelta} time_anchor={TimeAnchor} sealed_mode={SealedMode}", tenantId, bundleId, newFindings, resolvedFindings, criticalDelta, timeAnchor.ToString("O"), sealedMode); } public static void EmitAttestationPointerLinked( ILogger logger, string tenantId, string findingId, Guid pointerId, string attestationType, string digest) { if (logger is null) { return; } logger.LogInformation( AttestationPointerLinkedEvent, "timeline ledger.attestation.pointer_linked tenant={Tenant} finding={FindingId} pointer={PointerId} attestation_type={AttestationType} digest={Digest}", tenantId, findingId, pointerId, attestationType, digest); } public static void EmitSnapshotCreated( ILogger logger, string tenantId, Guid snapshotId, long sequenceNumber, long findingsCount) { if (logger is null) { return; } logger.LogInformation( SnapshotCreatedEvent, "timeline ledger.snapshot.created tenant={Tenant} snapshot={SnapshotId} sequence={SequenceNumber} findings_count={FindingsCount}", tenantId, snapshotId, sequenceNumber, findingsCount); } public static void EmitSnapshotDeleted( ILogger logger, string tenantId, Guid snapshotId) { if (logger is null) { return; } logger.LogInformation( SnapshotDeletedEvent, "timeline ledger.snapshot.deleted tenant={Tenant} snapshot={SnapshotId}", tenantId, snapshotId); } public static void EmitTimeTravelQuery( ILogger logger, string tenantId, string entityType, long atSequence, int resultCount) { if (logger is null) { return; } logger.LogInformation( TimeTravelQueryEvent, "timeline ledger.timetravel.query tenant={Tenant} entity_type={EntityType} at_sequence={AtSequence} result_count={ResultCount}", tenantId, entityType, atSequence, resultCount); } public static void EmitReplayCompleted( ILogger logger, string tenantId, long fromSequence, long toSequence, int eventsCount, long durationMs) { if (logger is null) { return; } logger.LogInformation( ReplayCompletedEvent, "timeline ledger.replay.completed tenant={Tenant} from_sequence={FromSequence} to_sequence={ToSequence} events_count={EventsCount} duration_ms={DurationMs}", tenantId, fromSequence, toSequence, eventsCount, durationMs); } public static void EmitDiffComputed( ILogger logger, string tenantId, long fromSequence, long toSequence, int added, int modified, int removed) { if (logger is null) { return; } logger.LogInformation( DiffComputedEvent, "timeline ledger.diff.computed tenant={Tenant} from_sequence={FromSequence} to_sequence={ToSequence} added={Added} modified={Modified} removed={Removed}", tenantId, fromSequence, toSequence, added, modified, removed); } }