From 271c54197ed803545ed0e3645a401793ec3e2c41 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Thu, 23 Jul 2026 14:35:47 +0200 Subject: [PATCH] feat(obs): OpenTelemetry distributed tracing across the five .NET services (refs #123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each host adds AddOpenTelemetry().WithTracing with ASP.NET Core + HttpClient auto-instrumentation and an OTLP exporter to Tempo; service name + endpoint from OTEL_* env set per app service in compose. Since every cross-service call goes through a typed HttpClient, traceparent propagates for free, so a request is one connected trace (bff → domain/projection → acl → openzaak). /health is filtered out. The exporter no-ops harmlessly when Tempo is unreachable (verified: domain boots healthy with no collector). refs #123 --- infra/docker-compose.yml | 20 +++++++++++++++++++ services/acl/Acl.Api/Acl.Api.csproj | 7 +++++++ services/acl/Acl.Api/Program.cs | 13 ++++++++++++ services/bff/Bff.Api/Bff.Api.csproj | 4 ++++ services/bff/Bff.Api/Program.cs | 14 +++++++++++++ services/domain/Big.Api/Big.Api.csproj | 4 ++++ services/domain/Big.Api/Program.cs | 15 ++++++++++++++ .../EventSubscriber.Api.csproj | 7 +++++++ .../EventSubscriber.Api/Program.cs | 13 ++++++++++++ .../ProjectionApi.Api/Program.cs | 13 ++++++++++++ .../ProjectionApi.Api.csproj | 7 +++++++ 11 files changed, 117 insertions(+) diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index fddfeef..f854bae 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -296,6 +296,10 @@ services: dockerfile: Dockerfile image: register-referentie/acl:dev environment: + # OpenTelemetry traces → Tempo (S-16b, ADR-0023). + OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317 + OTEL_EXPORTER_OTLP_PROTOCOL: grpc + OTEL_SERVICE_NAME: acl # Overridable so verify-domain can point the ACL at the same OpenZaak host that # owns the seeded zaaktype URL (host-consistent zaak creation, ADR-0009). Acl__OpenZaak__BaseUrl: ${ACL_OPENZAAK_BASEURL:-http://openzaak:8000/} @@ -334,6 +338,10 @@ services: dockerfile: Dockerfile image: register-referentie/domain:dev environment: + # OpenTelemetry traces → Tempo (S-16b, ADR-0023). + OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317 + OTEL_EXPORTER_OTLP_PROTOCOL: grpc + OTEL_SERVICE_NAME: domain Flowable__BaseUrl: http://flowable-rest:8080/flowable-rest/ Flowable__Username: rest-admin Flowable__Password: test @@ -360,6 +368,10 @@ services: dockerfile: Dockerfile image: register-referentie/bff:dev environment: + # OpenTelemetry traces → Tempo (S-16b, ADR-0023). + OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317 + OTEL_EXPORTER_OTLP_PROTOCOL: grpc + OTEL_SERVICE_NAME: bff # The BFF is the portals' only backend; it validates digid tokens and fans out (ADR-0010). # Keycloak (start-dev) derives the issuer from the request host, so the BFF authority and the # verify token request both use keycloak:8080 to keep the issuer consistent. @@ -412,6 +424,10 @@ services: dockerfile: services/event-subscriber/Dockerfile image: register-referentie/event-subscriber:dev environment: + # OpenTelemetry traces → Tempo (S-16b, ADR-0023). + OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317 + OTEL_EXPORTER_OTLP_PROTOCOL: grpc + OTEL_SERVICE_NAME: event-subscriber ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection # The subscriber enriches the projection with each zaak's reference (identificatie) by asking # the ACL — the only code allowed to read ZGW (§8.1, #78). @@ -441,6 +457,10 @@ services: dockerfile: services/projection-api/Dockerfile image: register-referentie/projection-api:dev environment: + # OpenTelemetry traces → Tempo (S-16b, ADR-0023). + OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317 + OTEL_EXPORTER_OTLP_PROTOCOL: grpc + OTEL_SERVICE_NAME: projection-api ConnectionStrings__Projection: Host=projection-db;Database=projection;Username=projection;Password=projection ports: - "8120:8080" diff --git a/services/acl/Acl.Api/Acl.Api.csproj b/services/acl/Acl.Api/Acl.Api.csproj index 0a0116c..316816e 100644 --- a/services/acl/Acl.Api/Acl.Api.csproj +++ b/services/acl/Acl.Api/Acl.Api.csproj @@ -5,6 +5,13 @@ + + + + + + + net10.0 enable diff --git a/services/acl/Acl.Api/Program.cs b/services/acl/Acl.Api/Program.cs index 22cceee..052e15d 100644 --- a/services/acl/Acl.Api/Program.cs +++ b/services/acl/Acl.Api/Program.cs @@ -1,8 +1,21 @@ using Acl.Application; using Acl.Infrastructure; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; var builder = WebApplication.CreateBuilder(args); +// OpenTelemetry tracing (S-16b, ADR-0023): auto-instrument incoming ASP.NET Core requests and +// outgoing HttpClient calls (the ACL → OpenZaak hop), exported over OTLP to Tempo. Service name + +// OTLP endpoint come from OTEL_* env (compose); the exporter no-ops when Tempo is unreachable. +builder.Services.AddOpenTelemetry() + .ConfigureResource(r => r.AddService( + builder.Configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName)) + .WithTracing(tracing => tracing + .AddAspNetCoreInstrumentation(o => o.Filter = ctx => ctx.Request.Path != "/health") + .AddHttpClientInstrumentation() + .AddOtlpExporter()); + builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService() .GetSection("Acl:Defaults").Get() diff --git a/services/bff/Bff.Api/Bff.Api.csproj b/services/bff/Bff.Api/Bff.Api.csproj index b97a2fc..cb4f665 100644 --- a/services/bff/Bff.Api/Bff.Api.csproj +++ b/services/bff/Bff.Api/Bff.Api.csproj @@ -10,6 +10,10 @@ + + + + diff --git a/services/bff/Bff.Api/Program.cs b/services/bff/Bff.Api/Program.cs index 8f7df13..dcadd14 100644 --- a/services/bff/Bff.Api/Program.cs +++ b/services/bff/Bff.Api/Program.cs @@ -3,9 +3,23 @@ using System.Text.Json; using System.Text.Json.Serialization; using Bff.Api; using Microsoft.AspNetCore.Authentication.JwtBearer; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; var builder = WebApplication.CreateBuilder(args); +// OpenTelemetry tracing (S-16b, ADR-0023): auto-instrument incoming ASP.NET Core requests and +// outgoing HttpClient calls (BFF → Domain, BFF → projection-api), exported over OTLP to Tempo, so a +// portal request is one connected trace across the services. Service name + OTLP endpoint come from +// OTEL_* env (compose); the exporter no-ops when Tempo is unreachable. /health is filtered out. +builder.Services.AddOpenTelemetry() + .ConfigureResource(r => r.AddService( + builder.Configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName)) + .WithTracing(tracing => tracing + .AddAspNetCoreInstrumentation(o => o.Filter = ctx => ctx.Request.Path != "/health") + .AddHttpClientInstrumentation() + .AddOtlpExporter()); + var keycloakAuthority = builder.Configuration["Keycloak:Authority"] ?? throw new InvalidOperationException("Missing configuration 'Keycloak:Authority'"); // Behandelaars authenticate against a *different* Keycloak realm (medewerker) than citizens (digid), diff --git a/services/domain/Big.Api/Big.Api.csproj b/services/domain/Big.Api/Big.Api.csproj index 673337b..eed138a 100644 --- a/services/domain/Big.Api/Big.Api.csproj +++ b/services/domain/Big.Api/Big.Api.csproj @@ -6,6 +6,10 @@ + + + + diff --git a/services/domain/Big.Api/Program.cs b/services/domain/Big.Api/Program.cs index 2a2827e..e4167e0 100644 --- a/services/domain/Big.Api/Program.cs +++ b/services/domain/Big.Api/Program.cs @@ -1,10 +1,25 @@ using Big.Application; using Big.Domain; using Big.Infrastructure; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using Quartz; var builder = WebApplication.CreateBuilder(args); +// OpenTelemetry tracing (S-16b, ADR-0023): auto-instrument incoming ASP.NET Core requests and +// outgoing HttpClient calls, exported over OTLP to Tempo, so a request is one connected trace across +// the services. Service name + OTLP endpoint come from OTEL_* env (compose); the exporter no-ops +// harmlessly when Tempo is unreachable (e.g. a service run standalone). /health is filtered out so +// liveness polls don't flood the traces. +builder.Services.AddOpenTelemetry() + .ConfigureResource(r => r.AddService( + builder.Configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName)) + .WithTracing(tracing => tracing + .AddAspNetCoreInstrumentation(o => o.Filter = ctx => ctx.Request.Path != "/health") + .AddHttpClientInstrumentation() + .AddOtlpExporter()); + // Options bound from configuration (compose sets Flowable__* and Acl__* env vars). builder.Services.AddSingleton(sp => sp.GetRequiredService() .GetSection("Flowable").Get() diff --git a/services/event-subscriber/EventSubscriber.Api/EventSubscriber.Api.csproj b/services/event-subscriber/EventSubscriber.Api/EventSubscriber.Api.csproj index 168c420..925003a 100644 --- a/services/event-subscriber/EventSubscriber.Api/EventSubscriber.Api.csproj +++ b/services/event-subscriber/EventSubscriber.Api/EventSubscriber.Api.csproj @@ -5,6 +5,13 @@ + + + + + + + net10.0 enable diff --git a/services/event-subscriber/EventSubscriber.Api/Program.cs b/services/event-subscriber/EventSubscriber.Api/Program.cs index d71b7eb..f21c8ca 100644 --- a/services/event-subscriber/EventSubscriber.Api/Program.cs +++ b/services/event-subscriber/EventSubscriber.Api/Program.cs @@ -1,9 +1,22 @@ using System.Text.Json; using EventSubscriber.Application; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using Projection.ReadModel; var builder = WebApplication.CreateBuilder(args); +// OpenTelemetry tracing (S-16b, ADR-0023): auto-instrument the incoming NRC notification callback and +// the outgoing ACL enrichment call, exported over OTLP to Tempo. Service name + OTLP endpoint come +// from OTEL_* env (compose); the exporter no-ops when Tempo is unreachable. +builder.Services.AddOpenTelemetry() + .ConfigureResource(r => r.AddService( + builder.Configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName)) + .WithTracing(tracing => tracing + .AddAspNetCoreInstrumentation(o => o.Filter = ctx => ctx.Request.Path != "/health") + .AddHttpClientInstrumentation() + .AddOtlpExporter()); + var connectionString = builder.Configuration.GetConnectionString("Projection") ?? throw new InvalidOperationException("Missing connection string 'ConnectionStrings:Projection'"); // The exact Authorization header value Open Notificaties sends on each abonnement callback. diff --git a/services/projection-api/ProjectionApi.Api/Program.cs b/services/projection-api/ProjectionApi.Api/Program.cs index a2b88aa..8a14c28 100644 --- a/services/projection-api/ProjectionApi.Api/Program.cs +++ b/services/projection-api/ProjectionApi.Api/Program.cs @@ -1,8 +1,21 @@ using Microsoft.EntityFrameworkCore; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using Projection.ReadModel; var builder = WebApplication.CreateBuilder(args); +// OpenTelemetry tracing (S-16b, ADR-0023): auto-instrument incoming ASP.NET Core requests, exported +// over OTLP to Tempo, so a BFF → projection-api read is one connected trace. Service name + OTLP +// endpoint come from OTEL_* env (compose); the exporter no-ops when Tempo is unreachable. +builder.Services.AddOpenTelemetry() + .ConfigureResource(r => r.AddService( + builder.Configuration["OTEL_SERVICE_NAME"] ?? builder.Environment.ApplicationName)) + .WithTracing(tracing => tracing + .AddAspNetCoreInstrumentation(o => o.Filter = ctx => ctx.Request.Path != "/health") + .AddHttpClientInstrumentation() + .AddOtlpExporter()); + var connectionString = builder.Configuration.GetConnectionString("Projection") ?? throw new InvalidOperationException("Missing connection string 'ConnectionStrings:Projection'"); diff --git a/services/projection-api/ProjectionApi.Api/ProjectionApi.Api.csproj b/services/projection-api/ProjectionApi.Api/ProjectionApi.Api.csproj index b91c803..e9b681a 100644 --- a/services/projection-api/ProjectionApi.Api/ProjectionApi.Api.csproj +++ b/services/projection-api/ProjectionApi.Api/ProjectionApi.Api.csproj @@ -4,6 +4,13 @@ + + + + + + + net10.0 enable