Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaServerOptions.cs
Joseph Doherty 9dd5e4e745 Phase 6.1 Stream C — health endpoints on :4841 + LogContextEnricher + Serilog JSON sink + CapabilityInvoker enrichment
Closes Stream C per docs/v2/implementation/phase-6-1-resilience-and-observability.md.

Core.Observability (new namespace):
- DriverHealthReport — pure-function aggregation over DriverHealthSnapshot list.
  Empty fleet = Healthy. Any Faulted = Faulted. Any Unknown/Initializing (no
  Faulted) = NotReady. Any Degraded or Reconnecting (no Faulted, no NotReady)
  = Degraded. Else Healthy. HttpStatus(verdict) maps to the Stream C.1 state
  matrix: Healthy/Degraded → 200, NotReady/Faulted → 503.
- LogContextEnricher — Serilog LogContext wrapper. Push(id, type, capability,
  correlationId) returns an IDisposable scope; inner log calls carry
  DriverInstanceId / DriverType / CapabilityName / CorrelationId structured
  properties automatically. NewCorrelationId = 12-hex-char GUID slice for
  cases where no OPC UA RequestHeader.RequestHandle is in flight.

CapabilityInvoker — now threads LogContextEnricher around every ExecuteAsync /
ExecuteWriteAsync call site. OtOpcUaServer passes driver.DriverType through
so logs correlate to the driver type too. Every capability call emits
structured fields per the Stream C.4 compliance check.

Server.Observability:
- HealthEndpointsHost — standalone HttpListener on http://localhost:4841/
  (loopback avoids Windows URL-ACL elevation; remote probing via reverse
  proxy or explicit netsh urlacl grant). Routes:
    /healthz → 200 when (configDbReachable OR usingStaleConfig); 503 otherwise.
      Body: status, uptimeSeconds, configDbReachable, usingStaleConfig.
    /readyz  → DriverHealthReport.Aggregate + HttpStatus mapping.
      Body: verdict, drivers[], degradedDrivers[], uptimeSeconds.
    anything else → 404.
  Disposal cooperative with the HttpListener shutdown.
