# Observability — current state: ScadaBridge Repo: `~/Desktop/ScadaBridge`. Stack: .NET 10, Akka.NET, Docker; solution `ZB.MOM.WW.ScadaBridge.slnx`. The telemetry posture is split across a dangling OTel package ref (metrics/traces) and a substantive Serilog setup (logs). All paths relative to repo root. Verified 2026-06-01. Structurally the cleanest logging enricher set in the family — `SiteId` / `NodeRole` / `NodeHostname` are already first-class Serilog enricher properties — but the weakest on metrics/tracing: zero instrumentation. The `OpenTelemetry.Api` package reference is a CVE-patch artefact, not instrumentation. ## 1. Metrics and traces (absent) ### `OpenTelemetry.Api` — CVE-patch ref, not instrumentation `src/ZB.MOM.WW.ScadaBridge.Host/ZB.MOM.WW.ScadaBridge.Host.csproj`: - `:31` — `` — a **direct version override** added to satisfy GHSA-g94r-2vxg-569j / GHSA-8785-wc3w-h8q6 (OpenTelemetry 1.9.0 CVEs introduced via `Akka.Hosting`'s pinned transitive dependency). There is **no `AddOpenTelemetry()` call** in the solution. No `Meter` is created. No `ActivitySource` is declared. No exporter is configured. The package reference solely overrides the transitive version — it has no runtime effect on observability. ### Instrument coverage Zero application instruments. There is no custom `Meter`, no counter, no histogram, no gauge, and no span in the ScadaBridge codebase. This is the largest gap in the family. ## 2. Logging (Serilog — strongest enricher set) ### Two-stage bootstrap `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs`: - `:27–54` — two-stage Serilog bootstrap: an initial logger is created for startup messages before the host is built; the full logger replaces it during `UseSerilog`. ### `LoggerConfigurationFactory.cs` `src/ZB.MOM.WW.ScadaBridge.Host/LoggerConfigurationFactory.cs`: Full factory method signature: `Build(IConfiguration config, string nodeRole, string siteId, string nodeHostname)`. - `:62` — reads `ScadaBridge:Logging:MinimumLevel` from configuration. - `:84` — `ReadFrom.Configuration(config)` pulls sink configuration from `appsettings.json`. - `:85` — explicit `MinimumLevel.Is(...)` override from the typed option. - `:86–88` — three structural enrichers: - `.Enrich.WithProperty("SiteId", siteId)` — site identifier (e.g. `"site-a"`). - `.Enrich.WithProperty("NodeHostname", nodeHostname)` — node hostname. - `.Enrich.WithProperty("NodeRole", nodeRole)` — Akka cluster role (e.g. `"central"`, `"site"`). These three properties are the cleanest and most complete set in the family. ScadaBridge's property names (`SiteId` / `NodeRole` / `NodeHostname`) are also the ones the shared `AddZbTelemetry` options object maps onto `site.id` / `node.role` / `host.name` OTel Resource attributes — no renaming needed on adoption. ### Sink configuration `appsettings.json:3–23` — Serilog sinks configured via `ReadFrom.Configuration`: - Console sink with output template that includes `[{NodeRole}/{NodeHostname}]`. - File sink (path in config; rolling interval). ### `LoggingOptions.cs` `src/ZB.MOM.WW.ScadaBridge.Host/LoggingOptions.cs`: - `MinimumLevel` — config-bound minimum level; default `Information`. ### Missing elements - **No custom enrichers** beyond the three structural properties. `LogContextEnricher` (OtOpcUa's driver-correlation enricher) has no equivalent; MxGateway's per-session correlation scope has no equivalent. Per-request/per-operation correlation is not present. - **No `trace_id` / `span_id` enricher.** As with the other two projects, log lines do not carry trace context. Because ScadaBridge has zero `ActivitySource` instrumentation, this is consistent — but it means no trace↔log correlation path exists even hypothetically. ## 3. Signal summary | Signal | Provider | Export | Resource / service.name | |---|---|---|---| | Metrics | ⛔ none | ⛔ none | ⛔ none | | Traces | ⛔ none | ⛔ none | ⛔ none | | Logs | Serilog | Console + file (`appsettings.json`) | ⛔ none (no `service.name` property) | | Trace↔log correlation | — | — | ⛔ absent (no ActivitySource; no enricher) | ## 4. Notable design choices - **`SiteId` / `NodeRole` / `NodeHostname` as first-class enrichers** — unlike OtOpcUa's driver- scoped `LogContextEnricher`, ScadaBridge's structural enrichers are attached at logger creation and appear on every log line from the process. This is the target pattern for the shared bootstrap. - **`nodeRole` + `siteId` passed into the factory** — ScadaBridge's `LoggerConfigurationFactory.Build` takes these as constructor arguments rather than reading them from a registered options object. The shared `AddZbSerilog` approach binds them from the same `ZbTelemetryOptions` used for the OTel Resource, unifying the source. - **Config-driven `MinimumLevel`** — `ScadaBridge:Logging:MinimumLevel` is a typed config path; `ReadFrom.Configuration` for sinks. The shared bootstrap's `AddZbSerilog` must support the same pattern. - **No custom enrichers** — ScadaBridge's logging is intentionally minimal on operation-scoped context. Correlation in the distributed model is provided by structured log fields from Akka actor context, not a log enricher pipeline. - **CVE-patch ref discipline** — the `OpenTelemetry.Api` pin is a responsible CVE response but leaves the telemetry story incomplete. On adoption, the CVE pin is superseded by the full OTel SDK pulled in by `AddZbTelemetry`; the explicit `` override can be removed. --- ## Adoption plan → `ZB.MOM.WW.Telemetry` **Replace CVE-patch ref with full OTel SDK via `AddZbTelemetry`:** - Remove the lone `OpenTelemetry.Api` override from `src/ZB.MOM.WW.ScadaBridge.Host/ZB.MOM.WW.ScadaBridge.Host.csproj:31`. - Add `builder.AddZbTelemetry(o => { o.ServiceName = "scadabridge"; o.SiteId = cfg.SiteId; o.NodeRole = cfg.NodeRole; o.Meters = ["ZB.MOM.WW.ScadaBridge"]; })`. The full OTel SDK supersedes the transitive version override; the CVE is resolved transitively via the SDK's current dependency. **Add first application instruments:** - Define a `ScadaBridgeTelemetry` class (mirror `OtOpcUaTelemetry`) with a `Meter` named `"ZB.MOM.WW.ScadaBridge"` and an initial set of instruments covering the most observable operations: site connection lifecycle, alarm received, data-change received, actor supervision events. Naming convention: `scadabridge..`. - Register the meter name in `AddZbTelemetry` options. Expose `/metrics` via `app.MapZbMetrics()`. ScadaBridge goes from zero instrumentation to a baseline exportable set. **Adopt `AddZbSerilog`:** - Replace the `LoggerConfigurationFactory.Build(config, nodeRole, siteId, nodeHostname)` call in `Program.cs:27–54` with `builder.AddZbSerilog(o => { o.ServiceName = "scadabridge"; o.SiteId = cfg.SiteId; o.NodeRole = cfg.NodeRole; })`. The three enrichers (`SiteId`, `NodeRole`, `NodeHostname`) are now provided by the shared `AddZbSerilog` path (`SiteId`/`NodeRole` from options; `NodeHostname` auto from `Environment.MachineName`); `LoggerConfigurationFactory` can be deleted. - `ReadFrom.Configuration` for sinks and `MinimumLevel.Is` override from config are preserved inside `AddZbSerilog` — behavior is unchanged. - The `TraceContextEnricher` is wired automatically by `AddZbSerilog`; once application instruments are added (above), `trace_id` / `span_id` will appear on log lines emitted during spans. **Keep bespoke:** - `LoggingOptions.cs` — the `MinimumLevel` typed option and its config path (`ScadaBridge:Logging:MinimumLevel`) remain; `AddZbSerilog` must accept the minimum-level override from configuration. The config path stays ScadaBridge's own. - Console output template including `[{NodeRole}/{NodeHostname}]` — driven by `appsettings.json`; no change. - Akka actor-context log fields — per-operation context emitted by Akka infrastructure; not an enricher concern. - `ZB.MOM.WW.ScadaBridge.Host.csproj` package set otherwise — no other changes to the project file. **Adoption is a follow-on task** (tracked in `GAPS.md`), not part of the `ZB.MOM.WW.Telemetry` library build. Adding instruments and adopting `AddZbSerilog`/`AddZbTelemetry` lands in the ScadaBridge repo as a separate commit once the nupkg is available.