using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; namespace ZB.MOM.WW.OtOpcUa.Cluster; /// /// Node-side selection of the transport a node's live-telemetry stream is carried over /// (per-cluster mesh Phase 5). The dark switch that lets a node stop publishing telemetry over /// the mesh-wide DistributedPubSub topic and instead serve it over a dedicated gRPC stream that /// does not require sharing a gossip ring with whoever is watching. /// public sealed class TelemetryOptions { /// Configuration section name. public const string SectionName = "Telemetry"; /// Publish telemetry over the mesh-wide DistributedPubSub topic — the default. public const string ModeDps = "Dps"; /// Serve telemetry over a dedicated gRPC stream. public const string ModeGrpc = "Grpc"; /// /// (default) or . Any other value fails host /// start. /// public string Mode { get; set; } = ModeDps; /// /// Dedicated gRPC listen port for the telemetry stream. 0 (the default) disables it — /// nothing is bound. Required on a driver-role node under . /// public int GrpcListenPort { get; set; } /// /// Shared bearer key the serve-side interceptor checks. Supply via the environment /// (Telemetry__ApiKey) — never commit it. Required under on any /// roled node — fail-closed against hosting an un-keyed telemetry surface. /// public string ApiKey { get; set; } = string.Empty; } /// /// Central-side selection of how central dials a node for live telemetry (per-cluster mesh /// Phase 5). Mirrors 's mode but carries the dial-side knobs /// (contact refresh cadence, per-call timeout) instead of a listen port. /// public sealed class TelemetryDialOptions { /// Configuration section name. public const string SectionName = "TelemetryDial"; /// Read telemetry from the mesh-wide DistributedPubSub topic — the default. public const string ModeDps = "Dps"; /// Dial nodes' dedicated gRPC telemetry streams. public const string ModeGrpc = "Grpc"; /// /// (default) or . Any other value fails host /// start. /// public string Mode { get; set; } = ModeDps; /// /// Shared bearer key; must equal a dialled node's . /// Supply via the environment (TelemetryDial__ApiKey) — never commit it. Required /// under . /// public string ApiKey { get; set; } = string.Empty; /// How often central refreshes its set of dialable node contacts, in seconds. public int ContactRefreshSeconds { get; set; } = 60; /// Per-call deadline for a gRPC telemetry dial, in seconds. public int CallTimeoutSeconds { get; set; } = 30; } /// /// Fails the host at startup on a shape that would leave a node /// unable to serve its live-telemetry stream. /// /// /// Like , every fault caught here otherwise surfaces as /// an absence — a telemetry consumer that simply never sees data from this node, with no /// stack trace and no failing node to point at; refusing to start is cheaper to diagnose. /// public sealed class TelemetryOptionsValidator : IValidateOptions { private readonly IConfiguration _configuration; /// /// DI-constructed by AddValidatedOptions (a plain AddSingleton), so a /// constructor dependency is safe here. — not /// IClusterRoleInfo — is the source of this node's roles: IClusterRoleInfo's /// implementation needs the ActorSystem, which does not exist yet at /// ValidateOnStart time. /// public TelemetryOptionsValidator(IConfiguration configuration) { _configuration = configuration; } /// public ValidateOptionsResult Validate(string? name, TelemetryOptions options) { ArgumentNullException.ThrowIfNull(options); var errors = new List(); var isDps = string.Equals(options.Mode, TelemetryOptions.ModeDps, StringComparison.OrdinalIgnoreCase); var isGrpc = string.Equals(options.Mode, TelemetryOptions.ModeGrpc, StringComparison.OrdinalIgnoreCase); if (!isDps && !isGrpc) { errors.Add( $"{TelemetryOptions.SectionName}:{nameof(TelemetryOptions.Mode)} is '{options.Mode}'. " + $"Expected '{TelemetryOptions.ModeDps}' or '{TelemetryOptions.ModeGrpc}'."); } var roles = _configuration.GetSection("Cluster:Roles").Get() ?? Array.Empty(); var isDriver = Array.IndexOf(roles, "driver") >= 0; if (isGrpc) { if (isDriver && options.GrpcListenPort <= 0) { errors.Add( $"Cluster:Roles has 'driver' and {TelemetryOptions.SectionName}:" + $"{nameof(TelemetryOptions.Mode)} is '{TelemetryOptions.ModeGrpc}', but " + $"{TelemetryOptions.SectionName}:{nameof(TelemetryOptions.GrpcListenPort)} is " + $"{options.GrpcListenPort}. A driver node in Grpc telemetry mode must set " + $"{TelemetryOptions.SectionName}:{nameof(TelemetryOptions.GrpcListenPort)}."); } if (roles.Length > 0 && string.IsNullOrEmpty(options.ApiKey)) { errors.Add( $"{TelemetryOptions.SectionName}:{nameof(TelemetryOptions.ApiKey)} is empty. Under " + $"{TelemetryOptions.ModeGrpc} the shared bearer key is the whole authentication " + "boundary to this node's telemetry surface; without it every dial is rejected. " + $"Supply it via the environment ({TelemetryOptions.SectionName}__ApiKey)."); } } return errors.Count == 0 ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(string.Join(" ", errors)); } } /// /// Fails the host at startup on a shape that would leave /// central unable to dial a node's live-telemetry stream. /// public sealed class TelemetryDialOptionsValidator : IValidateOptions { /// public ValidateOptionsResult Validate(string? name, TelemetryDialOptions options) { ArgumentNullException.ThrowIfNull(options); var errors = new List(); var isDps = string.Equals( options.Mode, TelemetryDialOptions.ModeDps, StringComparison.OrdinalIgnoreCase); var isGrpc = string.Equals( options.Mode, TelemetryDialOptions.ModeGrpc, StringComparison.OrdinalIgnoreCase); if (!isDps && !isGrpc) { errors.Add( $"{TelemetryDialOptions.SectionName}:{nameof(TelemetryDialOptions.Mode)} is " + $"'{options.Mode}'. Expected '{TelemetryDialOptions.ModeDps}' or " + $"'{TelemetryDialOptions.ModeGrpc}'."); } if (isGrpc && string.IsNullOrEmpty(options.ApiKey)) { errors.Add( $"{TelemetryDialOptions.SectionName}:{nameof(TelemetryDialOptions.ApiKey)} is empty. " + $"Under {TelemetryDialOptions.ModeGrpc} the shared bearer key is the whole " + "authentication boundary to a node's telemetry surface; without it every dial is " + $"rejected. Supply it via the environment ({TelemetryDialOptions.SectionName}__ApiKey)."); } return errors.Count == 0 ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(string.Join(" ", errors)); } }