diff --git a/Directory.Packages.props b/Directory.Packages.props
index a695772c..8b20addb 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -106,8 +106,8 @@
-
-
+
+
diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/ScadaBridgeLogging.cs b/src/ZB.MOM.WW.ScadaBridge.Host/ScadaBridgeLogging.cs
index 9ee7cf7b..9cee0aea 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Host/ScadaBridgeLogging.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Host/ScadaBridgeLogging.cs
@@ -18,8 +18,10 @@ namespace ZB.MOM.WW.ScadaBridge.Host;
/// Serilog section (the console and file sinks, output template, file path and rolling
/// interval all stay configuration-driven in appsettings.json), the SiteId /
/// NodeRole / NodeHostname identity enrichers, the shared TraceContextEnricher
-/// and the (inert here, no ILogRedactor is registered) redaction seam. No OTLP log sink is
-/// added: the telemetry options passed here never select the OTLP exporter, as before.
+/// and the (inert here, no ILogRedactor is registered) redaction seam. With
+/// ScadaBridge:Telemetry:Exporter=Otlp (Telemetry 0.3.0+, scadaproj#55) it also adds the OTLP
+/// log sink, to the same endpoint and with the same Resource as metrics and traces
+/// (); ScadaBridge:Telemetry:ExportLogs=false keeps logs local.
///
///
/// Minimum level (Host-011). ScadaBridge:Logging:MinimumLevel is the single source of
@@ -127,12 +129,10 @@ public static class ScadaBridgeLogging
ArgumentNullException.ThrowIfNull(warningWriter);
return builder.AddZbSerilog(
- telemetry =>
- {
- telemetry.ServiceName = ServiceName;
- telemetry.SiteId = siteId;
- telemetry.NodeRole = nodeRole;
- },
+ // The same options the AddZbTelemetry registration builds — including
+ // ScadaBridge:Telemetry:Exporter/OtlpEndpoint, so Exporter=Otlp sends logs to the
+ // collector too (scadaproj#55; this pre-host container has no AddZbTelemetry to follow).
+ telemetry => ScadaBridgeTelemetryConfig.Apply(telemetry, builder.Configuration, siteId, nodeRole),
serilog =>
{
serilog.MinimumLevelGovernor = configuration => GovernMinimumLevel(configuration, warningWriter);
diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/ScadaBridgeTelemetryConfig.cs b/src/ZB.MOM.WW.ScadaBridge.Host/ScadaBridgeTelemetryConfig.cs
new file mode 100644
index 00000000..1a96bdff
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Host/ScadaBridgeTelemetryConfig.cs
@@ -0,0 +1,42 @@
+using ZB.MOM.WW.Telemetry;
+
+namespace ZB.MOM.WW.ScadaBridge.Host;
+
+///
+/// The one place ScadaBridge fills , shared by the
+/// AddZbTelemetry registration (metrics + traces) and the pre-host AddZbSerilog
+/// logger (logs), so the three signals carry the same Resource and go to the same endpoint.
+///
+///
+/// The logger is built on its own pre-host container (see ), where
+/// no AddZbTelemetry registration exists for Telemetry 0.3.0's OTLP log sink to follow, so the
+/// exporter settings are passed to AddZbSerilog explicitly (scadaproj#55).
+///
+internal static class ScadaBridgeTelemetryConfig
+{
+ /// The operator section: Exporter, OtlpEndpoint, ServiceVersion,
+ /// ExportLogs, ResourceAttributes:*.
+ internal const string SectionName = "ScadaBridge:Telemetry";
+
+ ///
+ /// Applies the service name, the ScadaBridge:Telemetry keys and then the node identity.
+ /// / are applied LAST so the Resource always
+ /// matches the SiteId/NodeRole log enrichers (both come from ScadaBridge:Node);
+ /// a ScadaBridge:Telemetry:SiteId/:NodeRole key cannot make them diverge.
+ /// service.version defaults from the entry assembly (the git SHA).
+ ///
+ /// The options to fill.
+ /// The application configuration.
+ /// The node's site id (ScadaBridge:Node:SiteId, default central).
+ /// The node's role (ScadaBridge:Node:Role).
+ internal static void Apply(ZbTelemetryOptions options, IConfiguration configuration, string? siteId, string? nodeRole)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+ ArgumentNullException.ThrowIfNull(configuration);
+
+ options.ServiceName = ScadaBridgeLogging.ServiceName;
+ options.ApplyConfiguration(configuration.GetSection(SectionName));
+ options.SiteId = siteId;
+ options.NodeRole = nodeRole;
+ }
+}
diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs b/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs
index 2bbe0073..4259e0d4 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Host/SiteServiceRegistration.cs
@@ -384,15 +384,11 @@ public static class SiteServiceRegistration
// wired by follow-on tasks (the instruments are no-op until a listener attaches).
services.AddZbTelemetry(o =>
{
- o.ServiceName = "scadabridge";
- o.SiteId = config["ScadaBridge:Node:SiteId"] ?? "central";
- o.NodeRole = config["ScadaBridge:Node:Role"];
+ // Shared with the logger (ScadaBridgeLogging) so logs carry the same Resource and go to
+ // the same OTLP endpoint (scadaproj#55).
+ ScadaBridgeTelemetryConfig.Apply(
+ o, config, config["ScadaBridge:Node:SiteId"] ?? "central", config["ScadaBridge:Node:Role"]);
o.Meters = ObservedMeters;
- if (Enum.TryParse(config["ScadaBridge:Telemetry:Exporter"], ignoreCase: true, out var exporter))
- o.Exporter = exporter;
- var otlp = config["ScadaBridge:Telemetry:OtlpEndpoint"];
- if (!string.IsNullOrWhiteSpace(otlp))
- o.OtlpEndpoint = otlp;
});
}
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/ScadaBridgeTelemetryConfigTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/ScadaBridgeTelemetryConfigTests.cs
new file mode 100644
index 00000000..6648713b
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/ScadaBridgeTelemetryConfigTests.cs
@@ -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;
+
+///
+/// scadaproj#55: the logger and the AddZbTelemetry registration fill their options from one place,
+/// so ScadaBridge:Telemetry:Exporter=Otlp reaches the pre-host logger (OTLP log sink) as well as
+/// metrics and traces, and the node identity is unchanged.
+///
+public class ScadaBridgeTelemetryConfigTests
+{
+ private static IConfiguration Config(Dictionary values) =>
+ new ConfigurationBuilder().AddInMemoryCollection(values).Build();
+
+ /// Exporter and endpoint come from ScadaBridge:Telemetry; identity from the arguments.
+ [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
+ }
+
+ /// A Telemetry-section SiteId/NodeRole cannot make the Resource diverge from the log enrichers.
+ [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);
+ }
+
+ /// The OTLP logger path builds (validation passes) and still logs to the configured sinks.
+ [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");
+ }
+
+ /// A scheme-less endpoint now fails the logger at startup (Telemetry 0.3.0 validation).
+ [Fact]
+ public void CreateLogger_WithSchemelessEndpoint_Throws()
+ {
+ var config = Config(new()
+ {
+ ["ScadaBridge:Telemetry:Exporter"] = "Otlp",
+ ["ScadaBridge:Telemetry:OtlpEndpoint"] = "collector:4317",
+ });
+
+ Assert.Throws(() =>
+ ScadaBridgeLogging.CreateLogger(config, "Site", "site-a", "node-a", TextWriter.Null, additionalConfiguration: null));
+ }
+}