- OpcUaApplicationHost starts the health host after the OPC UA server comes up
  and disposes it on shutdown. New OpcUaServerOptions knobs:
  HealthEndpointsEnabled (default true), HealthEndpointsPrefix (default
  http://localhost:4841/).

Program.cs:
- Serilog pipeline adds Enrich.FromLogContext + opt-in JSON file sink via
  `Serilog:WriteJson = true` appsetting. Uses Serilog.Formatting.Compact's
  CompactJsonFormatter (one JSON object per line — SIEMs like Splunk,
  Datadog, Graylog ingest without a regex parser).

Server.Tests:
- Existing 3 OpcUaApplicationHost integration tests now set
  HealthEndpointsEnabled=false to avoid port :4841 collisions under parallel
  execution.
- New HealthEndpointsHostTests (9): /healthz healthy empty fleet; stale-config
  returns 200 with flag; unreachable+no-cache returns 503; /readyz empty/
  Healthy/Faulted/Degraded/Initializing drivers return correct status and
  bodies; unknown path → 404. Uses ephemeral ports via Interlocked counter.

Core.Tests:
- DriverHealthReportTests (8): empty fleet, all-healthy, any-Faulted trumps,
  any-NotReady without Faulted, Degraded without Faulted/NotReady, HttpStatus
  per-verdict theory.
- LogContextEnricherTests (8): all 4 properties attach; scope disposes cleanly;
  NewCorrelationId shape; null/whitespace driverInstanceId throws.
- CapabilityInvokerEnrichmentTests (2): inner logs carry structured
  properties; no context leak outside the call site.

Full solution dotnet test: 1016 passing (baseline 906, +110 for Phase 6.1 so
far across Streams A+B+C). Pre-existing Client.CLI Subscribe flake unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 08:15:44 -04:00

89 lines
4.2 KiB
C#

using ZB.MOM.WW.OtOpcUa.Server.Security;
namespace ZB.MOM.WW.OtOpcUa.Server.OpcUa;
/// <summary>
/// OPC UA transport security profile selector. Controls which <c>ServerSecurityPolicy</c>
/// entries the endpoint advertises + which token types the <c>UserTokenPolicies</c> permits.
/// </summary>
public enum OpcUaSecurityProfile
{
/// <summary>Anonymous only on <c>SecurityPolicies.None</c> — dev-only, no signing or encryption.</summary>
None,
/// <summary>
/// <c>Basic256Sha256 SignAndEncrypt</c> with <c>UserName</c> and <c>Anonymous</c> token
/// policies. Clients must present a valid application certificate + user credentials.
/// </summary>
Basic256Sha256SignAndEncrypt,
}
/// <summary>
/// OPC UA server endpoint + application-identity configuration. Bound from the
/// <c>OpcUaServer</c> section of <c>appsettings.json</c>. PR 17 minimum-viable scope: no LDAP,
/// no security profiles beyond None — those wire in alongside a future deployment-policy PR
/// that reads from the central config DB instead of appsettings.
/// </summary>
public sealed class OpcUaServerOptions
{
public const string SectionName = "OpcUaServer";
/// <summary>
/// Fully-qualified endpoint URI clients connect to. Use <c>0.0.0.0</c> to bind all
/// interfaces; the stack rewrites to the machine's hostname for the returned endpoint
/// description at GetEndpoints time.
/// </summary>
public string EndpointUrl { get; init; } = "opc.tcp://0.0.0.0:4840/OtOpcUa";
/// <summary>Human-readable application name surfaced in the endpoint description.</summary>
public string ApplicationName { get; init; } = "OtOpcUa Server";
/// <summary>Stable application URI — must match the subjectAltName of the app cert.</summary>
public string ApplicationUri { get; init; } = "urn:OtOpcUa:Server";
/// <summary>
/// Directory where the OPC UA stack stores the application certificate + trusted /
/// rejected cert folders. Defaults to <c>%ProgramData%\OtOpcUa\pki</c>; the stack
/// creates the directory tree on first run and generates a self-signed cert.
/// </summary>
public string PkiStoreRoot { get; init; } =
System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
"OtOpcUa", "pki");
/// <summary>
/// When true, the stack auto-trusts client certs on first connect. Dev-default = true,
/// production deployments should flip this to false and manually trust clients via the
/// Admin UI.
/// </summary>
public bool AutoAcceptUntrustedClientCertificates { get; init; } = true;
/// <summary>
/// Whether to start the Phase 6.1 Stream C <c>/healthz</c> + <c>/readyz</c> HTTP listener.
/// Defaults to <c>true</c>; set false in embedded deployments that don't need HTTP
/// (e.g. tests that only exercise the OPC UA surface).
/// </summary>
public bool HealthEndpointsEnabled { get; init; } = true;
/// <summary>
/// URL prefix the health endpoints bind to. Default <c>http://localhost:4841/</c> — loopback
/// avoids Windows URL-ACL elevation. Production deployments that need remote probing should
/// either reverse-proxy or use <c>http://+:4841/</c> with netsh urlacl granted.
/// </summary>
public string HealthEndpointsPrefix { get; init; } = "http://localhost:4841/";
/// <summary>
/// Security profile advertised on the endpoint. Default <see cref="OpcUaSecurityProfile.None"/>
/// preserves the PR 17 endpoint shape; set to <see cref="OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt"/>
/// for production deployments with LDAP-backed UserName auth.
/// </summary>
public OpcUaSecurityProfile SecurityProfile { get; init; } = OpcUaSecurityProfile.None;
/// <summary>
/// LDAP binding for UserName token validation. Only consulted when the active
/// <see cref="SecurityProfile"/> advertises a UserName token policy. When
/// <c>LdapOptions.Enabled = false</c>, UserName token attempts are rejected.
/// </summary>
public LdapOptions Ldap { get; init; } = new();
}