feat(mesh-phase5): telemetry.proto contract + oneof + contract-lock test
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Commons.Protos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The single source of truth for which <see cref="TelemetryEvent.EventOneofCase"/> variants the
|
||||||
|
/// Phase 5 telemetry transport actually handles. Both the contract-lock test (which proves this
|
||||||
|
/// list stays in lock-step with the generated oneof) and the later oneof↔domain converter
|
||||||
|
/// reference this array, so adding a fifth channel to <c>telemetry.proto</c> forces a matching
|
||||||
|
/// entry here or the build/test goes red.
|
||||||
|
/// </summary>
|
||||||
|
public static class TelemetryProtoContract
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exactly the four telemetry oneof cases mirrored from the domain records:
|
||||||
|
/// alerts / script-logs / driver-health / driver-resilience-status.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly TelemetryEvent.EventOneofCase[] HandledCases =
|
||||||
|
[
|
||||||
|
TelemetryEvent.EventOneofCase.AlarmTransition,
|
||||||
|
TelemetryEvent.EventOneofCase.ScriptLog,
|
||||||
|
TelemetryEvent.EventOneofCase.DriverHealth,
|
||||||
|
TelemetryEvent.EventOneofCase.DriverResilience,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package telemetry.v1;
|
||||||
|
|
||||||
|
option csharp_namespace = "ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
// Per-cluster mesh Phase 5: the four live-telemetry channels that used to fan out over
|
||||||
|
// DistributedPubSub (alerts / script-logs / driver-health / driver-resilience-status) now travel over
|
||||||
|
// ONE gRPC server-streaming contract. Central dials each driver node and opens Subscribe; the node
|
||||||
|
// streams its own live telemetry as a sequence of TelemetryEvent envelopes.
|
||||||
|
//
|
||||||
|
// Additive-only field evolution: never renumber or reuse a tag; a pre-field peer must read a new
|
||||||
|
// field's proto3 default correctly (that is why nullable domain fields are modelled with `optional`
|
||||||
|
// for explicit presence). Locked by the contract test in the Commons test project.
|
||||||
|
service TelemetryStreamService {
|
||||||
|
rpc Subscribe(TelemetryStreamRequest) returns (stream TelemetryEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
message TelemetryStreamRequest {
|
||||||
|
string correlation_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TelemetryEvent {
|
||||||
|
string correlation_id = 1;
|
||||||
|
oneof event {
|
||||||
|
AlarmTransition alarm_transition = 2; // <- "alerts" / AlarmTransitionEvent
|
||||||
|
ScriptLog script_log = 3; // <- "script-logs" / ScriptLogEntry
|
||||||
|
DriverHealth driver_health = 4; // <- "driver-health" / DriverHealthChanged
|
||||||
|
DriverResilienceStatus driver_resilience = 5; // <- "driver-resilience-status" / DriverResilienceStatusChanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Alerts.AlarmTransitionEvent.
|
||||||
|
message AlarmTransition {
|
||||||
|
string alarm_id = 1;
|
||||||
|
string equipment_path = 2;
|
||||||
|
string alarm_name = 3;
|
||||||
|
string transition_kind = 4; // string in the record (Activated/Cleared/...); kept as string
|
||||||
|
int32 severity = 5;
|
||||||
|
string message = 6;
|
||||||
|
string user = 7;
|
||||||
|
google.protobuf.Timestamp timestamp_utc = 8;
|
||||||
|
string alarm_type_name = 9; // record default "AlarmCondition"
|
||||||
|
optional string comment = 10; // nullable in the record — presence distinguishes null from ""
|
||||||
|
optional bool historize_to_aveva = 11; // bool? in the record — three states (null / true / false)
|
||||||
|
repeated string referencing_equipment_paths = 12; // null in the record is treated as empty
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging.ScriptLogEntry.
|
||||||
|
message ScriptLog {
|
||||||
|
string script_id = 1;
|
||||||
|
string level = 2; // string in the record (Trace/Debug/Information/...); kept as string
|
||||||
|
string message = 3;
|
||||||
|
google.protobuf.Timestamp timestamp_utc = 4;
|
||||||
|
optional string virtual_tag_id = 5; // nullable in the record
|
||||||
|
optional string alarm_id = 6; // nullable in the record
|
||||||
|
optional string equipment_id = 7; // nullable in the record
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers.DriverHealthChanged.
|
||||||
|
message DriverHealth {
|
||||||
|
string cluster_id = 1;
|
||||||
|
string driver_instance_id = 2;
|
||||||
|
string state = 3; // DriverState-as-string in the record; kept as string
|
||||||
|
google.protobuf.Timestamp last_successful_read_utc = 4; // DateTime? — absent Timestamp encodes null
|
||||||
|
optional string last_error = 5; // nullable in the record
|
||||||
|
int32 error_count_5min = 6;
|
||||||
|
google.protobuf.Timestamp published_utc = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers.DriverResilienceStatusChanged.
|
||||||
|
message DriverResilienceStatus {
|
||||||
|
string driver_instance_id = 1;
|
||||||
|
string host_name = 2;
|
||||||
|
bool breaker_open = 3;
|
||||||
|
int32 consecutive_failures = 4;
|
||||||
|
int32 current_in_flight = 5;
|
||||||
|
google.protobuf.Timestamp last_breaker_open_utc = 6; // DateTime? — absent Timestamp encodes null
|
||||||
|
google.protobuf.Timestamp last_sampled_utc = 7;
|
||||||
|
google.protobuf.Timestamp published_utc = 8;
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Protobuf Include="Protos\deployment_artifact.proto" GrpcServices="Both"/>
|
<Protobuf Include="Protos\deployment_artifact.proto" GrpcServices="Both"/>
|
||||||
|
<Protobuf Include="Protos\telemetry.proto" GrpcServices="Both"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using Xunit;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Commons.Protos;
|
||||||
|
using ZB.MOM.WW.OtOpcUa.Commons.Protos.Telemetry.V1;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.OtOpcUa.Commons.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contract-lock for the Phase 5 telemetry oneof. If a fifth variant is added to
|
||||||
|
/// <c>telemetry.proto</c> without adding a matching entry to
|
||||||
|
/// <see cref="TelemetryProtoContract.HandledCases"/>, this test goes red — the single guard that
|
||||||
|
/// keeps the wire contract and the handled-case set from silently drifting apart.
|
||||||
|
/// </summary>
|
||||||
|
public class TelemetryProtoContractTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void EveryOneofVariant_IsAccountedFor()
|
||||||
|
{
|
||||||
|
var variants = System.Enum.GetValues<TelemetryEvent.EventOneofCase>()
|
||||||
|
.Where(c => c != TelemetryEvent.EventOneofCase.None).ToArray();
|
||||||
|
|
||||||
|
variants.ShouldBe(TelemetryProtoContract.HandledCases, ignoreOrder: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void The_service_base_and_client_types_generate()
|
||||||
|
{
|
||||||
|
// Referencing these types is the whole assertion — GrpcServices="Both" must emit both.
|
||||||
|
typeof(TelemetryStreamService.TelemetryStreamServiceBase).ShouldNotBeNull();
|
||||||
|
typeof(TelemetryStreamService.TelemetryStreamServiceClient).ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user