Observe your system
FCQRS reports command, event, saga, dispatch, and projection activity through ILogger and
ActivitySource. Configure both before production so one correlation id can be followed across the
complete workflow.
Message-flow logs
At Information level, the FCQRS.MessageFlow category records aggregate decisions, persisted event
versions, saga transitions, and commands issued by sagas. Every line contains the correlation id:
info: FCQRS.MessageFlow
Command Publish (doc-42, "guides/fcqrs") to aggregate doc-42 yielded PersistEvent (PublicationRequested ...) [cid: ...]
info: FCQRS.MessageFlow
Aggregate doc-42 persisted event PublicationRequested (...) (v2) [cid: ...]
info: FCQRS.MessageFlow
Saga doc-42~PublicationSaga~... changed state to ReservingSlug [cid: ...]
info: FCQRS.MessageFlow
Saga doc-42~PublicationSaga~... sent command Reserve (...) to guides/fcqrs [cid: ...]
Disable the process-wide narrative with
FCQRS.Common.Telemetry.MessageFlowLogging <- false or
builder.WithMessageFlowLogging(false). Standard logger filtering also applies:
{
"Logging": {
"LogLevel": {
"FCQRS.MessageFlow": "None"
}
}
}
When the category is disabled, FCQRS skips formatting the message payload.
Distributed traces
Aggregates, sagas, and projections use three activity sources. A W3C traceparent is copied into
command metadata and carried through later events and saga commands. Register all three sources:
tracing.AddSource(FCQRS.Common.Telemetry.AllActivitySources); // "FCQRS", "FCQRS.Saga", "FCQRS.Query"
ActivitySource avoids creating activities when no listener is attached. Restart-detection aborts and
fatal errors set span status to Error.
Start an activity at the application boundary before constructing the first command. The resulting trace should contain the initial command, stored event, saga states, follow-up commands, and projection handler. The CID remains a domain correlation value; trace context travels beside it in metadata.
Span names are low-cardinality
Span names contain the case name, such as Command:Register, Event:Registered,
Saga:GeneratingCode, or Abort:VerificationRequested. Payload values do not appear in the span name,
so trace backends can group operations without creating one name per entity. On .NET 11, tracing rules
can select a source and operation:
builder.Services.AddTracing(tracing =>
{
tracing.EnableTracing(sourceName: "FCQRS.Saga");
tracing.DisableTracing(sourceName: "FCQRS", operationName: "Command:HealthPing");
});
Payload detail may still appear in tags and logs as described below.
Keep payloads out of diagnostics
Rendered payloads appear in span tags and message-flow logs by default. Disable them before processing sensitive values when detailed payload diagnostics are not acceptable:
FCQRS.Common.Telemetry.IncludePayloads <- false // or builder.WithPayloadDiagnostics(false)
FCQRSIncludePayloads: boolProcess-wide switch for including rendered message *payloads* in diagnostics. Default: on. Span *names* are always low-cardinality case names (e.g. "Command:Register") regardless of this switch — payload values never appear there, so per-operation tracing rules and trace-viewer grouping always work, and nothing sensitive leaks into the indexed span name. This switch governs the *detail*: the payload rendered into span tags (command.type / event.type) and into the message-flow log lines. Turn it off (Telemetry.IncludePayloads <- false, or FcqrsBuilder.WithPayloadDiagnostics(false)) for sensitive domains — tags and log lines then carry the case name only, matching the span name.
FCQRS.CommonContains common types like Events and Commands Functionality for Write Side.
FCQRS.Common.TelemetryFCQRS's ActivitySource names — register them with your tracing pipeline (e.g. OpenTelemetry: tracing.AddSource(Telemetry.AllActivitySources)).
builder.WithPayloadDiagnostics(false);
Tags and log lines then contain the case name only. This switch affects diagnostics, not persisted events. A secret stored in an event remains in the journal regardless of the diagnostics setting.
Flush telemetry on a fatal exit
FCQRS terminates the process when a fold, aggregate handler, saga handler, effect runner, or projection handler fails in a way that could leave state processing inconsistent. Fail-fast skips normal finalizer and process-exit flushing. Register a bounded flush hook for buffered telemetry:
Shared setup
open OpenTelemetry.Trace
open OpenTelemetry.Logs
let configureFlush (tracerProvider: TracerProvider) (loggerProvider: LoggerProvider) =
OpenTelemetryTraceLogsconfigureFlush: TracerProvider -> LoggerProvider -> unittracerProvider: TracerProviderOpenTelemetry.Trace.TracerProviderTracerProvider is the entry point of the OpenTelemetry API. It provides access to .
loggerProvider: LoggerProviderOpenTelemetry.Logs.LoggerProviderLoggerProvider is the entry point of the OpenTelemetry API. It provides access to .
FCQRS.Common.Telemetry.FatalFlush <- System.Action(fun () ->
tracerProvider.ForceFlush(3000) |> ignore
loggerProvider.ForceFlush(3000) |> ignore) // or Serilog's Log.CloseAndFlush()
FCQRSFatalFlush: System.Action | nullOptional hook invoked right before FCQRS kills the process on a fatal error. FailFast skips finalizers and ProcessExit handlers, so without this everything still sitting in a batch exporter or buffered log sink is silently dropped — including the span and log entry of the fatal flow itself (every fatal site logs before invoking this hook). Flush your whole pipeline here, e.g. Telemetry.FatalFlush <- Action(fun () -> tracerProvider.ForceFlush(3000) |> ignore loggerProvider.ForceFlush(3000) |> ignore) // or Serilog's Log.CloseAndFlush() It runs on a background thread with a 5-second cap so a hung exporter cannot block the kill.
FCQRS.CommonContains common types like Events and Commands Functionality for Write Side.
FCQRS.Common.TelemetryFCQRS's ActivitySource names — register them with your tracing pipeline (e.g. OpenTelemetry: tracing.AddSource(Telemetry.AllActivitySources)).
SystemSystem.ActionEncapsulates a method that has no parameters and does not return a value.
tracerProvider: TracerProviderForceFlush: int -> boolFlushes all the processors registered under TracerProviderSdk, blocks the current thread until flush completed, shutdown signaled or timed out. TracerProviderSdk instance on which ForceFlush will be called. The number (non-negative) of milliseconds to wait, or Timeout.Infinite to wait indefinitely. Returns true when force flush succeeded; otherwise, false. Thrown when the timeoutMilliseconds is smaller than -1. This function guarantees thread-safety.
(|>): 'T1 -> ('T1 -> 'U) -> 'UApply a function to a value, the value being on the left, the function on the right The argument. The function. The function result. let doubleIt x = x * 2 3 |> doubleIt // Evaluates to 6
ignore: 'T -> unitIgnore the passed value. This is often used to throw away results of a computation. The value to ignore. ignore 55555 // Evaluates to ()
loggerProvider: LoggerProviderForceFlush: int -> boolFlushes all the processors registered under , blocks the current thread until flush completed, shutdown signaled or timed out. instance on which ForceFlush will be called. The number (non-negative) of milliseconds to wait, or Timeout.Infinite to wait indefinitely. Returns true when force flush succeeded; otherwise, false. Thrown when the timeoutMilliseconds is smaller than -1. This function guarantees thread-safety.
FCQRS.Common.Telemetry.FatalFlush = new Action(() =>
{
tracerProvider.ForceFlush(3000);
loggerProvider.ForceFlush(3000); // or Serilog.Log.CloseAndFlush()
});
The hook runs on a background thread with a five-second cap.
Alerts to add
At minimum, alert on:
- process fail-fast and repeated restarts;
- projection handler failure and growing projection lag;
- a saga remaining in one state beyond its domain timeout;
- unreachable cluster members and shard movement that does not settle;
- journal or snapshot storage latency and errors;
- exhausted retries or workflows sent to manual intervention.
FCQRS supplies the message and trace context. Domain timeouts, projection-lag metrics, and manual-intervention counters belong to the application because only it knows the expected duration and business impact.
Akka's own logging
Akka.NET internal logging defaults to OFF; FCQRS logs still use the application's ILoggerFactory.
Enable Akka.NET internals with builder.WithAkkaLogging(AkkaLogLevel.Info)
from the hosting builder, or set config:akka:loglevel in configuration. See
Configuration for the config-key details.