Common.Logging (Serilog + OpenTelemetry)
Logging is configured once, in a shared library, and every service opts in with one line:
builder.Host.UseSerilog(Logging.ConfigureLogger);What ConfigureLogger does
Section titled “What ConfigureLogger does”src/BuildingBlocks/Common.Logging/Logging.cs exposes a static Action<HostBuilderContext, LoggerConfiguration>:
| Setting | Value |
|---|---|
| Minimum level | Information |
| Overrides | Microsoft.AspNetCore and Microsoft.Hosting.Lifetime at Warning |
| Development overrides | Catalog, Basket, Discount, Ordering namespaces at Debug |
| Enrichers | FromLogContext, ApplicationName, EnvironmentName, WithExceptionDetails (Serilog.Exceptions) |
| Sinks | Console always; Elasticsearch when ElasticConfiguration:Uri is set |
| Elasticsearch options | AutoRegisterTemplate, template version ESv8, index ecommerce-Logs-{yyyy.MM.dd}, min level Debug |
Because Enrich.FromLogContext() is on, logging scopes flow into every event. BasketOrderingConsumer opens a scope with the message’s CorrelationId, so every log line written while handling a checkout carries it, and you can search Kibana for a single checkout.
Handlers mostly use structured message templates ("Deleting image from S3: Bucket={Bucket}, Key={Key}"), which Elasticsearch indexes as fields. A few older call sites use string interpolation ($"Order with Id {generatedOrder.Id} ..."), which loses the structure. Those are worth converting.
flowchart LR
svc["Service code (ILogger)"] --> serilog["Serilog pipeline (enrichers)"]
serilog --> console["Console sink: kubectl logs / docker logs"]
serilog --> es[("Elasticsearch index ecommerce-Logs-yyyy.MM.dd")]
es --> kibana["Kibana :5601"]
Tracing (per service, not in the shared library)
Section titled “Tracing (per service, not in the shared library)”Tracing is not in Common.Logging. Each Program.cs repeats the same block:
builder.Services.AddOpenTelemetry() .WithTracing(t => t .AddSource("Catalog.API") .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Catalog.API")) .AddAspNetCoreInstrumentation() .AddOtlpExporter(o => o.Endpoint = new Uri( builder.Configuration["Otlp:Endpoint"] ?? "http://jaeger-collector.istio-system:4317")));| Service | Instrumentation |
|---|---|
| Catalog | ASP.NET Core |
| Basket | ASP.NET Core + gRPC client (AddGrpcClientInstrumentation) |
| Discount | ASP.NET Core (covers incoming gRPC) |
| Ordering | ASP.NET Core |
Consequences:
- An add-to-cart request shows the Basket server span, the gRPC client span and the Discount server span in one trace, because W3C
traceparentpropagates over HTTP/2. - MassTransit’s
ActivitySourceis not added, so a checkout trace stops at the publish. The Ordering consumer starts a separate, unlinked trace. - There are no spans for MongoDB, Redis, Npgsql or EF Core.
- The default endpoint assumes the Jaeger collector in the
istio-systemnamespace. In Docker Compose, where no Jaeger runs, the exporter fails quietly in the background.
Moving this block into a shared AddPlatformTelemetry() extension, adding MassTransit and EF Core instrumentation, and adding OTel metrics would close these gaps. See Observability for the mesh-level view Istio adds.