chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Observability;
|
||||
|
||||
/// <summary>
|
||||
/// Domain-layer health aggregation for Phase 6.1 Stream C. Pure functions over the driver
|
||||
/// fleet — given each driver's <see cref="DriverState"/>, produce a <see cref="ReadinessVerdict"/>
|
||||
/// that maps to HTTP status codes at the endpoint layer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// State matrix per <c>docs/v2/implementation/phase-6-1-resilience-and-observability.md</c>
|
||||
/// §Stream C.1:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="DriverState.Unknown"/> / <see cref="DriverState.Initializing"/>
|
||||
/// → /readyz 503 (not yet ready).</item>
|
||||
/// <item><see cref="DriverState.Healthy"/> → /readyz 200.</item>
|
||||
/// <item><see cref="DriverState.Degraded"/> → /readyz 200 with flagged driver IDs.</item>
|
||||
/// <item><see cref="DriverState.Faulted"/> → /readyz 503.</item>
|
||||
/// </list>
|
||||
/// The overall verdict is computed across the fleet: any Faulted → Faulted; any
|
||||
/// Unknown/Initializing → NotReady; any Degraded → Degraded; else Healthy. An empty fleet
|
||||
/// is Healthy (nothing to degrade).
|
||||
/// </remarks>
|
||||
public static class DriverHealthReport
|
||||
{
|
||||
/// <summary>Compute the fleet-wide readiness verdict from per-driver states.</summary>
|
||||
public static ReadinessVerdict Aggregate(IReadOnlyList<DriverHealthSnapshot> drivers)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drivers);
|
||||
if (drivers.Count == 0) return ReadinessVerdict.Healthy;
|
||||
|
||||
var anyFaulted = drivers.Any(d => d.State == DriverState.Faulted);
|
||||
if (anyFaulted) return ReadinessVerdict.Faulted;
|
||||
|
||||
var anyInitializing = drivers.Any(d =>
|
||||
d.State == DriverState.Unknown || d.State == DriverState.Initializing);
|
||||
if (anyInitializing) return ReadinessVerdict.NotReady;
|
||||
|
||||
// Reconnecting = driver alive but not serving live data; report as Degraded so /readyz
|
||||
// stays 200 (the fleet can still serve cached / last-good data) while operators see the
|
||||
// affected driver in the body.
|
||||
var anyDegraded = drivers.Any(d =>
|
||||
d.State == DriverState.Degraded || d.State == DriverState.Reconnecting);
|
||||
if (anyDegraded) return ReadinessVerdict.Degraded;
|
||||
|
||||
return ReadinessVerdict.Healthy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map a <see cref="ReadinessVerdict"/> to the HTTP status the /readyz endpoint should
|
||||
/// return per the Stream C.1 state matrix.
|
||||
/// </summary>
|
||||
public static int HttpStatus(ReadinessVerdict verdict) => verdict switch
|
||||
{
|
||||
ReadinessVerdict.Healthy => 200,
|
||||
ReadinessVerdict.Degraded => 200,
|
||||
ReadinessVerdict.NotReady => 503,
|
||||
ReadinessVerdict.Faulted => 503,
|
||||
_ => 500,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Per-driver snapshot fed into <see cref="DriverHealthReport.Aggregate"/>.</summary>
|
||||
/// <param name="DriverInstanceId">Driver instance identifier (from <c>IDriver.DriverInstanceId</c>).</param>
|
||||
/// <param name="State">Current <see cref="DriverState"/> from <c>IDriver.GetHealth</c>.</param>
|
||||
/// <param name="DetailMessage">Optional driver-supplied detail (e.g. "primary PLC unreachable").</param>
|
||||
public sealed record DriverHealthSnapshot(
|
||||
string DriverInstanceId,
|
||||
DriverState State,
|
||||
string? DetailMessage = null);
|
||||
|
||||
/// <summary>Overall fleet readiness — derived from driver states by <see cref="DriverHealthReport.Aggregate"/>.</summary>
|
||||
public enum ReadinessVerdict
|
||||
{
|
||||
/// <summary>All drivers Healthy (or fleet is empty).</summary>
|
||||
Healthy,
|
||||
|
||||
/// <summary>At least one driver Degraded; none Faulted / NotReady.</summary>
|
||||
Degraded,
|
||||
|
||||
/// <summary>At least one driver Unknown / Initializing; none Faulted.</summary>
|
||||
NotReady,
|
||||
|
||||
/// <summary>At least one driver Faulted.</summary>
|
||||
Faulted,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Serilog.Context;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Core.Observability;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience wrapper around Serilog <see cref="LogContext"/> — attaches the set of
|
||||
/// structured properties a capability call should carry (DriverInstanceId, DriverType,
|
||||
/// CapabilityName, CorrelationId). Callers wrap their call-site body in a <c>using</c>
|
||||
/// block; inner <c>Log.Information</c> / <c>Log.Warning</c> calls emit the context
|
||||
/// automatically via the Serilog enricher chain.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Per <c>docs/v2/implementation/phase-6-1-resilience-and-observability.md</c> §Stream C.2.
|
||||
/// The correlation ID should be the OPC UA <c>RequestHeader.RequestHandle</c> when in-flight;
|
||||
/// otherwise a short random GUID. Callers supply whichever is available.
|
||||
/// </remarks>
|
||||
public static class LogContextEnricher
|
||||
{
|
||||
/// <summary>Attach the capability-call property set. Dispose the returned scope to pop.</summary>
|
||||
public static IDisposable Push(string driverInstanceId, string driverType, DriverCapability capability, string correlationId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(correlationId);
|
||||
|
||||
var a = LogContext.PushProperty("DriverInstanceId", driverInstanceId);
|
||||
var b = LogContext.PushProperty("DriverType", driverType);
|
||||
var c = LogContext.PushProperty("CapabilityName", capability.ToString());
|
||||
var d = LogContext.PushProperty("CorrelationId", correlationId);
|
||||
return new CompositeScope(a, b, c, d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a short correlation ID when no OPC UA RequestHandle is available.
|
||||
/// 12-hex-char slice of a GUID — long enough for log correlation, short enough to
|
||||
/// scan visually.
|
||||
/// </summary>
|
||||
public static string NewCorrelationId() => Guid.NewGuid().ToString("N")[..12];
|
||||
|
||||
private sealed class CompositeScope : IDisposable
|
||||
{
|
||||
private readonly IDisposable[] _inner;
|
||||
public CompositeScope(params IDisposable[] inner) => _inner = inner;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Reverse-order disposal matches Serilog's stack semantics.
|
||||
for (var i = _inner.Length - 1; i >= 0; i--)
|
||||
_inner[i].Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user