Timeline Architecture
Audience: developers integrating with the audit/timeline APIs, and operators reasoning about tamper-evident audit, anomaly detection, and event replay. For the module overview and quick links, see
./README.md.
Unified audit sink plus query/presentation service for HLC-ordered cross-service event timelines.
Overview
Timeline is a single deployable Stella Ops service (StellaOps.Timeline.WebService) that fulfils two related responsibilities:
- Unified audit sink. It owns the platform-wide, tamper-evident audit log in
timeline.unified_audit_events. Every Stella Ops service emits audit events here (for example, Authority login/auth events, Policy governance changes, JobEngine/Release operations, EvidenceLocker, Notify). Events are pushed toPOST /api/v1/audit/ingestand persisted with a per-tenant SHA-256 hash chain for tamper evidence. The/api/v1/audit/*API serves filtered event lists, statistics, correlation clusters, chain verification, retention policies, GDPR redaction, and an anomaly-detection (anomaly v2) subsystem. - HLC event timeline. It provides a REST API for querying, analyzing, and replaying events indexed by the TimelineIndexer (consolidated into this module). All such events carry Hybrid Logical Clock (HLC) timestamps that establish causal ordering across distributed services. The service supports correlation-based queries, critical path analysis for latency diagnosis, and deterministic event replay/export.
The TimelineIndexer ingestion side (NATS/Redis subscribers, the timeline.timeline_events store, and query/evidence endpoints) was merged into StellaOps.Timeline.WebService and is co-hosted in the same process. The separate StellaOps.TimelineIndexer.WebService and StellaOps.TimelineIndexer.Worker projects exist but are dormant — their logic now runs inside the unified WebService (see Components).
Production Timeline hosts do not use process-local replay/export state. The in-memory replay orchestrator and export bundle builder are registered only in Development and Testing (AddTimelineServices); other environments fall back to UnsupportedTimelineReplayOrchestrator / UnsupportedTimelineBundleBuilder, so replay/export operations return 501 problem+json (timeline_replay_unavailable / timeline_export_unavailable) until durable operation storage and worker-backed execution are configured.
Components
src/Timeline/
StellaOps.Timeline.WebService/ # Unified WebService (audit sink + HLC timeline + indexer)
Audit/ # Unified audit store, providers, aggregation, retention, classifier
PostgresUnifiedAuditEventStore.cs # Hash-chained Postgres store (primary source of truth)
PostgresUnifiedAuditStateStore.cs # Durable ack/export operation state
CompositeUnifiedAuditEventProvider.cs # Merges Postgres store + (neutered) HTTP provider
HttpUnifiedAuditEventProvider.cs # Legacy per-service poller (neutered, DEPRECATE-002)
UnifiedAuditAggregationService.cs # Stats, correlations, timeline search, export
AuditDataClassifier.cs # GDPR classification (none/personal/sensitive/restricted)
AuditRetentionPurgeService.cs # Scheduled per-classification retention purge (hosted)
TimelineAuthorizationAuditSink.cs # IAuthEventSink logging auth outcomes
UnifiedAuditContracts.cs # Event model + module/action/severity catalogs
Anomalies/ # Anomaly v2 rule engine + evaluators + escalation chains
HostedServices/ # AnomalyDefaultRuleSeed, ActorBehaviorBaselineRollup
Authorization/TimelineAuthorizationMiddleware.cs
Security/TimelinePolicies.cs # Read/Write/Admin policy names (timeline:read/write/admin)
Endpoints/
TimelineEndpoints.cs # HLC query endpoints (/api/v1/timeline/hlc/*)
ExportEndpoints.cs # HLC event export endpoints
ReplayEndpoints.cs # Deterministic replay endpoints
UnifiedAuditEndpoints.cs # Unified /api/v1/audit ingest + query endpoints
HealthEndpoints.cs # /health, /health/ready, /health/live
Program.cs # Host configuration; also defines /api/v1/timeline indexer routes
StellaOps.TimelineIndexer.WebService/ # Dormant (logic merged into Timeline.WebService)
StellaOps.TimelineIndexer.Worker/ # Dormant standalone worker (logic merged; runs in-process)
__Libraries/
StellaOps.Timeline.Core/ # HLC query/replay/export + unified-audit persistence + anomalies
ITimelineQueryService.cs # HLC query interface
TimelineQueryService.cs # HLC query implementation (over StellaOps.Eventing store)
Migrations/ # timeline schema: ONE live file, 001_v1_timeline_core_baseline.sql
# (collapsed 002-010: critical-path view, unified_audit_events,
# GIN/retention/operation-state/baseline/rules/alerts).
# Pre-collapse files live in _archived/ and are NOT embedded.
Postgres/ # TimelineCoreDataSource + anomaly/baseline/alert repositories
Replay/ # ITimelineReplayOrchestrator (+ Unsupported fallback)
Export/ # ITimelineBundleBuilder (+ Unsupported fallback)
Anomalies/ # Contracts, default rules, baseline calculator/repository
StellaOps.TimelineIndexer.Core/ # Ingestion domain logic
Abstractions/ # ITimelineEventStore, ITimelineIngestionService, etc.
Models/ # TimelineEventEnvelope, TimelineEventView, etc.
Services/ # TimelineIngestionService, TimelineQueryService
StellaOps.TimelineIndexer.Infrastructure/ # Persistence, EfCore, messaging subscribers
Db/ # Migrations (001_initial_schema.sql), event store, query store
EfCore/ # Compiled models, context, entity models
Subscriptions/ # NATS, Redis, Null subscribers, envelope parser, ingestion worker
Options/TimelineIngestionOptions.cs # Ingestion:Nats / Ingestion:Redis transport config
__Tests/
StellaOps.Timeline.Core.Tests/ # HLC query tests
StellaOps.Timeline.WebService.Tests/ # API integration, audit store, anomaly, replay tests
StellaOps.TimelineIndexer.Tests/ # Indexer unit and integration tests
Data Flow
HLC Timeline Flow
- Events are produced by various Stella Ops services and carry HLC timestamps.
- The in-process ingestion worker (
TimelineIngestionWorker) consumes events from the message bus (NATS or Redis subscriber) and writes indexed events to thetimeline.timeline_eventsstore. - Timeline WebService receives HLC query requests (Platform, CLI, Web) under
/api/v1/timeline/hlc/*. TimelineQueryService(StellaOps.Timeline.Core) executes queries against theStellaOps.Eventingevent store (ITimelineEventStore), applying correlation, service, kind, and HLC-range filters.- Results are returned in HLC-sorted order, with optional critical path analysis computing latency stages between correlated events. The
timeline.critical_pathmaterialized view (Timeline.Core baseline001_v1_timeline_core_baseline.sql:28) pre-computes stage transitions overtimeline.timeline_eventsfor performance.
Unified Audit Ingest + Query Flow
- Ingest. Any service emits an audit event by
POSTing to/api/v1/audit/ingest(wire-compatible with theAuditEventPayloadfrom the sharedStellaOps.Audit.Emissionlibrary,timeline:writescope).IngestEventAsyncnormalizes the module/action/severity (UnifiedAuditValueMapperagainstUnifiedAuditCatalog) and persists viaPostgresUnifiedAuditEventStore.AddAsync, which appends totimeline.unified_audit_eventsunderSERIALIZABLEisolation with a per-tenant SHA-256 hash chain (content_hashlinked toprevious_entry_hash, monotonicsequence_number). PostgreSQL serialization/deadlock aborts retry the whole transaction with bounded deterministic backoff, so sequence allocation and hash computation are repeated against the current chain head. Duplicate event IDs roll back their sequence allocation and return idempotently instead of leaving a gap in the chain head. Returns202 Accepted. - Read.
UnifiedAuditEndpointsserves/api/v1/audit/*read requests from Web/CLI clients. UnifiedAuditAggregationServiceroutes event-list requests through the optionalIUnifiedAuditPagedEventProvidercapability implemented by the productionCompositeUnifiedAuditEventProvider.PostgresUnifiedAuditEventStore.GetPageAsyncapplies the public filters, exact total count, and stable(timestamp DESC, id ASC)cursor paging in PostgreSQL; the event-list path is not bounded by the legacy 10,000-row snapshot.- The composite provider merges two sources: the
PostgresUnifiedAuditEventStore(primary source of truth) and theHttpUnifiedAuditEventProvider. Note (orphaned/transitional): the HTTP provider is neutered as of DEPRECATE-002 — itsGetEventsAsyncreturns an empty list because the per-service audit endpoints it used to poll (Authority, Policy, Notify, JobEngine, EvidenceLocker) now proxy into Timeline, so polling them would self-loop. In effect, Postgres is the sole live source; the HTTP path is retained dead code pending DEPRECATE-003 removal. - The production query capabilities remove the legacy fixed snapshot from the remaining reads: event lookup and export count execute directly in PostgreSQL, statistics are aggregated in PostgreSQL, and timeline search plus seven-day anomaly evaluation receive the complete tenant-filtered PostgreSQL result. Correlation listing uses
IUnifiedAuditCorrelationEventProvider: PostgreSQL selects at most the requested 1-200 newest multi-event correlation IDs by root-event time, then returns only the events for those clusters. This preserves complete selected clusters without materializing the tenant/date event set in service memory or silently truncating at the legacy 10,000-event boundary. Anomaly acknowledgements and unified audit export status are persisted per tenant in PostgreSQL (PostgresUnifiedAuditStateStore); they are not process-local service memory. - If a source is unavailable, the composite provider logs the failure and continues with whatever source returned data instead of failing the unified endpoint.
Anomaly v2 Flow
- Audit events feed deterministic per-actor behavior baselines, materialized by
ActorBehaviorBaselineRollupHostedServiceintotimeline.actor_behavior_baseline. Source rows are keyset-paged and folded immediately intoActorBehaviorBaselineAccumulator; each page is consumed before the next read, while the accumulator retains only per-group hourly counters plus the deterministic 50-value IP/user-agent caps. One pass retains at mostTimeline:BaselineRollup:MaxDistinctGroupsdistinct(tenant, actor, action, resource type)groups (default 10,000; clamped to 20,000). After saturation, already-admitted groups remain complete, later-group source events are omitted, and the result, warning log, and metrics explicitly report a degradeddistinct_group_limitstate rather than risking unbounded memory growth or claiming a complete projection. - The
AnomalyRuleEngineevaluates tenant-scoped, configurable rules (timeline.anomaly_rules, seeded byAnomalyDefaultRuleSeedHostedService) using a set of registeredIAnomalyRuleEvaluators (unusual volume, failed-auth spike, privilege escalation, off-hours activity, per-actor z-score, new actor/IP, action novelty, escalation chains, etc.). - Detected anomalies are persisted to
timeline.audit_anomaly_alertswith a dispatch-state worklist (pending → dispatched → failed). Notify polls pending, unacknowledged alerts via the admin dispatch endpoints and fans out notifications.
Database Schema
Everything lives under the timeline PostgreSQL schema. Two independent migration runners converge it on startup (both wired via AddStartupMigrations in Program.cs, against the Postgres:Timeline connection):
TimelineIndexerrunner — appliesStellaOps.TimelineIndexer.Infrastructure/Db/Migrations/(001_initial_schema.sql): the indexer event tables and RLS scaffolding. Runs first so base tables exist.TimelineAuditrunner — appliesStellaOps.Timeline.Core/Migrations/, which is a single live file,001_v1_timeline_core_baseline.sql: the critical-path view, the unified audit sink, retention, durable operation state, and the anomaly v2 tables.
The library also ships
TimelineIndexerMigrationHostedService, butProgram.csremoves that duplicate registration so a single serialized runner owns thetimelineschema (avoids two competing runners).
Migration-filename note (2026-07-12). The Timeline.Core chain formerly numbered
002–010was collapsed into001_v1_timeline_core_baseline.sql; those files now exist only underMigrations/_archived/pre_1.0/mig061/and are excluded from embedding. The parenthetical “(fromNNN_*)” markers below are provenance of the folded-in DDL, not live filenames. The TimelineIndexer side is different: its001_initial_schema.sqlis still a real, live migration.
Unified audit sink: timeline.unified_audit_events (baseline line 87; from 003_unified_audit_events.sql)
The platform-wide tamper-evident audit log. Immutable append-only; primary key (id, tenant_id).
| Column | Type | Description |
|---|---|---|
id | TEXT | Event ID (caller-supplied or minted ingest-<guid>) |
tenant_id | TEXT | Owning tenant |
timestamp | TIMESTAMPTZ | Event time |
module | TEXT | Originating module (normalized against UnifiedAuditCatalog) |
action | TEXT | Action verb (normalized) |
severity | TEXT | info / warning / error / critical |
actor_* | TEXT | actor_id, actor_name, actor_email, actor_type, actor_ip, actor_user_agent |
resource_* | TEXT | resource_type, resource_id, resource_name |
description | TEXT | Human-readable description (GIN full-text index) |
details_jsonb | JSONB | Structured details (GIN index; folded in from the archived 004_details_gin_index.sql) |
diff_jsonb | JSONB | Optional before/after diff |
correlation_id | TEXT | Cross-service correlation identifier |
parent_event_id | TEXT | Hierarchy link |
tags | TEXT[] | Tags (GIN index) |
content_hash | TEXT | SHA-256 of canonical event JSON (tamper evidence) |
previous_entry_hash | TEXT | Prior event’s content_hash (hash chain link) |
sequence_number | BIGINT | Monotonic per-tenant sequence |
data_classification | TEXT | none / personal / sensitive / restricted (from 005_*) |
compliance_hold | BOOLEAN | Legal hold — exempt from retention purge (from 005_*) |
pii_redacted_at | TIMESTAMPTZ | Set when PII columns redacted (GDPR Art. 17; from 005_*) |
created_at | TIMESTAMPTZ | Wall-clock ingestion time |
Supporting objects: timeline.unified_audit_sequences (per-tenant chain head + next_unified_audit_sequence / update_unified_audit_sequence_hash functions) and verify_unified_audit_chain(tenant, from_seq, to_seq) which backs GET /api/v1/audit/chain/verify.
Retention + GDPR (baseline line 326; folded in from the archived 005_audit_data_classification_retention.sql)
| Object | Description |
|---|---|
timeline.audit_retention_policies | Per-tenant/per-classification retention windows; tenant_id = '*' is the platform default (none/personal 365d, sensitive 730d, restricted 2555d). |
resolve_audit_retention_days(tenant, class) | Resolves the effective retention, falling back to the platform default. |
purge_expired_audit_events(tenant, dry_run) | Deletes events older than the per-classification window, honouring compliance_hold. Driven by AuditRetentionPurgeService. |
redact_actor_pii(tenant, actor) | Right-to-erasure: replaces PII columns with [REDACTED] while preserving actor_id so the hash chain stays verifiable. |
Durable operation state (folded in from the archived 006_unified_audit_operation_state.sql)
| Table / Object | Description |
|---|---|
unified_audit_anomaly_acknowledgements | Per-tenant acknowledgement records for computed anomaly alert IDs. |
unified_audit_exports | Per-tenant unified audit export status records, including filters, format, event count, and retention timestamps. |
unified_audit_export_sequence | Monotonic sequence used to mint stable export IDs without process-local counters. |
Anomaly v2 (baseline lines 538-640; from 007–010)
| Table | Description |
|---|---|
actor_behavior_baseline | Deterministic per-actor hourly behavior baselines (p50/p95/p99, sample counts, last-seen IPs/UAs). Materialized by the rollup hosted service. |
anomaly_rules | Tenant-scoped, configurable anomaly rules (rule kind + JSONB params + severity). |
tenant_business_hours | Timeline-owned fallback for tenant timezone/business-hours policy (off-hours detection). |
audit_anomaly_alerts | Persisted anomaly alerts with a dispatch-state worklist (pending/dispatched/failed/suppressed) consumed by Notify. |
anomaly_subscriptions | Reserved placeholder for subscription/dispatcher coordination — not present in the baseline; no live schema yet. |
Indexer tables: HLC timeline (TimelineIndexer 001_initial_schema.sql — a live migration)
| Table | Description |
|---|---|
timeline_events | Core event header (event_seq bigserial, event_id, source, event_type, occurred_at, correlation_id, trace_id, severity enum, payload_hash, attributes). Row-Level Security enforces tenant isolation via app.current_tenant. |
timeline_event_details | Raw + normalized payloads per event. |
timeline_event_digests | Evidence linkage (bundle/attestation digests, manifest URI). |
critical_path (materialized view; Timeline.Core baseline line 28, from 002_create_critical_path_view.sql) | Pre-computed stage transitions and wall-clock durations over timeline_events. |
HLC event store (Eventing)
The HLC query path (/api/v1/timeline/hlc/*) reads through the StellaOps.Eventing infrastructure (ITimelineEventStore). Runtime Eventing registration is PostgreSQL-only: AddStellaOpsEventing(IConfiguration) registers PostgresTimelineEventStore plus PostgresHlcStateStore. Test harnesses that need volatile storage must call AddStellaOpsEventingInMemory(...) explicitly; setting Eventing:UseInMemoryStore=true on the runtime configuration path fails closed.
Dependencies
| Service/Library | Purpose |
|---|---|
| StellaOps.Eventing | HLC event store access and query primitives |
| StellaOps.HybridLogicalClock | HLC timestamp parsing and comparison |
| StellaOps.TimelineIndexer.* | In-process ingestion (NATS/Redis subscribers, indexer query/evidence) |
| StellaOps.Infrastructure.Postgres.Migrations | AddStartupMigrations auto-migration of the timeline schema |
| StellaOps.Audit.Emission | Shared library other services use to push events to /api/v1/audit/ingest (wire contract) |
| Router | Service mesh routing and discovery |
| Authority | JWT/OAuth token validation and scope policies |
| Notify | Downstream consumer of pending anomaly alerts (dispatch worklist) |
Configuration
| Key | Purpose |
|---|---|
Postgres:Timeline:ConnectionString (+ SchemaName, CommandTimeoutSeconds) | timeline schema connection used by both migration runners, the audit store, and the indexer. |
Eventing:* (ServiceName, UseInMemoryStore, ConnectionString, SignEvents, EnableOutbox) | HLC event store wiring. UseInMemoryStore=true fails closed at runtime. |
Ingestion:Nats:Enabled / Ingestion:Redis:Enabled (+ Url/ConnectionString, Subject/Stream, queue/consumer group, batch/prefetch) | Ingestion transports. Outside Testing, at least one must be enabled or startup fails (ValidateOnStart). |
UnifiedAudit:Sources:{Authority,JobEngine,Policy,EvidenceLocker,Notify} (or STELLAOPS_*_URL) | Base URLs for the legacy HTTP audit poller (now neutered, retained for transition). |
UnifiedAudit:FetchLimitPerModule, UnifiedAudit:RequestTimeoutSeconds | Legacy HTTP poller tuning. |
AuditRetentionPurge:{Enabled,DryRun,InitialDelay,Interval} | Scheduled retention purge service. |
Timeline:BaselineRollup:{Enabled,IntervalSeconds,BatchSize,MaxDistinctGroups} (or TIMELINE_BASELINE_ROLLUP_INTERVAL_SECONDS / TIMELINE_BASELINE_ROLLUP_MAX_DISTINCT_GROUPS) | Actor-behavior baseline rollup hosted service. MaxDistinctGroups defaults to 10,000 and is clamped to 20,000 as the per-pass memory safety ceiling. |
Router:*, Cors, localization | Standard Stella service wiring. Env vars are also bound with the TIMELINE_ prefix. |
Endpoints
Scopes: timeline:read, timeline:write, timeline:admin (mapped to the Timeline.Read/Timeline.Write/Timeline.Admin policies), plus the narrow audit:ingest machine scope accepted by the ingest policy. Interactive callers authenticate with an Authority JWT. Router-forwarded internal callers use a verified, short-lived signed identity envelope. Direct service HTTP callers use an Authority-issued bearer token: ExportCenter’s audit-bundle reader requests only tenant-bound timeline:read, while emitters use tenant-bound audit:ingest. Neither path may derive tenant identity from a raw header.
HLC timeline — /api/v1/timeline/hlc/*
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/timeline/hlc/{correlationId} | read | Query events by correlation ID (HLC-ordered); supports limit/offset/fromHlc/toHlc/services/kinds. |
| GET | /api/v1/timeline/hlc/{correlationId}/critical-path | read | Critical path analysis (longest latency stages) for a correlation. |
| POST | /api/v1/timeline/hlc/{correlationId}/replay | write | Initiate deterministic replay (dry-run/verify). 501 outside Dev/Test. |
| GET | /api/v1/timeline/hlc/replay/{replayId} | write | Replay status lookup. |
| POST | /api/v1/timeline/hlc/replay/{replayId}/cancel | write | Cancel replay operation. |
| DELETE | /api/v1/timeline/hlc/replay/{replayId} | write | Delete/cancel a replay operation. |
| POST | /api/v1/timeline/hlc/{correlationId}/export | write | Initiate timeline export (NDJSON/JSON, optional DSSE signing). 501 outside Dev/Test. |
| GET | /api/v1/timeline/hlc/export/{exportId} | write | Export status lookup. |
| GET | /api/v1/timeline/hlc/export/{exportId}/download | write | Download the completed export bundle. |
Indexer event queries — /api/v1/timeline/* (and bare /timeline/*)
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/timeline | read | List indexer events for the tenant (filters: eventType, source, correlationId, traceId, severity, since, after, limit). |
| GET | /api/v1/timeline/{eventId} | read | Get a single indexer event. |
| GET | /api/v1/timeline/{eventId}/evidence | read | Get evidence linkage (bundle/attestation digests) for an event. |
| POST | /api/v1/timeline/events | write | Ingests a TimelineEventEnvelope through the same scoped ITimelineIngestionService the message-bus worker (TimelineIngestionWorker) uses: validates required fields, enforces tenant ownership against the authenticated tenant, and persists via the indexer store. Returns 202 Accepted with { "status": "accepted", "eventId": … } on insert, 200 OK with { "status": "duplicate", … } on an idempotent re-submit, 400 for a missing body / missing required field, and 403 if the body tenant differs from the caller’s tenant. |
Unified audit — /api/v1/audit/*
| Method | Path | Scope | Description |
|---|---|---|---|
| POST | /api/v1/audit/ingest | audit:ingest or write | Ingest a single audit event from any service (202 Accepted); the authenticated tenant is bound to the payload tenant. |
| GET | /api/v1/audit/events | read | PostgreSQL-filtered event list with exact total count and stable (timestamp DESC, id ASC) cursor pagination. An unknown cursor preserves first-page behavior. |
| GET | /api/v1/audit/events/{eventId} | read | Event-by-id lookup. |
| GET | /api/v1/audit/stats | read | Exact tenant-scoped PostgreSQL summary statistics, including top actors/resources. |
| GET | /api/v1/audit/timeline/search | read | Tenant-filtered timeline search (q, date range, limit) without the legacy 10,000-event truncation. |
| GET | /api/v1/audit/correlations | read | Tenant-filtered correlation cluster list without the legacy 10,000-event truncation. |
| GET | /api/v1/audit/correlations/{correlationId} | read | Correlation cluster details. |
| GET | /api/v1/audit/chain/verify | read | Verify the SHA-256 hash chain integrity for a tenant (optional sequence range). |
| GET | /api/v1/audit/anomalies | read | List anomaly alerts (acknowledged, limit). |
| POST | /api/v1/audit/anomalies/{alertId}/acknowledge | write | Acknowledge an anomaly alert. |
| GET | /api/v1/audit/anomalies/_pending-dispatch | admin | Pending, unacknowledged alerts for Notify dispatch fan-out. |
| POST | /api/v1/audit/anomalies/{alertId}/dispatched | admin | Mark an alert dispatched after Notify fan-out succeeds. |
| POST | /api/v1/audit/anomalies/{alertId}/dispatch-failed | admin | Terminally mark an alert failed after Notify exhausts retries. |
| GET | /api/v1/audit/anomalies/rules | admin | List tenant-scoped anomaly rule configuration. |
| POST | /api/v1/audit/anomalies/rules | admin | Create an anomaly rule. |
| PUT | /api/v1/audit/anomalies/rules/{id} | admin | Update an anomaly rule. |
| DELETE | /api/v1/audit/anomalies/rules/{id} | admin | Delete an anomaly rule. |
| POST | /api/v1/audit/export | write | Request a unified audit export with an exact PostgreSQL-filtered event count. |
| GET | /api/v1/audit/export/{exportId} | read | Export status lookup. |
| GET | /api/v1/audit/retention-policies | read | Effective retention window (days) per classification for the tenant. |
| DELETE | /api/v1/audit/actors/{actorId}/pii | admin | GDPR Art. 17 right-to-erasure: redact actor PII while keeping the hash chain verifiable. |
Health
| Method | Path | Description |
|---|---|---|
| GET | /health | Runs the registered TimelineHealthCheck, which probes the StellaOps.Eventing event store (CountByCorrelationIdAsync). |
| GET | /health/ready, /health/live | Predicate-filtered (ready/live tags). TimelineHealthCheck is registered without tags, so no checks currently match either predicate and both return healthy once the host is up. |
Observability
Metrics and traces are emitted from the StellaOps.Timeline meter / ActivitySource (both version 1.0.0), defined in TimelineMetrics (StellaOps.Timeline.Core/Telemetry/TimelineMetrics.cs).
Metrics
| Instrument | Type | Unit | Tags | Description |
|---|---|---|---|---|
stellaops_timeline_queries_total | Counter | — | query_type | HLC timeline queries. |
stellaops_timeline_query_duration_seconds | Histogram | s | query_type, event_count_bucket | HLC query latency. |
stellaops_timeline_replays_total | Counter | — | mode, status | Replay operations. |
stellaops_timeline_replay_duration_seconds | Histogram | s | mode, event_count_bucket | Replay latency. |
stellaops_timeline_exports_total | Counter | — | format, signed | Export operations. |
stellaops_timeline_export_size_bytes | Histogram | By | format, event_count_bucket | Exported bundle size. |
stellaops_timeline_cache_hits_total / stellaops_timeline_cache_misses_total | Counter | — | cache_type | Query cache hit/miss. |
timeline_baseline_rollup_duration_seconds | Histogram | s | — | Anomaly v2 actor-baseline rollup pass duration. |
timeline_baseline_rollup_rows_upserted_total | Counter | — | — | Actor-baseline rows upserted by rollups. |
timeline_baseline_rollup_degraded_total | Counter | — | reason | Rollup passes that completed with an explicitly partial projection. |
timeline_baseline_rollup_omitted_source_events_total | Counter | — | reason | Source events omitted from partial rollups after the distinct-group hard bound was reached. |
timeline_baseline_actor_count | ObservableGauge | — | tenant_id | Per-tenant actor count from the latest baseline rollup. |
event_count_bucket is bucketed as 1-10 / 11-100 / 101-1000 / 1001-10000 / 10000+.
Traces
| Span | Kind | Tags |
|---|---|---|
timeline.query | Server | correlation_id, query_type |
timeline.replay | Server | correlation_id, mode |
Security Considerations
- Authentication: Interactive endpoints accept a valid JWT issued by Authority. Internal audit reads and ingest calls may instead use a verified signed service identity envelope; callers fail closed when the shared envelope key is absent. Authorization outcomes for timeline read/write are logged via
TimelineAuthorizationAuditSink(IAuthEventSink). - Scope model:
timeline:readfor tenant-scoped reads, including effective retention-policy queries; narrowaudit:ingestfor service emitters (timeline:writeis also accepted by that endpoint);timeline:writefor acknowledge, export, and replay/export initiation;timeline:adminfor anomaly-rule CRUD, the Notify dispatch worklist, and GDPR PII redaction. - Tenant isolation: Queries, unified audit rows, anomaly state, and export status are scoped to the authenticated tenant; cross-tenant access is prohibited. Indexer tables additionally enforce PostgreSQL Row-Level Security keyed on
app.current_tenant. - Tamper evidence:
timeline.unified_audit_eventsis an append-only log with a per-tenant SHA-256 hash chain (content_hash→previous_entry_hash, monotonicsequence_number), written underSERIALIZABLEisolation. Retryable PostgreSQL concurrency aborts repeat the complete transaction; duplicate deliveries roll back the allocated sequence.GET /api/v1/audit/chain/verifydetects breaks. GDPR redaction preservesactor_idso the chain stays verifiable. - Data classification + retention: Events are classified (
none/personal/sensitive/restricted); a scheduled purge enforces per-classification retention windows and honourscompliance_hold(legal hold). - Ingest surface:
POST /api/v1/audit/ingestaccepts service-to-service events under narrowaudit:ingestor operatortimeline:write; the authenticated tenant is checked against the payload tenant, and failures return503so emitters can retry without data loss. - Export controls: Exported event payloads may contain sensitive operational data; exports are tracked in durable per-tenant state.
- Replay determinism: Replay operations produce identical output given identical input sequences, supporting audit and compliance verification. Production replay/export endpoints fail closed (
501) when only process-local implementations are available.
TimelineIndexer (Event Ingestion and Indexing)
TimelineIndexer was consolidated into the Timeline module (Sprint 210, 2026-03-04). It provides the write/ingestion side of the CQRS pattern while Timeline provides the read/query side. Both share the same schema domain and live under src/Timeline/.
TimelineIndexer Responsibilities
- Event ingestion: Consumes events from NATS/Redis message bus via configurable subscribers.
- HLC timestamping: Assigns Hybrid Logical Clock timestamps to establish causal ordering.
- Event indexing: Writes indexed events to PostgreSQL via EfCore (compiled model preserved for migration identity).
- Authorization audit: Provides audit sink for authorization events.
Ingestion Transport Contract
- Non-testing Timeline hosts must enable at least one real ingestion transport:
Ingestion:Nats:Enabled=trueorIngestion:Redis:Enabled=true. NullTimelineEventSubscriberis a testing-only harness and is not registered in live hosts.- If both transports are disabled outside
Testing, startup fails fast with a configuration error instead of exposing an idle ingestion worker.
Deployable Services
- Timeline WebService (
StellaOps.Timeline.WebService): the single live deployable. It hosts the unified audit sink, the HLC query/analysis/export/replay API, the indexer query/evidence endpoints, the in-process ingestion worker (NATS/Redis), the retention purge service, and the anomaly v2 hosted services. - The local Compose topology assigns this unified deployable the
resources-mediumtier (0.50CPU,1 GiBmemory). The lighter512 MiBtier is insufficient for its combined request, ingestion, retention, and anomaly-rollup responsibilities under concurrent audit reads. - TimelineIndexer WebService (
StellaOps.TimelineIndexer.WebService) and TimelineIndexer Worker (StellaOps.TimelineIndexer.Worker): dormant. These standalone projects remain in the tree, but their ingestion/query logic was merged intoStellaOps.Timeline.WebService, which runs the ingestion worker in-process (AddTimelineIngestionRuntime). They are not deployed independently in current topologies.
The merged design keeps ingestion and query co-hosted while sharing domain libraries under a single module boundary. (Roadmap: the dormant projects are retained should ingestion ever need to scale out as a separate container again.)
