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:
@@ -106,8 +106,8 @@
|
|||||||
<PackageVersion Include="ZB.MOM.WW.Health" Version="0.3.0" />
|
<PackageVersion Include="ZB.MOM.WW.Health" Version="0.3.0" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.Health.Akka" Version="0.3.0" />
|
<PackageVersion Include="ZB.MOM.WW.Health.Akka" Version="0.3.0" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.Health.EntityFrameworkCore" Version="0.3.0" />
|
<PackageVersion Include="ZB.MOM.WW.Health.EntityFrameworkCore" Version="0.3.0" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.Telemetry" Version="0.2.0" />
|
<PackageVersion Include="ZB.MOM.WW.Telemetry" Version="0.3.0" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.Telemetry.Serilog" Version="0.2.0" />
|
<PackageVersion Include="ZB.MOM.WW.Telemetry.Serilog" Version="0.3.0" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.MxGateway.Client" Version="0.2.1" />
|
<PackageVersion Include="ZB.MOM.WW.MxGateway.Client" Version="0.2.1" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.MxGateway.Contracts" Version="0.2.1" />
|
<PackageVersion Include="ZB.MOM.WW.MxGateway.Contracts" Version="0.2.1" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.Configuration" Version="0.1.0" />
|
<PackageVersion Include="ZB.MOM.WW.Configuration" Version="0.1.0" />
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ namespace ZB.MOM.WW.ScadaBridge.Host;
|
|||||||
/// <c>Serilog</c> section (the console and file sinks, output template, file path and rolling
|
/// <c>Serilog</c> section (the console and file sinks, output template, file path and rolling
|
||||||
/// interval all stay configuration-driven in <c>appsettings.json</c>), the <c>SiteId</c> /
|
/// interval all stay configuration-driven in <c>appsettings.json</c>), the <c>SiteId</c> /
|
||||||
/// <c>NodeRole</c> / <c>NodeHostname</c> identity enrichers, the shared <c>TraceContextEnricher</c>
|
/// <c>NodeRole</c> / <c>NodeHostname</c> identity enrichers, the shared <c>TraceContextEnricher</c>
|
||||||
/// and the (inert here, no <c>ILogRedactor</c> is registered) redaction seam. No OTLP log sink is
|
/// and the (inert here, no <c>ILogRedactor</c> is registered) redaction seam. With
|
||||||
/// added: the telemetry options passed here never select the OTLP exporter, as before.
|
/// <c>ScadaBridge:Telemetry:Exporter=Otlp</c> (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
|
||||||
|
/// (<see cref="ScadaBridgeTelemetryConfig"/>); <c>ScadaBridge:Telemetry:ExportLogs=false</c> keeps logs local.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>Minimum level (Host-011).</b> <c>ScadaBridge:Logging:MinimumLevel</c> is the single source of
|
/// <b>Minimum level (Host-011).</b> <c>ScadaBridge:Logging:MinimumLevel</c> is the single source of
|
||||||
@@ -127,12 +129,10 @@ public static class ScadaBridgeLogging
|
|||||||
ArgumentNullException.ThrowIfNull(warningWriter);
|
ArgumentNullException.ThrowIfNull(warningWriter);
|
||||||
|
|
||||||
return builder.AddZbSerilog(
|
return builder.AddZbSerilog(
|
||||||
telemetry =>
|
// The same options the AddZbTelemetry registration builds — including
|
||||||
{
|
// ScadaBridge:Telemetry:Exporter/OtlpEndpoint, so Exporter=Otlp sends logs to the
|
||||||
telemetry.ServiceName = ServiceName;
|
// collector too (scadaproj#55; this pre-host container has no AddZbTelemetry to follow).
|
||||||
telemetry.SiteId = siteId;
|
telemetry => ScadaBridgeTelemetryConfig.Apply(telemetry, builder.Configuration, siteId, nodeRole),
|
||||||
telemetry.NodeRole = nodeRole;
|
|
||||||
},
|
|
||||||
serilog =>
|
serilog =>
|
||||||
{
|
{
|
||||||
serilog.MinimumLevelGovernor = configuration => GovernMinimumLevel(configuration, warningWriter);
|
serilog.MinimumLevelGovernor = configuration => GovernMinimumLevel(configuration, warningWriter);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using ZB.MOM.WW.Telemetry;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The one place ScadaBridge fills <see cref="ZbTelemetryOptions"/>, shared by the
|
||||||
|
/// <c>AddZbTelemetry</c> registration (metrics + traces) and the pre-host <c>AddZbSerilog</c>
|
||||||
|
/// logger (logs), so the three signals carry the same Resource and go to the same endpoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The logger is built on its own pre-host container (see <see cref="ScadaBridgeLogging"/>), where
|
||||||
|
/// no <c>AddZbTelemetry</c> registration exists for Telemetry 0.3.0's OTLP log sink to follow, so the
|
||||||
|
/// exporter settings are passed to <c>AddZbSerilog</c> explicitly (scadaproj#55).
|
||||||
|
/// </remarks>
|
||||||
|
internal static class ScadaBridgeTelemetryConfig
|
||||||
|
{
|
||||||
|
/// <summary>The operator section: <c>Exporter</c>, <c>OtlpEndpoint</c>, <c>ServiceVersion</c>,
|
||||||
|
/// <c>ExportLogs</c>, <c>ResourceAttributes:*</c>.</summary>
|
||||||
|
internal const string SectionName = "ScadaBridge:Telemetry";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the service name, the <c>ScadaBridge:Telemetry</c> keys and then the node identity.
|
||||||
|
/// <paramref name="siteId"/> / <paramref name="nodeRole"/> are applied LAST so the Resource always
|
||||||
|
/// matches the <c>SiteId</c>/<c>NodeRole</c> log enrichers (both come from <c>ScadaBridge:Node</c>);
|
||||||
|
/// a <c>ScadaBridge:Telemetry:SiteId</c>/<c>:NodeRole</c> key cannot make them diverge.
|
||||||
|
/// <c>service.version</c> defaults from the entry assembly (the git SHA).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">The options to fill.</param>
|
||||||
|
/// <param name="configuration">The application configuration.</param>
|
||||||
|
/// <param name="siteId">The node's site id (<c>ScadaBridge:Node:SiteId</c>, default <c>central</c>).</param>
|
||||||
|
/// <param name="nodeRole">The node's role (<c>ScadaBridge:Node:Role</c>).</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -384,15 +384,11 @@ public static class SiteServiceRegistration
|
|||||||
// wired by follow-on tasks (the instruments are no-op until a listener attaches).
|
// wired by follow-on tasks (the instruments are no-op until a listener attaches).
|
||||||
services.AddZbTelemetry(o =>
|
services.AddZbTelemetry(o =>
|
||||||
{
|
{
|
||||||
o.ServiceName = "scadabridge";
|
// Shared with the logger (ScadaBridgeLogging) so logs carry the same Resource and go to
|
||||||
o.SiteId = config["ScadaBridge:Node:SiteId"] ?? "central";
|
// the same OTLP endpoint (scadaproj#55).
|
||||||
o.NodeRole = config["ScadaBridge:Node:Role"];
|
ScadaBridgeTelemetryConfig.Apply(
|
||||||
|
o, config, config["ScadaBridge:Node:SiteId"] ?? "central", config["ScadaBridge:Node:Role"]);
|
||||||
o.Meters = ObservedMeters;
|
o.Meters = ObservedMeters;
|
||||||
if (Enum.TryParse<ZbExporter>(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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user