feat(telemetry): Telemetry 0.3.0 - service.version, site.id/node.role, logs over OTLP (scadaproj#55)
v2-ci / unit-tests (push) Has been cancelled
v2-ci / unit-tests (push) Has been cancelled
Pins ZB.MOM.WW.Telemetry(.Serilog) 0.1.0 -> 0.3.0. The OTel Resource now names the node: site.id = its application Cluster id (the cluster-<ClusterId> role suffix, omitted on a legacy node), node.role = its plain roles in canonical order (admin / driver / admin,driver), service.version = the entry assembly's informational version (the git SHA). Program passes its resolved EffectiveRoles. The whole OtOpcUa:Telemetry section goes through the shared ApplyConfiguration, so SiteId / NodeRole / ServiceVersion / ResourceAttributes:* are operator overrides, and Exporter=Otlp now also sends the Serilog logs to the same collector. ObservabilityResourceIdentityTests cover the mapping and the built container's Resource. Host.Tests 288/288, Host.IntegrationTests 325/327 (2 skipped). Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
@@ -228,8 +228,8 @@
|
|||||||
<PackageVersion Include="ZB.MOM.WW.LocalDb.Replication" Version="0.3.7" />
|
<PackageVersion Include="ZB.MOM.WW.LocalDb.Replication" Version="0.3.7" />
|
||||||
<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.1.0" />
|
<PackageVersion Include="ZB.MOM.WW.Telemetry" Version="0.3.0" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.Telemetry.Serilog" Version="0.1.0" />
|
<PackageVersion Include="ZB.MOM.WW.Telemetry.Serilog" Version="0.3.0" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.MxGateway.Client" Version="0.4.0" />
|
<PackageVersion Include="ZB.MOM.WW.MxGateway.Client" Version="0.4.0" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.MxGateway.Contracts" Version="0.4.0" />
|
<PackageVersion Include="ZB.MOM.WW.MxGateway.Contracts" Version="0.4.0" />
|
||||||
<PackageVersion Include="ZB.MOM.WW.Configuration" Version="0.1.0" />
|
<PackageVersion Include="ZB.MOM.WW.Configuration" Version="0.1.0" />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using ZB.MOM.WW.LocalDb.Replication;
|
using ZB.MOM.WW.LocalDb.Replication;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Cluster;
|
||||||
using ZB.MOM.WW.OtOpcUa.Commons.Observability;
|
using ZB.MOM.WW.OtOpcUa.Commons.Observability;
|
||||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
||||||
using ZB.MOM.WW.Telemetry;
|
using ZB.MOM.WW.Telemetry;
|
||||||
@@ -46,23 +47,67 @@ public static class ObservabilityExtensions
|
|||||||
/// Configuration read for the opt-in OTLP exporter. <c>OtOpcUa:Telemetry:Exporter</c>
|
/// Configuration read for the opt-in OTLP exporter. <c>OtOpcUa:Telemetry:Exporter</c>
|
||||||
/// (parsed case-insensitively to <see cref="ZbExporter"/>) switches to OTLP when set to
|
/// (parsed case-insensitively to <see cref="ZbExporter"/>) switches to OTLP when set to
|
||||||
/// <c>Otlp</c>; <c>OtOpcUa:Telemetry:OtlpEndpoint</c> sets the OTLP endpoint. With no
|
/// <c>Otlp</c>; <c>OtOpcUa:Telemetry:OtlpEndpoint</c> sets the OTLP endpoint. With no
|
||||||
/// config the exporter stays Prometheus (the default).
|
/// config the exporter stays Prometheus (the default). Since Telemetry 0.3.0 the same switch
|
||||||
|
/// also sends the host's Serilog logs to that endpoint (scadaproj#55).
|
||||||
|
/// </param>
|
||||||
|
/// <param name="roles">
|
||||||
|
/// The node's resolved roles (Program's single role authority). <see langword="null"/> resolves
|
||||||
|
/// them from <paramref name="configuration"/>. They feed the Resource identity — see
|
||||||
|
/// <see cref="ConfigureTelemetry"/>.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <returns>The service collection, for chaining.</returns>
|
/// <returns>The service collection, for chaining.</returns>
|
||||||
public static IServiceCollection AddOtOpcUaObservability(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddOtOpcUaObservability(
|
||||||
|
this IServiceCollection services,
|
||||||
|
IConfiguration configuration,
|
||||||
|
EffectiveRoles? roles = null)
|
||||||
{
|
{
|
||||||
return services.AddZbTelemetry(o =>
|
roles ??= EffectiveRoles.Resolve(configuration);
|
||||||
{
|
return services.AddZbTelemetry(o => ConfigureTelemetry(o, configuration, roles));
|
||||||
o.ServiceName = "otopcua";
|
}
|
||||||
// Strict allowlists — see ObservedMeters / ObservedActivitySources.
|
|
||||||
o.Meters = ObservedMeters;
|
/// <summary>
|
||||||
o.ActivitySources = ObservedActivitySources;
|
/// Populates the shared telemetry options. Resource identity (scadaproj#55):
|
||||||
if (Enum.TryParse<ZbExporter>(configuration["OtOpcUa:Telemetry:Exporter"], ignoreCase: true, out var exporter))
|
/// <c>site.id</c> = the node's application <c>Cluster</c> id (the suffix of its
|
||||||
o.Exporter = exporter;
|
/// <c>cluster-<ClusterId></c> role — one mesh per cluster, so it names the pair the node
|
||||||
var otlp = configuration["OtOpcUa:Telemetry:OtlpEndpoint"];
|
/// belongs to; omitted on a legacy node without one), <c>node.role</c> = its plain roles in
|
||||||
if (!string.IsNullOrWhiteSpace(otlp))
|
/// canonical order (<c>admin</c>, <c>driver</c>, <c>admin,driver</c>, …; omitted when none).
|
||||||
o.OtlpEndpoint = otlp;
|
/// <c>service.version</c> defaults from the entry assembly's informational version (the git SHA).
|
||||||
});
|
/// Operator keys under <c>OtOpcUa:Telemetry</c> are applied last (see
|
||||||
|
/// <see cref="ZbTelemetryConfigurationExtensions.ApplyConfiguration"/>).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="o">The options to populate.</param>
|
||||||
|
/// <param name="configuration">The host configuration (exporter keys).</param>
|
||||||
|
/// <param name="roles">The node's resolved roles.</param>
|
||||||
|
public static void ConfigureTelemetry(ZbTelemetryOptions o, IConfiguration configuration, EffectiveRoles roles)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(o);
|
||||||
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
ArgumentNullException.ThrowIfNull(roles);
|
||||||
|
|
||||||
|
o.ServiceName = "otopcua";
|
||||||
|
o.SiteId = roles.ClusterId;
|
||||||
|
o.NodeRole = NodeRoleOf(roles);
|
||||||
|
// Strict allowlists — see ObservedMeters / ObservedActivitySources.
|
||||||
|
o.Meters = ObservedMeters;
|
||||||
|
o.ActivitySources = ObservedActivitySources;
|
||||||
|
// OtOpcUa:Telemetry:{Exporter, OtlpEndpoint, ServiceVersion, SiteId, NodeRole, ExportLogs,
|
||||||
|
// ResourceAttributes:*} — an explicit SiteId/NodeRole there overrides the role-derived values.
|
||||||
|
o.ApplyConfiguration(configuration.GetSection("OtOpcUa:Telemetry"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The plain (non-cluster) roles, lower-case, sorted and comma-joined; null when none.</summary>
|
||||||
|
/// <param name="roles">The node's resolved roles.</param>
|
||||||
|
/// <returns>For example <c>admin,driver</c>, or <see langword="null"/>.</returns>
|
||||||
|
public static string? NodeRoleOf(EffectiveRoles roles)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(roles);
|
||||||
|
var plain = roles.Gossiped
|
||||||
|
.Where(r => !RoleParser.IsClusterRole(r))
|
||||||
|
.Select(r => r.ToLowerInvariant())
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.Order(StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
return plain.Length == 0 ? null : string.Join(",", plain);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -615,7 +615,7 @@ builder.Services.AddOtOpcUaSecrets(builder.Configuration);
|
|||||||
// probing it would throw when the readiness endpoint resolves the factory. hasDriver adds the
|
// probing it would throw when the readiness endpoint resolves the factory. hasDriver adds the
|
||||||
// opcua-endpoint probe (ready + active): a driver node whose SDK failed to start is Unhealthy, not green.
|
// opcua-endpoint probe (ready + active): a driver node whose SDK failed to start is Unhealthy, not green.
|
||||||
builder.Services.AddOtOpcUaHealth(hasAdmin, hasDriver);
|
builder.Services.AddOtOpcUaHealth(hasAdmin, hasDriver);
|
||||||
builder.Services.AddOtOpcUaObservability(builder.Configuration);
|
builder.Services.AddOtOpcUaObservability(builder.Configuration, effectiveRoles);
|
||||||
|
|
||||||
// gRPC server plumbing shared by three endpoints: the LocalDb passive sync endpoint (driver-role,
|
// gRPC server plumbing shared by three endpoints: the LocalDb passive sync endpoint (driver-role,
|
||||||
// mapped below on hasDriver && syncListenPort > 0), the ConfigServe artifact endpoint (admin-role,
|
// mapped below on hasDriver && syncListenPort > 0), the ConfigServe artifact endpoint (admin-role,
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using OpenTelemetry;
|
||||||
|
using OpenTelemetry.Trace;
|
||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Cluster;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Host.Observability;
|
||||||
|
using ZB.MOM.WW.Telemetry;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// scadaproj#55 — the OTel Resource carries <c>site.id</c> (the node's application Cluster id),
|
||||||
|
/// <c>node.role</c> (its plain roles) and <c>service.version</c> (entry-assembly default), so two
|
||||||
|
/// OtOpcUa nodes are distinguishable by more than <c>service.instance.id</c>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ObservabilityResourceIdentityTests
|
||||||
|
{
|
||||||
|
private static IConfiguration EmptyConfig() => new ConfigurationBuilder().Build();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FusedNode_ReportsClusterIdAndCanonicalRoles()
|
||||||
|
{
|
||||||
|
var roles = EffectiveRoles.Resolve(["driver", "cluster-SITE-A", "admin"], envRoles: null);
|
||||||
|
var o = new ZbTelemetryOptions();
|
||||||
|
|
||||||
|
ObservabilityExtensions.ConfigureTelemetry(o, EmptyConfig(), roles);
|
||||||
|
|
||||||
|
o.SiteId.ShouldBe("SITE-A");
|
||||||
|
o.NodeRole.ShouldBe("admin,driver");
|
||||||
|
o.ServiceVersion.ShouldBeNull(); // null = the library's entry-assembly default
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LegacyNodeWithoutClusterRole_OmitsSiteId()
|
||||||
|
{
|
||||||
|
var roles = EffectiveRoles.Resolve(["driver"], envRoles: null);
|
||||||
|
var o = new ZbTelemetryOptions();
|
||||||
|
|
||||||
|
ObservabilityExtensions.ConfigureTelemetry(o, EmptyConfig(), roles);
|
||||||
|
|
||||||
|
o.SiteId.ShouldBeNull();
|
||||||
|
o.NodeRole.ShouldBe("driver");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoRoles_OmitsNodeRole()
|
||||||
|
{
|
||||||
|
ObservabilityExtensions.NodeRoleOf(EffectiveRoles.Resolve(null, null)).ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuiltContainer_ResourceCarriesIdentityAndVersion()
|
||||||
|
{
|
||||||
|
var builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = [] });
|
||||||
|
builder.Services.AddOtOpcUaObservability(
|
||||||
|
builder.Configuration, EffectiveRoles.Resolve(["admin", "driver", "cluster-MAIN"], null));
|
||||||
|
using var app = builder.Build();
|
||||||
|
|
||||||
|
var attributes = app.Services.GetRequiredService<TracerProvider>().GetResource().Attributes
|
||||||
|
.ToDictionary(a => a.Key, a => a.Value);
|
||||||
|
|
||||||
|
attributes["service.name"].ShouldBe("otopcua");
|
||||||
|
attributes["site.id"].ShouldBe("MAIN");
|
||||||
|
attributes["node.role"].ShouldBe("admin,driver");
|
||||||
|
attributes["service.version"].ShouldBe(ZbResource.DefaultServiceVersion);
|
||||||
|
((string)attributes["service.version"]).ShouldNotBeNullOrWhiteSpace();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user