diff --git a/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry/ZbTelemetryExtensions.cs b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry/ZbTelemetryExtensions.cs index 54442c3..b44d5b5 100644 --- a/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry/ZbTelemetryExtensions.cs +++ b/ZB.MOM.WW.Telemetry/src/ZB.MOM.WW.Telemetry/ZbTelemetryExtensions.cs @@ -25,6 +25,7 @@ public static class ZbTelemetryExtensions Action configure) { ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); builder.Services.AddZbTelemetry(BuildOptions(configure)); return builder; } @@ -94,20 +95,23 @@ public static class ZbTelemetryExtensions ArgumentNullException.ThrowIfNull(configure); var options = new ZbTelemetryOptions(); configure(options); + if (string.IsNullOrWhiteSpace(options.ServiceName)) + { + throw new ArgumentException( + "ZbTelemetryOptions.ServiceName is required (e.g. \"otopcua\").", + nameof(configure)); + } return options; } private static void ApplyMetricsExporter(MeterProviderBuilder metrics, ZbTelemetryOptions options) { - switch (options.Exporter) + // Prometheus is always wired so that /metrics and MapZbMetrics() work regardless of + // the exporter setting. OTLP is an additive overlay when explicitly requested. + metrics.AddPrometheusExporter(); + if (options.Exporter == ZbExporter.Otlp) { - case ZbExporter.Otlp: - metrics.AddOtlpExporter(o => ConfigureOtlp(o, options)); - break; - case ZbExporter.Prometheus: - default: - metrics.AddPrometheusExporter(); - break; + metrics.AddOtlpExporter(o => ConfigureOtlp(o, options)); } } diff --git a/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Tests/AddZbTelemetryTests.cs b/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Tests/AddZbTelemetryTests.cs index 7aee2ce..024859f 100644 --- a/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Tests/AddZbTelemetryTests.cs +++ b/ZB.MOM.WW.Telemetry/tests/ZB.MOM.WW.Telemetry.Tests/AddZbTelemetryTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry; using OpenTelemetry.Metrics; @@ -10,6 +11,66 @@ namespace ZB.MOM.WW.Telemetry.Tests; public sealed class AddZbTelemetryTests { + // Fix #2: empty ServiceName must throw ArgumentException -------------------------- + + [Fact] + public void AddZbTelemetry_Throws_WhenServiceNameIsEmpty() + { + var builder = WebApplication.CreateBuilder(); + var ex = Assert.Throws(() => + builder.AddZbTelemetry(o => + { + o.ServiceName = ""; // explicitly empty + })); + Assert.Equal("configure", ex.ParamName); + } + + [Fact] + public void AddZbTelemetry_Throws_WhenServiceNameIsWhitespace() + { + var builder = WebApplication.CreateBuilder(); + var ex = Assert.Throws(() => + builder.AddZbTelemetry(o => + { + o.ServiceName = " "; + })); + Assert.Equal("configure", ex.ParamName); + } + + // Fix #1: Prometheus coexists with OTLP — /metrics must still serve under Otlp exporter + + [Fact] + public async Task AddZbTelemetry_OtlpExporter_StillServesPrometheusEndpoint() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseUrls("http://127.0.0.1:0"); + builder.AddZbTelemetry(o => + { + o.ServiceName = "telemetry-test"; + o.Exporter = ZbExporter.Otlp; + // OtlpEndpoint intentionally left null — exporter will be registered but won't + // connect anywhere; we are only verifying Prometheus remains present. + o.Meters = ["Test.OtlpCoexist.Meter"]; + }); + + await using var app = builder.Build(); + app.MapZbMetrics(); + + await app.StartAsync(); + + var address = app.Urls.First(); + using var client = new HttpClient { BaseAddress = new Uri(address) }; + + var response = await client.GetAsync("/metrics"); + + Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); + Assert.Equal("text/plain", response.Content.Headers.ContentType?.MediaType); + + await app.StopAsync(); + } + + // Existing test --------------------------------------------------------------- + [Fact] public void AddZbTelemetry_ExportsAppMeter_WithSharedResource() { diff --git a/components/observability/shared-contract/ZB.MOM.WW.Telemetry.md b/components/observability/shared-contract/ZB.MOM.WW.Telemetry.md index 152d39c..f0a9604 100644 --- a/components/observability/shared-contract/ZB.MOM.WW.Telemetry.md +++ b/components/observability/shared-contract/ZB.MOM.WW.Telemetry.md @@ -111,6 +111,13 @@ public static class ZbTelemetryExtensions public static IServiceCollection AddZbTelemetry( this IServiceCollection services, ZbTelemetryOptions options); + + /// IServiceCollection convenience overload that accepts a configure delegate. + /// Equivalent to calling BuildOptions(configure) then AddZbTelemetry(services, options). + /// Use when only an IServiceCollection is available but the lambda form is preferred. + public static IServiceCollection AddZbTelemetry( + this IServiceCollection services, + Action configure); } /// Builds the shared OTel ResourceBuilder from ZbTelemetryOptions.