feat(scadabridge): wire AddZbTelemetry + /metrics in both composition roots
This commit is contained in:
@@ -21,6 +21,7 @@ using ZB.MOM.WW.ScadaBridge.Security;
|
|||||||
using ZB.MOM.WW.ScadaBridge.SiteCallAudit;
|
using ZB.MOM.WW.ScadaBridge.SiteCallAudit;
|
||||||
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
|
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
|
||||||
using ZB.MOM.WW.ScadaBridge.Transport;
|
using ZB.MOM.WW.ScadaBridge.Transport;
|
||||||
|
using ZB.MOM.WW.Telemetry;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
// SCADABRIDGE_CONFIG determines which role-specific config to load (Central or Site)
|
// SCADABRIDGE_CONFIG determines which role-specific config to load (Central or Site)
|
||||||
@@ -248,6 +249,12 @@ try
|
|||||||
// All three are anonymous and use the canonical ZbHealthWriter JSON output.
|
// All three are anonymous and use the canonical ZbHealthWriter JSON output.
|
||||||
app.MapZbHealth();
|
app.MapZbHealth();
|
||||||
|
|
||||||
|
// Observability — mount the always-on Prometheus /metrics scrape endpoint.
|
||||||
|
// AddZbTelemetry (in SiteServiceRegistration.BindSharedOptions) wires the OTel
|
||||||
|
// Resource + standard instrumentation + Prometheus exporter; this exposes them.
|
||||||
|
// Requires endpoint routing (app.UseRouting() above).
|
||||||
|
app.MapZbMetrics();
|
||||||
|
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
app.MapCentralUI<ZB.MOM.WW.ScadaBridge.Host.Components.App>();
|
app.MapCentralUI<ZB.MOM.WW.ScadaBridge.Host.Components.App>();
|
||||||
app.MapInboundAPI();
|
app.MapInboundAPI();
|
||||||
@@ -304,6 +311,17 @@ try
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Endpoint routing middleware. The gRPC service mapping below and the
|
||||||
|
// /metrics scrape endpoint both run on endpoint routing, so UseRouting()
|
||||||
|
// must be present before the Map* calls on the site role.
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
// Observability — mount the always-on Prometheus /metrics scrape endpoint.
|
||||||
|
// AddZbTelemetry (in SiteServiceRegistration.Configure → BindSharedOptions)
|
||||||
|
// wires the OTel Resource + standard instrumentation + Prometheus exporter;
|
||||||
|
// this exposes them on the site node too.
|
||||||
|
app.MapZbMetrics();
|
||||||
|
|
||||||
// Map gRPC service — resolves the singleton SiteStreamGrpcServer from DI
|
// Map gRPC service — resolves the singleton SiteStreamGrpcServer from DI
|
||||||
app.MapGrpcService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
|
app.MapGrpcService<ZB.MOM.WW.ScadaBridge.Communication.Grpc.SiteStreamGrpcServer>();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using ZB.MOM.WW.ScadaBridge.NotificationService;
|
|||||||
using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
using ZB.MOM.WW.ScadaBridge.SiteEventLogging;
|
||||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime;
|
using ZB.MOM.WW.ScadaBridge.SiteRuntime;
|
||||||
using ZB.MOM.WW.ScadaBridge.StoreAndForward;
|
using ZB.MOM.WW.ScadaBridge.StoreAndForward;
|
||||||
|
using ZB.MOM.WW.Telemetry;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.ScadaBridge.Host;
|
namespace ZB.MOM.WW.ScadaBridge.Host;
|
||||||
|
|
||||||
@@ -114,5 +115,19 @@ public static class SiteServiceRegistration
|
|||||||
// writers so they can stamp the SourceNode column. Registered here in
|
// writers so they can stamp the SourceNode column. Registered here in
|
||||||
// shared bootstrap because every node (central + site) needs it.
|
// shared bootstrap because every node (central + site) needs it.
|
||||||
services.AddSingleton<INodeIdentityProvider, NodeIdentityProvider>();
|
services.AddSingleton<INodeIdentityProvider, NodeIdentityProvider>();
|
||||||
|
|
||||||
|
// Observability — shared ZB.MOM.WW.Telemetry. Registered in shared bootstrap so
|
||||||
|
// BOTH the central and site composition roots wire the OTel Resource (the
|
||||||
|
// service.name/site.id/node.role identity triple) + standard instrumentation +
|
||||||
|
// the always-on Prometheus exporter. Mount the /metrics scrape endpoint per role
|
||||||
|
// with app.MapZbMetrics(). The same `?? "central"` SiteId default Program.cs uses
|
||||||
|
// is applied here so the Resource attribute matches the log-enricher value.
|
||||||
|
// Meters left empty — application instruments are a deferred follow-on.
|
||||||
|
services.AddZbTelemetry(o =>
|
||||||
|
{
|
||||||
|
o.ServiceName = "scadabridge";
|
||||||
|
o.SiteId = config["ScadaBridge:Node:SiteId"] ?? "central";
|
||||||
|
o.NodeRole = config["ScadaBridge:Node:Role"];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Observability adoption: verifies the shared ZB.MOM.WW.Telemetry Prometheus
|
||||||
|
/// scrape endpoint (<c>/metrics</c>, mounted by <c>app.MapZbMetrics()</c>) is wired into the
|
||||||
|
/// Central composition root. <c>AddZbTelemetry</c> (registered in
|
||||||
|
/// <see cref="SiteServiceRegistration.BindSharedOptions"/>) always wires the Prometheus
|
||||||
|
/// exporter, so the endpoint returns the Prometheus exposition format regardless of DB /
|
||||||
|
/// cluster state. This is a pure route assertion — it requires no database, LDAP, or formed
|
||||||
|
/// Akka cluster. The Central-role factory bootstrap mirrors <see cref="HealthCheckTests"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class MetricsEndpointTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<IDisposable> _disposables = new();
|
||||||
|
|
||||||
|
public MetricsEndpointTests()
|
||||||
|
{
|
||||||
|
// Host-003: connection strings are externalised; supply them via env vars.
|
||||||
|
_disposables.Add(new CentralDbTestEnvironment());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var d in _disposables)
|
||||||
|
{
|
||||||
|
try { d.Dispose(); } catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebApplicationFactory<Program> CreateCentralFactory()
|
||||||
|
{
|
||||||
|
var factory = new WebApplicationFactory<Program>()
|
||||||
|
.WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.ConfigureAppConfiguration((context, config) =>
|
||||||
|
{
|
||||||
|
config.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["ScadaBridge:Node:NodeHostname"] = "localhost",
|
||||||
|
["ScadaBridge:Node:RemotingPort"] = "0",
|
||||||
|
["ScadaBridge:Cluster:SeedNodes:0"] = "akka.tcp://scadabridge@localhost:2551",
|
||||||
|
["ScadaBridge:Cluster:SeedNodes:1"] = "akka.tcp://scadabridge@localhost:2552",
|
||||||
|
["ScadaBridge:Database:SkipMigrations"] = "true",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
builder.UseSetting("ScadaBridge:Node:Role", "Central");
|
||||||
|
builder.UseSetting("ScadaBridge:Database:SkipMigrations", "true");
|
||||||
|
});
|
||||||
|
_disposables.Add(factory);
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Metrics_Endpoint_IsMapped()
|
||||||
|
{
|
||||||
|
var previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central");
|
||||||
|
var factory = CreateCentralFactory();
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
_disposables.Add(client);
|
||||||
|
|
||||||
|
var response = await client.GetAsync("/metrics");
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("# ", body); // Prometheus exposition (HELP/TYPE comments)
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", previousEnv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user