LogEvents Schema
Stratara mandates source-generated [LoggerMessage] for all new logging — no logger.LogInformation(...) direct calls. Every log event has a stable EventId from a known range.
ID range allocation
| Range | Owner |
|---|---|
0 – 99_999 |
Reserved (Microsoft / framework defaults) |
100_000 – 199_999 |
Stratara framework (this repo) — currently allocated 100_000 – 113_999 |
200_000+ |
Consumer applications |
Sub-buckets inside the framework's 100_000 range are defined in src/Stratara.Diagnostics/LogEvents.cs. The current allocation:
| Bucket | Subsystem | LogEvents nested class |
|---|---|---|
100_000s |
Change-set / aggregate-update | LogEvents.ChangeSet |
101_000s |
Background-task queue | LogEvents.BackgroundTasks |
102_000s |
Event-store append / read | LogEvents.EventStore |
103_000s |
Validation | LogEvents.Validation |
104_000s |
Projection worker | LogEvents.Projection |
105_000s |
Command-handling worker | LogEvents.CommandProcessing |
106_000s |
Outbox worker | LogEvents.OutboxProcessing |
107_000s |
Event-stream-hash worker | LogEvents.EventStreamHashing |
108_000s |
Messaging | LogEvents.Messaging |
109_000s |
Aggregate update | LogEvents.Update |
110_000s |
Saga worker | LogEvents.Saga |
111_000s |
Event-bundle integrity | LogEvents.EventBundleIntegrity |
112_000s |
Key management | LogEvents.KeyManagement |
113_000s |
Bus-envelope integrity (startup probe) | LogEvents.BusEnvelopeIntegrity |
Even hundreds are info/debug, the _1xx band is error (e.g. 100_002 info, 100_101 error). Consult src/Stratara.Diagnostics/LogEvents.cs for the authoritative current list — buckets shift as features mature.
Authoring a new log event
- Pick a bucket. In your own app, start at
200_000+; the100_000block is the framework's. Add a nested class per subsystem. - Add the constants as plain literals — even hundreds for info/debug, the
_1xxband for errors:public static class OrderProjection { public const int OrderProjectionStarted = 200_001; // info public const int OrderProjectionFailed = 200_101; // error } - Add the
[LoggerMessage]partial method in apublic static partial class(the source generator requires it):[LoggerMessage( EventId = OrderProjection.OrderProjectionStarted, Level = LogLevel.Information, Message = "Order projection {ProjectionName} started.")] public static partial void LogOrderProjectionStarted(this ILogger logger, string projectionName);
Logger-extension file naming
| Convention | Example |
|---|---|
One Logger{Subject}Extensions.cs per subsystem |
LoggerProjectionExtensions.cs, LoggerSagaExtensions.cs |
Namespace Stratara.Shared.Diagnostics.Extensions regardless of source package |
All packages' logger extensions live in this single namespace |
Class is public static partial class |
Required by the LoggerMessage source generator |
Parameter-type discipline
[LoggerMessage] source-gen accepts any type, but Stratara's Clean Code rule restricts parameters to simple types (string, Guid, int, DateTimeOffset, enums). For aggregate / collection arguments that would otherwise force expensive formatting at call-time, use a small wrapper struct with ToString() — the formatter calls ToString() lazily, only when the channel is enabled.
Canonical examples in the repo:
Stratara.Shared.Diagnostics.Extensions.DistinctEventTypeNames— wrapsIReadOnlyList<IEvent>.Stratara.Projections.Diagnostics.Extensions.ChangeSetFieldNames— wrapsIReadOnlyList<ChangeDetail>.
What never to do
- ❌
logger.LogInformation("…", arg)— direct logger calls. - ❌
if (logger.IsEnabled(LogLevel.Debug)) { logger.LogXxx(...) }— manual IsEnabled guards. The source-gen formatter checks IsEnabled internally; expensive arguments belong in deferred-formatting wrappers. - ❌ Sharing an
EventIdacross two[LoggerMessage]methods — IDs are unique per code path. - ❌ Repurposing a freed
EventId— once shipped, anEventIdis part of the schema's observable contract.