diff --git a/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbLogEnricherNames.cs b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbLogEnricherNames.cs new file mode 100644 index 0000000..a1b602e --- /dev/null +++ b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbLogEnricherNames.cs @@ -0,0 +1,27 @@ +namespace ZB.MOM.WW.Telemetry.Serilog; + +/// +/// Canonical Serilog property name constants for the identity enrichers stamped by +/// . Use these constants — not literal strings — +/// when querying properties in sinks or tests. Each property mirrors a shared OTel Resource +/// attribute so logs and metrics/traces from the same node carry identical dimensions. +/// +public static class ZbLogEnricherNames +{ + /// + /// Serilog property: physical or logical site identifier. Matches OTel Resource site.id. + /// + public const string SiteId = "SiteId"; + + /// + /// Serilog property: node function (central, site, hub, standalone). + /// Matches OTel Resource node.role. + /// + public const string NodeRole = "NodeRole"; + + /// + /// Serilog property: machine name (). + /// Matches OTel Resource host.name. Populated automatically — not a caller-supplied option. + /// + public const string NodeHostname = "NodeHostname"; +} diff --git a/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogConfig.cs b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogConfig.cs new file mode 100644 index 0000000..416d27d --- /dev/null +++ b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogConfig.cs @@ -0,0 +1,69 @@ +using System; +using Serilog; +using Serilog.Configuration; +using ZB.MOM.WW.Telemetry; + +namespace ZB.MOM.WW.Telemetry.Serilog; + +/// +/// Reusable seam that applies the shared ZB.MOM.WW logging configuration (identity enrichers, +/// trace-context correlation, redaction, and OTel log export) to a +/// . Shared by +/// and unit tests so both exercise an identical enricher/sink set. +/// +public static class ZbSerilogConfig +{ + /// + /// Applies the shared identity enrichers — and + /// from , and + /// from + /// (auto, never a caller-supplied option) — to + /// . SiteId/NodeRole are stamped only when + /// the option is non-null/non-empty, mirroring the shared OTel Resource omission rules. + /// + /// The Serilog configuration to enrich. + /// The telemetry options describing the service identity. + /// The same for chaining. + public static LoggerConfiguration Apply( + LoggerConfiguration loggerConfiguration, + ZbTelemetryOptions options) => + Apply(loggerConfiguration, options, serviceProvider: null); + + /// + /// Overload of that additionally + /// wires the service-provider-dependent stages — the redaction enricher (which lazily resolves + /// a registered ILogRedactor). When is null, only the + /// provider-independent enrichers are applied. + /// + /// The Serilog configuration to enrich. + /// The telemetry options describing the service identity. + /// + /// Provider used to lazily resolve project-supplied seams (e.g. ILogRedactor); + /// may be null in tests or pipelines without DI. + /// + /// The same for chaining. + public static LoggerConfiguration Apply( + LoggerConfiguration loggerConfiguration, + ZbTelemetryOptions options, + IServiceProvider? serviceProvider) + { + ArgumentNullException.ThrowIfNull(loggerConfiguration); + ArgumentNullException.ThrowIfNull(options); + + LoggerEnrichmentConfiguration enrich = loggerConfiguration.Enrich; + + if (!string.IsNullOrEmpty(options.SiteId)) + { + enrich.WithProperty(ZbLogEnricherNames.SiteId, options.SiteId); + } + + if (!string.IsNullOrEmpty(options.NodeRole)) + { + enrich.WithProperty(ZbLogEnricherNames.NodeRole, options.NodeRole); + } + + enrich.WithProperty(ZbLogEnricherNames.NodeHostname, Environment.MachineName); + + return loggerConfiguration; + } +} diff --git a/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogExtensions.cs b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogExtensions.cs new file mode 100644 index 0000000..b22d107 --- /dev/null +++ b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry.Serilog/ZbSerilogExtensions.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; +using ZB.MOM.WW.Telemetry; + +namespace ZB.MOM.WW.Telemetry.Serilog; + +/// +/// Extension point for configuring the shared Serilog bootstrap on an +/// . Wires config-driven sinks +/// (ReadFrom.Configuration), an explicit minimum level (Serilog:MinimumLevel, +/// default ), and the shared enricher/redaction/OTel-export +/// set via . Does NOT configure OTel metrics/traces — call +/// AddZbTelemetry in the core package for that. +/// +public static class ZbSerilogExtensions +{ + /// + /// Registers Serilog on the host with the shared ZB.MOM.WW configuration: sinks read from + /// (ReadFrom.Configuration), an explicit minimum level + /// (Serilog:MinimumLevel, default ), and the + /// identity enrichers (SiteId/NodeRole from , + /// NodeHostname = ). The + /// delegate receives the same + /// used by AddZbTelemetry — typically share one options-population lambda across both. + /// + /// The host application builder. + /// Populates the . + public static IHostApplicationBuilder AddZbSerilog( + this IHostApplicationBuilder builder, + Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + var options = new ZbTelemetryOptions(); + configure(options); + + var minimumLevel = ReadMinimumLevel(builder.Configuration); + + builder.Services.AddSerilog((serviceProvider, loggerConfiguration) => + { + loggerConfiguration + .ReadFrom.Configuration(builder.Configuration) + .MinimumLevel.Is(minimumLevel); + + ZbSerilogConfig.Apply(loggerConfiguration, options, serviceProvider); + }); + + return builder; + } + + /// + /// Reads Serilog:MinimumLevel (or the nested Serilog:MinimumLevel:Default) from + /// configuration, falling back to for missing or + /// unparseable values. + /// + private static LogEventLevel ReadMinimumLevel(IConfiguration configuration) + { + var raw = configuration["Serilog:MinimumLevel"] + ?? configuration["Serilog:MinimumLevel:Default"]; + + return Enum.TryParse(raw, ignoreCase: true, out var parsed) + ? parsed + : LogEventLevel.Information; + } +} diff --git a/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Serilog.Tests/EnricherTests.cs b/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Serilog.Tests/EnricherTests.cs new file mode 100644 index 0000000..2fc138f --- /dev/null +++ b/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Serilog.Tests/EnricherTests.cs @@ -0,0 +1,46 @@ +using Serilog; +using Serilog.Events; +using Serilog.Sinks.InMemory; +using ZB.MOM.WW.Telemetry; +using ZB.MOM.WW.Telemetry.Serilog; + +namespace ZB.MOM.WW.Telemetry.Serilog.Tests; + +public sealed class EnricherTests +{ + private static string ScalarValue(LogEvent logEvent, string propertyName) + { + Assert.True( + logEvent.Properties.TryGetValue(propertyName, out var value), + $"expected property '{propertyName}' to be present"); + var scalar = Assert.IsType(value); + return scalar.Value?.ToString() ?? ""; + } + + [Fact] + public void Identity_enrichers_stamp_SiteId_NodeRole_and_NodeHostname() + { + var sink = new InMemorySink(); + var options = new ZbTelemetryOptions + { + ServiceName = "otopcua", + SiteId = "s1", + NodeRole = "Central", + }; + + var loggerConfig = new LoggerConfiguration(); + ZbSerilogConfig.Apply(loggerConfig, options); + using var logger = loggerConfig + .WriteTo.Sink(sink) + .CreateLogger(); + + logger.Information("hello"); + + var logEvent = Assert.Single(sink.LogEvents); + Assert.Equal("s1", ScalarValue(logEvent, ZbLogEnricherNames.SiteId)); + Assert.Equal("Central", ScalarValue(logEvent, ZbLogEnricherNames.NodeRole)); + Assert.Equal( + Environment.MachineName, + ScalarValue(logEvent, ZbLogEnricherNames.NodeHostname)); + } +}