feat(telemetry): Telemetry 0.3.0 - service.version and logs over OTLP (scadaproj#55)

Pins ZB.MOM.WW.Telemetry(.Serilog) 0.2.0 -> 0.3.0. ScadaBridgeTelemetryConfig is now
the one place both the AddZbTelemetry registration and the pre-host AddZbSerilog
logger fill their options, so ScadaBridge:Telemetry:Exporter=Otlp sends logs to the
same collector as metrics and traces, with the same Resource. The pre-host container
has no AddZbTelemetry registration for the library's log sink to follow, which is
why the exporter is passed to the logger explicitly. site.id / node.role are
unchanged (still ScadaBridge:Node:SiteId ?? central / ScadaBridge:Node:Role, applied
last so a Telemetry-section key cannot make them diverge from the log enrichers);
service.version now defaults to the entry assembly's informational version.

Host.Tests 699/699 incl. ScadaBridgeTelemetryConfigTests.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-12 21:13:25 -05:00
parent 9b0720af9c
commit a633005f4f
5 changed files with 137 additions and 18 deletions
@@ -0,0 +1,81 @@
using Microsoft.Extensions.Configuration;
using ZB.MOM.WW.ScadaBridge.Host;
using ZB.MOM.WW.Telemetry;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <summary>
/// scadaproj#55: the logger and the AddZbTelemetry registration fill their options from one place,
/// so <c>ScadaBridge:Telemetry:Exporter=Otlp</c> reaches the pre-host logger (OTLP log sink) as well as
/// metrics and traces, and the node identity is unchanged.
/// </summary>
public class ScadaBridgeTelemetryConfigTests
{
private static IConfiguration Config(Dictionary<string, string?> values) =>
new ConfigurationBuilder().AddInMemoryCollection(values).Build();
/// <summary>Exporter and endpoint come from ScadaBridge:Telemetry; identity from the arguments.</summary>
[Fact]
public void Apply_CarriesExporterAndIdentity()
{
var options = new ZbTelemetryOptions();
ScadaBridgeTelemetryConfig.Apply(options, Config(new()
{
["ScadaBridge:Telemetry:Exporter"] = "Otlp",
["ScadaBridge:Telemetry:OtlpEndpoint"] = "http://collector:4317",
}), siteId: "site-a", nodeRole: "Site");
Assert.Equal("scadabridge", options.ServiceName);
Assert.Equal(ZbExporter.Otlp, options.Exporter);
Assert.Equal("http://collector:4317", options.OtlpEndpoint);
Assert.Equal("site-a", options.SiteId);
Assert.Equal("Site", options.NodeRole);
Assert.Null(options.ServiceVersion); // library default: entry-assembly informational version
}
/// <summary>A Telemetry-section SiteId/NodeRole cannot make the Resource diverge from the log enrichers.</summary>
[Fact]
public void Apply_NodeIdentityWinsOverTelemetrySection()
{
var options = new ZbTelemetryOptions();
ScadaBridgeTelemetryConfig.Apply(options, Config(new()
{
["ScadaBridge:Telemetry:SiteId"] = "other",
["ScadaBridge:Telemetry:NodeRole"] = "other",
}), siteId: "central", nodeRole: "Central");
Assert.Equal("central", options.SiteId);
Assert.Equal("Central", options.NodeRole);
Assert.Equal(ZbExporter.Prometheus, options.Exporter);
}
/// <summary>The OTLP logger path builds (validation passes) and still logs to the configured sinks.</summary>
[Fact]
public void CreateLogger_WithOtlpExporter_Builds()
{
var config = Config(new()
{
["ScadaBridge:Telemetry:Exporter"] = "Otlp",
["ScadaBridge:Telemetry:OtlpEndpoint"] = "http://127.0.0.1:1",
});
using var logger = ScadaBridgeLogging.CreateLogger(config, "Site", "site-a", "node-a", TextWriter.Null, additionalConfiguration: null);
logger.Information("otlp wiring smoke test");
}
/// <summary>A scheme-less endpoint now fails the logger at startup (Telemetry 0.3.0 validation).</summary>
[Fact]
public void CreateLogger_WithSchemelessEndpoint_Throws()
{
var config = Config(new()
{
["ScadaBridge:Telemetry:Exporter"] = "Otlp",
["ScadaBridge:Telemetry:OtlpEndpoint"] = "collector:4317",
});
Assert.Throws<ArgumentException>(() =>
ScadaBridgeLogging.CreateLogger(config, "Site", "site-a", "node-a", TextWriter.Null, additionalConfiguration: null));
}
}