diff --git a/nuget.config b/nuget.config index 5223539..a2b8c8a 100644 --- a/nuget.config +++ b/nuget.config @@ -14,6 +14,8 @@ + + diff --git a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactorSeam.cs b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactorSeam.cs new file mode 100644 index 0000000..0beab89 --- /dev/null +++ b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactorSeam.cs @@ -0,0 +1,28 @@ +using ZB.MOM.WW.Telemetry.Serilog; + +namespace ZB.MOM.WW.MxGateway.Server.Diagnostics; + +/// +/// Adapts the static to the shared seam +/// so the telemetry RedactionEnricher masks API-key/credential material on every log event. +/// +public sealed class GatewayLogRedactorSeam : ILogRedactor +{ + private static readonly string[] IdentityKeys = ["ClientIdentity", "authorization", "Authorization"]; + + /// + /// Masks API-key/credential material in known identity-bearing log properties. + /// + /// The log event property dictionary to redact in place. + public void Redact(IDictionary properties) + { + ArgumentNullException.ThrowIfNull(properties); + foreach (var key in IdentityKeys) + { + if (properties.TryGetValue(key, out var value) && value is string s) + { + properties[key] = GatewayLogRedactor.RedactClientIdentity(s); + } + } + } +} diff --git a/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs b/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs index beba939..b1931a9 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs @@ -15,6 +15,8 @@ using ZB.MOM.WW.MxGateway.Server.Security.Authentication; using ZB.MOM.WW.MxGateway.Server.Security.Authorization; using ZB.MOM.WW.MxGateway.Server.Sessions; using ZB.MOM.WW.MxGateway.Server.Workers; +using ZB.MOM.WW.Telemetry; +using ZB.MOM.WW.Telemetry.Serilog; namespace ZB.MOM.WW.MxGateway.Server; @@ -61,6 +63,8 @@ public static class GatewayApplication ConfigureSelfSignedTls(builder); + builder.AddZbSerilog(o => o.ServiceName = "mxgateway"); + builder.Services.AddGatewayConfiguration(); builder.Services.AddSqliteAuthStore(); builder.Services.AddGatewayGrpcAuthorization(); @@ -70,6 +74,12 @@ public static class GatewayApplication failureStatus: null, tags: new[] { ZbHealthTags.Ready }); builder.Services.AddSingleton(); + builder.AddZbTelemetry(o => + { + o.ServiceName = "mxgateway"; + o.Meters = [GatewayMetrics.MeterName]; // "MxGateway.Server" — name unchanged + }); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -175,6 +185,7 @@ public static class GatewayApplication endpoints.MapStaticAssets(ResolveStaticAssetsManifestPath()); endpoints.MapZbHealth(); + endpoints.MapZbMetrics(); endpoints.MapGrpcService(); endpoints.MapGrpcService(); diff --git a/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj b/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj index c8c8527..855acc4 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj +++ b/src/ZB.MOM.WW.MxGateway.Server/ZB.MOM.WW.MxGateway.Server.csproj @@ -7,6 +7,11 @@ + + + + + diff --git a/src/ZB.MOM.WW.MxGateway.Server/appsettings.Development.json b/src/ZB.MOM.WW.MxGateway.Server/appsettings.Development.json index 0c208ae..674b2e0 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/appsettings.Development.json +++ b/src/ZB.MOM.WW.MxGateway.Server/appsettings.Development.json @@ -1,8 +1,13 @@ { - "Logging": { - "LogLevel": { + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], + "MinimumLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } + "Override": { "Microsoft.AspNetCore": "Warning" } + }, + "WriteTo": [ + { "Name": "Console" }, + { "Name": "File", "Args": { "path": "logs/mxgateway-.log", "rollingInterval": "Day" } } + ] } } diff --git a/src/ZB.MOM.WW.MxGateway.Server/appsettings.json b/src/ZB.MOM.WW.MxGateway.Server/appsettings.json index 63928c7..bc565ee 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/appsettings.json +++ b/src/ZB.MOM.WW.MxGateway.Server/appsettings.json @@ -1,9 +1,14 @@ { - "Logging": { - "LogLevel": { + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], + "MinimumLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } + "Override": { "Microsoft.AspNetCore": "Warning" } + }, + "WriteTo": [ + { "Name": "Console" }, + { "Name": "File", "Args": { "path": "logs/mxgateway-.log", "rollingInterval": "Day" } } + ] }, "AllowedHosts": "*", "MxGateway": { diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs new file mode 100644 index 0000000..494808e --- /dev/null +++ b/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using ZB.MOM.WW.MxGateway.Server.Diagnostics; +using Xunit; + +public class GatewayLogRedactorSeamTests +{ + [Fact] + public void Redact_MasksApiKeyInClientIdentity() + { + var redactor = new GatewayLogRedactorSeam(); + var props = new Dictionary { ["ClientIdentity"] = "Bearer mxgw_operator01_super-secret" }; + redactor.Redact(props); + Assert.Equal("Bearer mxgw_operator01_[redacted]", props["ClientIdentity"]); + } +} diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs index 9fb3f05..e5d5e91 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/GatewayApplicationTests.cs @@ -1,7 +1,10 @@ +using System.Net; +using System.Net.Http; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.MxGateway.Server; using ZB.MOM.WW.MxGateway.Server.Dashboard; @@ -29,6 +32,15 @@ public sealed class GatewayApplicationTests Assert.DoesNotContain("/health/live", paths); } + /// Verifies that Build registers Serilog as the host logging provider. + [Fact] + public void Build_UsesSerilogLoggerProvider() + { + using var app = GatewayApplication.Build([]); + var factory = app.Services.GetRequiredService(); + Assert.Equal("SerilogLoggerFactory", factory.GetType().Name); + } + /// Verifies that Build registers the gateway metrics service. [Fact] public async Task Build_RegistersGatewayMetrics() @@ -40,6 +52,28 @@ public sealed class GatewayApplicationTests Assert.NotNull(metrics); } + /// Verifies that Build mounts the Prometheus /metrics scrape endpoint. + [Fact] + public async Task Build_MapsMetricsEndpoint() + { + // Bind an ephemeral port (:0) — xUnit runs test collections in parallel, so any + // started-host test must avoid a fixed port to prevent a bind collision. + await using WebApplication app = GatewayApplication.Build(["--urls=http://127.0.0.1:0"]); + await app.StartAsync(); + try + { + using var client = new HttpClient { BaseAddress = new Uri(app.Urls.First()) }; + + using HttpResponseMessage response = await client.GetAsync("/metrics"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + finally + { + await app.StopAsync(); + } + } + /// Verifies that Build maps dashboard and authentication endpoints when the dashboard is enabled. [Fact] public async Task Build_WhenDashboardEnabled_MapsBlazorDashboardAndAuthEndpoints()