Skip to content

Observability — Serilog & OpenTelemetry

Granit.Observability wires Serilog structured logging and OpenTelemetry (traces + metrics) into a single AddGranitObservability() call. Logs ship to Loki, traces to Tempo, and metrics to Mimir — all via an OTLP collector.

PackageRoleDepends on
Granit.ObservabilitySerilog + OpenTelemetry (traces, metrics, logs)Granit
graph LR
    App[ASP.NET Core App] --> Serilog
    App --> OTel[OpenTelemetry SDK]

    Serilog -->|WriteTo.OpenTelemetry| Collector[OTLP Collector :4317]
    OTel -->|OTLP gRPC| Collector

    Collector --> Loki[Loki — Logs]
    Collector --> Tempo[Tempo — Traces]
    Collector --> Mimir[Mimir — Metrics]

    Loki --> Grafana
    Tempo --> Grafana
    Mimir --> Grafana
[DependsOn(typeof(GranitObservabilityModule))]
public class AppModule : GranitModule { }
{
"Observability": {
"ServiceName": "my-backend",
"ServiceVersion": "1.2.0",
"OtlpEndpoint": "http://otel-collector:4317",
"ServiceNamespace": "my-company",
"Environment": "production"
}
}

AddGranitObservability() configures two Serilog sinks:

SinkPurpose
ConsoleLocal development, [HH:mm:ss LEV] SourceContext Message
OpenTelemetryOTLP export to Loki via the collector

Every log entry is enriched with ServiceName, ServiceVersion, and Environment properties, matching the OpenTelemetry resource attributes for correlation.

Additional Serilog settings (minimum level, overrides, extra sinks) can be added via standard Serilog configuration in appsettings.jsonReadFrom.Configuration is called before the Granit enrichers.

Three built-in instrumentations are registered automatically:

InstrumentationWhat it captures
ASP.NET CoreInbound HTTP requests (method, route, status code)
HttpClientOutbound HTTP calls (dependency tracking)
EF CoreDatabase queries (command text, duration)

Health check endpoints (/health/*) are filtered out of traces to avoid noise.

Granit modules register their own ActivitySource names via GranitActivitySourceRegistry.Register() during host configuration. AddGranitObservability() reads the registry and calls AddSource() for each — no manual wiring needed.

// Inside a module's AddGranit*() extension method
GranitActivitySourceRegistry.Register("Granit.Workflow");
ActivitySourceSpan names
Granit.Vaultvault.encrypt, vault.decrypt, vault.get-secret, vault.check-rotation
Granit.Vault.Azureakv.encrypt, akv.decrypt, akv.get-secret, akv.check-rotation
Granit.Wolverinewolverine.send, wolverine.handle
Granit.Notificationsnotification.dispatch, notification.deliver
Granit.Notifications.Email.Smtpsmtp.send
Granit.Notifications.Email.AwsSesses.send
Granit.Notifications.Email.AzureCommunicationServicesacs-email.send
Granit.Notifications.Sms.AzureCommunicationServicesacs-sms.send
Granit.Notifications.MobilePush.AzureNotificationHubsanh.send
Granit.Notifications.Brevobrevo.send
Granit.Notifications.Zulipzulip.send
Granit.Workflowworkflow.transition
Granit.BlobStorageblob.upload, blob.download, blob.delete
Granit.DataExchangeimport.execute, export.execute
Granit.EventBuseventbus.publish.local, eventbus.publish.distributed
Granit.Privacyprivacy.export.execute, privacy.deletion.execute
Granit.Observability.AIobservability-ai.analysis.execute
Granit.Persistencepersistence.save, persistence.purge

Logs and trace spans must never contain raw PII (GDPR Art. 5, ISO 27001 A.5.34). The Granit.Diagnostics.LogRedaction static class provides redaction helpers for use in [LoggerMessage] call sites and Activity.SetTag() calls:

MethodInputOutputUse case
Email(string)[email protected]joh***@example.comLog templates
EmailDomain(string)[email protected]example.comSpan tags (bounded cardinality)
Phone(string)+33612345678+336*****78Log templates
Token(string)dLkj3FDmAbCdEfGhdLkj...fGhDevice tokens, API tokens
IpAddress(string)192.168.1.42192.168.1.***IPv4 /24 masking
Username(string)john_adminjoh***DB credentials, identity
HashPrefix(string)(any)a1b2c3d4Non-reversible correlation (span tags)

Architecture tests enforce PII-safe naming in [LoggerMessage] templates (LoggerMessagePiiConventionTests) and *ActivitySource.cs tag constants (ActivitySourcePiiConventionTests). GUIDs (user IDs, session IDs) are pseudonymous and exempt.

Logs and trace spans must never contain raw PII (GDPR Art. 5, ISO 27001 A.5.34). The Granit.Diagnostics.LogRedaction static class provides redaction helpers for use in [LoggerMessage] call sites and Activity.SetTag() calls:

MethodInputOutputUse case
Email(string)[email protected]joh***@example.comLog templates
EmailDomain(string)[email protected]example.comSpan tags (bounded cardinality)
Phone(string)+33612345678+336*****78Log templates
Token(string)dLkj3FDmAbCdEfGhdLkj...fGhDevice tokens, API tokens
IpAddress(string)192.168.1.42192.168.1.***IPv4 /24 masking
Username(string)john_adminjoh***DB credentials, identity
HashPrefix(string)(any)a1b2c3d4Non-reversible correlation (span tags)

Architecture tests enforce PII-safe naming in [LoggerMessage] templates (LoggerMessagePiiConventionTests) and *ActivitySource.cs tag constants (ActivitySourcePiiConventionTests). GUIDs (user IDs, session IDs) are pseudonymous and exempt.

PropertyTypeDefaultDescription
ServiceNamestring"unknown-service"Service name for OTEL resource
ServiceVersionstring"0.0.0"Service version
OtlpEndpointstring"http://localhost:4317"OTLP gRPC endpoint
ServiceNamespacestring"my-company"Service namespace
Environmentstring"development"Deployment environment
EnableTracingbooltrueEnable trace export via OTLP
EnableMetricsbooltrueEnable metrics export via OTLP
CategoryKey typesPackage
ModuleGranitObservabilityModule
OptionsObservabilityOptionsGranit.Observability
RegistryGranitActivitySourceRegistryGranit
PII redactionLogRedactionGranit
ExtensionsAddGranitObservability()Granit.Observability