* Add structured log persistence spec * Clarify structured log persistence spec * Plan structured log persistence implementation * Regenerate structured log persistence tasks * Address structured log persistence analysis findings * Add structured log SQLite persistence * Address structured log persistence review * Harden structured log write buffer shutdown * Start structured log SQLite migrations before buffer
13 KiB
Feature Specification: Structured Log Persistence
Feature Branch: 005-structured-log-persistence
Created: 2026-05-12
Status: Draft
Input: User description: "Make structured log storage a pluggable concern. Keep in-memory support, add an easy durable SQLite storage story first, use FluentMigrator for schema management, and defer OpenTelemetry/exporter scope."
Clarifications
Session 2026-05-12
- Q: Should this feature add vendor sinks such as Logstash, Datadog, Splunk, Loki, or Seq now? -> A: No. Keep the first slice small.
- Q: Should this feature add an OTLP logs exporter now? -> A: No. Defer OpenTelemetry until the diagnostics OpenTelemetry boundary is clearer.
- Q: What storage providers should exist initially? -> A: Preserve in-memory storage and add opt-in SQLite durable storage.
- Q: Should SQLite be implemented as a one-off store? -> A: No. Implement it as the first relational provider so SQL Server, PostgreSQL, MySQL, and similar providers can be added later.
- Q: Should EF Core be used for persistence? -> A: No. Avoid EF Core and per-provider EF migrations for this feature.
- Q: Should FluentMigrator manage schema creation and upgrades? -> A: Yes. Use FluentMigrator for schema versioning and migration execution; use explicit SQL/Dapper-style access for the hot storage path.
Session 2026-05-13
- Q: What durability guarantee should SQLite writes provide on the logging hot path? -> A: Async batched writes with graceful-shutdown flush; a crash may lose queued events.
- Q: What should happen when the SQLite write queue is full? -> A: Drop newest queued events and report dropped counts/metrics.
- Q: Should SQLite migrations run automatically on startup? -> A: Run migrations on startup by default, with opt-out.
- Q: How should SQLite persist timestamps? -> A: Store timestamps as UTC ISO-8601 text.
- Q: What default retention policy should SQLite use? -> A: No default deletion; retention runs only when max age or max rows is configured.
User Scenarios & Testing (mandatory)
User Story 1 - Preserve the existing in-memory structured logs behavior (Priority: P1)
An Elsa host developer enables structured logs without configuring durable persistence and gets the same bounded in-memory recent history and live streaming behavior as today.
Why this priority: The storage refactor must not regress the current module. In-memory remains the zero-configuration default and the fastest validation path.
Independent Test: Enable UseStructuredLogs with no persistence options, emit several ILogger events, and verify recent queries, filtering, source listing, live subscriptions, redaction, and dropped-event summaries continue to work.
Acceptance Scenarios:
- Given no durable store is configured, When structured logs are enabled, Then the module uses bounded in-memory storage by default.
- Given recent logs are queried through the existing REST endpoint, When events have been captured, Then the endpoint returns filtered recent events using the same contract as before.
- Given Studio subscribes over SignalR, When new matching events are emitted, Then live events and dropped-event summaries continue to stream.
User Story 2 - Enable durable SQLite structured log storage (Priority: P2)
An Elsa host developer opts into SQLite persistence so structured log entries survive process restarts and can be queried by Studio after restart.
Why this priority: Customers need an easy persistence story without standing up a separate database service or observability stack.
Independent Test: Configure SQLite storage, emit structured log entries, restart or recreate the service provider with the same database file, and verify recent queries return the previously written events.
Acceptance Scenarios:
- Given SQLite storage is configured with a database file, When structured log events are captured, Then the events are written durably to SQLite.
- Given the host restarts with the same SQLite database, When recent structured logs are queried, Then persisted events are returned according to the filter.
- Given retention settings are configured, When cleanup runs, Then old or excess records are deleted without breaking recent queries.
- Given schema migrations have not run, When the SQLite provider starts, Then FluentMigrator creates or upgrades the structured log schema.
- Given an operator disables startup migrations, When the SQLite provider starts, Then it does not run migrations and requires the schema to be prepared separately.
- Given no SQLite retention settings are configured, When cleanup runs, Then no persisted log entries are deleted by default.
User Story 3 - Keep relational persistence extensible for future databases (Priority: P3)
An Elsa maintainer can add SQL Server, PostgreSQL, MySQL, or another relational provider later without rewriting the structured logs module or changing Studio contracts.
Why this priority: SQLite should prove the relational persistence model, not become a dead-end implementation.
Independent Test: Review the relational contracts, dialect hooks, migrations, and SQLite package to confirm provider-specific behavior is isolated from the core structured logs module.
Acceptance Scenarios:
- Given a future database provider is added, When it supplies a connection factory, SQL dialect, and FluentMigrator runner registration, Then it can reuse the shared relational store implementation.
- Given a provider needs database-specific DDL, When migrations run, Then the migration can branch by database while keeping a shared migration version.
- Given Studio uses existing structured logs REST and SignalR contracts, When storage changes from in-memory to SQLite, Then Studio requires no API changes.
Edge Cases
- Durable writes must not block
ILoggercallers on slow disk I/O; SQLite persistence accepts possible loss of queued-but-unflushed events after process crashes. - If the SQLite write queue reaches capacity, newest events are dropped and loss counts are reported instead of blocking logging calls or growing memory without bound.
- A host can emit logs before migrations complete if startup ordering is wrong.
- Multiple app instances can start against a shared future relational database and attempt migrations concurrently.
- SQLite file paths can be missing, relative, or point to directories without write permissions.
- A process can crash after events enter a background queue but before they are flushed.
- Free-text filtering over JSON fields can be provider-specific and initially approximate.
- Retention cleanup can race with recent queries or live subscriptions.
- Schema changes must preserve already persisted log events where practical.
Requirements (mandatory)
Functional Requirements
Storage abstraction
- FR-001: The structured logs module MUST separate queryable storage from live streaming and capture/provider facade responsibilities.
- FR-002: The existing
IStructuredLogProvidercontract MAY remain as the facade used by REST endpoints and SignalR, but storage-specific responsibilities MUST be represented by a replaceable store abstraction. - FR-003: The storage abstraction MUST support appending redacted
StructuredLogEventvalues, querying recent events byStructuredLogFilter, and listing structured log sources. - FR-004: Live streaming MUST remain available when durable storage is configured.
- FR-005: Redaction MUST continue to happen before any event reaches in-memory storage, SQLite storage, or live subscribers.
In-memory default
- FR-006: The module MUST keep bounded in-memory storage as the default when no durable storage provider is configured.
- FR-007: The in-memory implementation MUST preserve existing recent query, source listing, subscription, dropped-event summary, capacity, and filtering behavior.
SQLite durable storage
- FR-008: The feature MUST add an opt-in SQLite structured log persistence provider.
- FR-009: SQLite storage MUST persist structured log events across process restarts.
- FR-010: SQLite storage MUST support the existing
StructuredLogFilterfields used by the REST recent-log endpoint. - FR-011: SQLite storage MUST persist scalar filter fields as queryable columns and persist exception, scopes, and properties as serialized JSON.
- FR-012: SQLite storage MUST provide configurable retention by maximum age and/or maximum row count.
- FR-013: SQLite storage MUST persist
TimestampandReceivedAtas UTC ISO-8601 text values. - FR-014: SQLite writes MUST be batched or queued so logging calls do not synchronously wait on disk I/O.
- FR-015: SQLite storage MUST flush queued events during graceful shutdown where possible.
- FR-016: SQLite storage MAY lose events that were queued but not flushed before an ungraceful process crash.
- FR-017: SQLite storage MUST use a bounded write queue.
- FR-018: When the SQLite write queue is full, SQLite storage MUST drop newly received events instead of blocking logging calls or using unbounded memory.
- FR-019: SQLite storage MUST expose dropped-write counts through
IStructuredLogWriteBuffer.DroppedWriteCountand log warning summaries. - FR-020: SQLite retention MUST delete no records by default unless
MaxAge,MaxRows, or both are configured.
Relational extensibility
- FR-021: Shared relational persistence code MUST be reusable by future SQL Server, PostgreSQL, MySQL, and similar providers.
- FR-022: Provider-specific SQL syntax MUST be isolated behind dialect or provider services.
- FR-023: The first SQLite provider MUST not introduce SQLite-only assumptions into the core structured logs module.
- FR-024: Future relational providers MUST be able to supply their own connection factory, dialect, and migration runner configuration without changing REST, SignalR, or Studio contracts.
Schema management
- FR-025: Relational schema creation and upgrades MUST use FluentMigrator.
- FR-026: Migrations MUST be versioned and idempotently executable by the host.
- FR-027: Provider packages MUST register only the FluentMigrator runner dependencies they need.
- FR-028: SQLite storage MUST run migrations on startup by default.
- FR-029: SQLite storage MUST provide an option to disable startup migrations for hosts that prepare schemas separately.
- FR-030: The feature MUST document startup migration behavior and the multi-instance locking consideration for future shared database providers.
Configuration and safety
- FR-031: Host configuration MUST make the active storage mode explicit when SQLite is selected.
- FR-032: The existing no-configuration in-memory setup MUST continue to work.
- FR-033: The feature MUST document that OpenTelemetry/exporter sinks are deferred and out of scope for this persistence slice.
Key Entities (include if feature involves data)
- Structured Log Store: Queryable storage for redacted structured log events and sources.
- Structured Log Live Feed: Runtime stream of new events and dropped-event summaries for SignalR subscribers.
- Relational Structured Log Record: Database representation of a
StructuredLogEventwith indexed scalar columns and JSON payload columns. - Relational Storage Dialect: Provider-specific SQL and parameter behavior needed by the shared relational store.
- Structured Log Migration Runner: FluentMigrator-based schema creation and upgrade service.
- Structured Log Retention Policy: Settings and cleanup behavior that bound durable storage growth.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: Existing structured logs unit and integration tests continue to pass with the in-memory default.
- SC-002: New tests verify SQLite persistence survives service-provider or process recreation with the same database file.
- SC-003: New tests verify SQLite recent queries honor level, category, source, workflow, correlation, trace/span, time range, and limit filters.
- SC-004: New tests verify FluentMigrator creates the SQLite schema from an empty database.
- SC-005: New tests verify retention cleanup removes expired or excess records only when retention settings are configured.
- SC-006: New tests verify queued SQLite writes are flushed during graceful shutdown.
- SC-007: New tests verify a full SQLite write queue drops newest events and reports dropped-write counts.
- SC-008: New tests verify SQLite startup migrations run by default and can be disabled.
- SC-009: New tests verify SQLite stores and filters UTC ISO-8601 timestamp text values consistently.
- SC-010: New tests verify SQLite retention deletes no records by default.
- SC-011: Documentation shows both zero-configuration in-memory setup and opt-in SQLite setup.
- SC-012: The core structured logs module does not reference SQLite-specific types.
Assumptions
- The structured logs module from
004-diagnostics-structured-logsis available and remains the owning diagnostics module. - SQLite is the only durable provider implemented in this feature.
- Future relational providers can be added in separate specs and packages.
- OpenTelemetry logs export, vendor sinks, raw console streaming, and trace/metric exploration remain out of scope.
- The implementation can introduce new package dependencies such as FluentMigrator and Dapper where appropriate, with versions managed centrally.