using System.Diagnostics; using System.Diagnostics.Metrics; namespace ZB.MOM.WW.OtOpcUa.Commons.Observability; /// /// Central + definitions for OtOpcUa. /// All Akka actors, the OPC UA publish path, and the deploy coordinator emit through these /// pre-created instruments so a single OpenTelemetry / Prometheus binding in Host /// catches everything. No exporter is required — instruments are no-op until a listener /// attaches, so tests and dev hosts pay nothing for instrumentation that nobody scrapes. /// /// Instrument names follow the OpenTelemetry semantic convention pattern /// otopcua.<subsystem>.<event>. Subsystem is one of: deploy, driver, /// virtualtag, scriptedalarm, opcua, redundancy. /// public static class OtOpcUaTelemetry { public const string MeterName = "ZB.MOM.WW.OtOpcUa"; public const string ActivitySourceName = "ZB.MOM.WW.OtOpcUa"; /// Singleton all counters/histograms hang off. public static readonly Meter Meter = new(MeterName); /// Singleton used to start spans wrapping deploy/apply/rebuild. public static readonly ActivitySource ActivitySource = new(ActivitySourceName); // ---------------- Deployment / driver-host coordination ---------------- /// Incremented every time DriverHostActor finishes applying a deployment (Ack or Reject). public static readonly Counter DeploymentApplied = Meter.CreateCounter("otopcua.deploy.applied", unit: "{deployment}", description: "Deployments applied by a driver-role node (outcome=ack|reject)."); /// Time from DriverHostActor receiving DispatchDeployment to emitting the ack/reject. public static readonly Histogram DeploymentApplyDurationSec = Meter.CreateHistogram("otopcua.deploy.apply.duration", unit: "s", description: "Driver-role apply latency from DispatchDeployment → Ack/Reject."); /// DriverInstanceActor spawn count (added=new instance; stop=disposed). public static readonly Counter DriverInstanceLifecycle = Meter.CreateCounter("otopcua.driver.lifecycle", unit: "{event}", description: "DriverInstanceActor lifecycle transitions (event=spawn|stop|fault)."); // ---------------- VirtualTag / ScriptedAlarm engines ---------------- public static readonly Counter VirtualTagEval = Meter.CreateCounter("otopcua.virtualtag.eval", unit: "{eval}", description: "Virtual-tag evaluations attempted (outcome=ok|fail|skip)."); public static readonly Counter ScriptedAlarmTransition = Meter.CreateCounter("otopcua.scriptedalarm.transition", unit: "{transition}", description: "Scripted-alarm state transitions (state=active|acknowledged|inactive)."); // ---------------- OPC UA address-space + redundancy ---------------- public static readonly Counter OpcUaSinkWrite = Meter.CreateCounter("otopcua.opcua.sink.write", unit: "{write}", description: "Writes that landed in IOpcUaAddressSpaceSink (kind=value|alarm|rebuild)."); /// Address-space apply/materialise passes that swallowed at least one sink failure — /// a failed rebuild (kind=rebuild) or per-node materialise failures (kind=nodes). A non-zero rate /// means the running server holds a stale or partially-materialised address space despite a /// reported-successful deploy (archreview 01/S-1). public static readonly Counter OpcUaApplyFailed = Meter.CreateCounter("otopcua.opcua.apply.failed", unit: "{apply}", description: "Apply/materialise passes with swallowed sink failures (kind=rebuild|nodes)."); public static readonly Counter ServiceLevelChange = Meter.CreateCounter("otopcua.redundancy.service_level_change", unit: "{change}", description: "OPC UA Server.ServiceLevel transitions emitted by the redundancy state."); /// Inbound operations denied by the Primary data-plane gate (archreview 03/S4) — /// site=write|ack|alarm-emit, reason=secondary|detached|role-unknown. public static readonly Counter PrimaryGateDenied = Meter.CreateCounter("otopcua.redundancy.primary_gate_denied", unit: "{denial}", description: "Operations denied by the Primary gate (site=write|ack|alarm-emit, reason=secondary|detached|role-unknown)."); // ---------------- Convenience helpers ---------------- /// /// Starts a deploy span tagged with the deployment id. Caller disposes to close. Returns /// null when no listener is attached so the call site stays cheap on undecorated builds. /// /// The deployment identifier to tag the span with. /// The started activity, or null when no listener is attached. public static Activity? StartDeployApplySpan(string deploymentId) { var activity = ActivitySource.StartActivity("otopcua.deploy.apply", ActivityKind.Internal); activity?.SetTag("otopcua.deployment_id", deploymentId); return activity; } /// Span wrapping a full OPC UA address-space rebuild (AddressSpace plan → apply). /// The started activity, or null when no listener is attached. public static Activity? StartAddressSpaceRebuildSpan() => ActivitySource.StartActivity("otopcua.opcua.address_space_rebuild", ActivityKind.Internal); }