13 KiB
ZB.MOM.WW.HistorianGateway — Design
Date: 2026-06-23 Status: Design approved (brainstorming complete) — ready for implementation planning Author: brainstorming session (Joseph Doherty + Claude)
1. Summary
A new full-feature sidecar in the SCADA/OT sister-project family, modelled on
MxAccessGateway (mxaccessgw). It does two things:
- Read-only Galaxy metadata server — exposes the AVEVA System Platform
("Wonderware") Galaxy object hierarchy (areas / objects / templates /
instances / attributes), sourced from the Galaxy Repository SQL DB, the same
data
mxaccessgw'sgalaxy_repositoryfeature serves today. - Full read/write gRPC API to the AVEVA (Wonderware) Historian — reads (raw, aggregate with all 15 retrieval modes, at-time, blocks, events, browse, metadata, status) and writes (historical/backfill values, event send, tag-config create/delete/rename/extended-properties, plus resilience helpers).
It reuses the family's common shared packages and styles: ZB.MOM.WW.Auth,
ZB.MOM.WW.Theme, ZB.MOM.WW.Telemetry(+.Serilog), ZB.MOM.WW.Health,
ZB.MOM.WW.Configuration, ZB.MOM.WW.Audit.
Key reframing discovered during brainstorming
- The historian write surface lives in the
histsdkrepo (gitea.dohertylan.com/dohertj2/histsdk, namespaceAVEVA.Historian.Client), which is far ahead of the stalescadaproj/ZB.MOM.WW.SPHistorianClientport (2026-06-19 snapshot, reads + tag create/delete only, value-writes marked "blocked").histsdkhas since added a live-validated gRPC write surface:AddHistoricalValuesAsync(historical/backfill, gRPC-only),SendEventAsync(events, both transports),EnsureTags/DeleteTags/RenameTags/AddTagExtendedProperties(config writes, gRPC), plus higher-levelHistorianStoreForwardWriter(durable outbox) and a redundant-write cluster. - Hard server-side limit no client can lift:
AddS2streaming live process-sample writes are GATED — the historian runtime cache only ingests from configured IOServer / Application Server pipelines. "Live current value" writes are therefore done via the SQL path (aaAnalogTagInsert→INSERT INTO History), not gRPC. - No COM anywhere. The historian SDK is pure-managed and Galaxy browse is plain
SQL, so — unlike
mxaccessgw— this sidecar needs no x86 worker and no two-process split. It is a single .NET 10 x64 process.
2. Decisions (locked during brainstorming)
| Decision | Choice |
|---|---|
| Purpose | General-purpose gateway — reusable modern façade over Historian + Galaxy metadata for any gRPC client (the way mxaccessgw is the façade for MXAccess). |
| Historian client code | Vendor histsdk (AVEVA.Historian.Client) into the sidecar repo as self-contained vendored source; namespace kept as-is to ease future re-sync. |
| Galaxy browse code | New shared lib ZB.MOM.WW.GalaxyRepository in scadaproj, extracted from mxaccessgw's galaxy_repository browse; consumed by both mxaccessgw and this sidecar. |
| Dashboard | Full Blazor dashboard on ZB.MOM.WW.Theme (login + Galaxy browser + Historian console + API-key admin + status/health). |
| Historian write scope | All: historical/backfill writes, event send, tag-config writes, and resilience extras (store-and-forward outbox, redundant write fan-out, SQL live-value path). |
| Connection model | Approach A — stateless gateway over pooled service-identity connections. Clients authenticate to the gateway (ZB API key); the gateway owns historian credentials and reuses pooled, pre-authenticated connections. |
| Repo location | New standalone sibling repo ~/Desktop/HistorianGateway, gitea remote historiangw. |
| Name / namespace | ZB.MOM.WW.HistorianGateway. |
3. Architecture & solution structure
Single .NET 10 x64 ASP.NET Core process.
~/Desktop/HistorianGateway/
src/
ZB.MOM.WW.HistorianGateway.Server/ ASP.NET Core host: gRPC services + Blazor dashboard + /healthz + /metrics
ZB.MOM.WW.HistorianGateway.Contracts/ the gateway's own .proto + generated types (for client codegen distribution)
vendor/AVEVA.Historian.Client/ VENDORED from histsdk; ArchestrA.Grpc.Contract.* protos + reads/writes/store-forward/redundancy
tests/
ZB.MOM.WW.HistorianGateway.Tests/ unit + env-gated live integration + bUnit dashboard
docs/plans/, CLAUDE.md, README.md
ZB.MOM.WW.HistorianGateway.slnx
Cross-repo pieces:
scadaproj/ZB.MOM.WW.GalaxyRepository(new shared lib, plain files — NOT a nested git repo): carries the canonicalgalaxy_repository.proto(adopted frommxaccessgw's existing contract so OtOpcUa's wire shape is not broken), the SQL browse provider (connect to Galaxy Repository SQL → hierarchy model), and a reusable gRPC service implementation both hosts canMapGrpcService<>().mxaccessgwadopting it is a tracked follow-on (same "built → adopted" pattern as the other normalized components); this sidecar consumes it from the start.- Shared ZB packages consumed:
ZB.MOM.WW.Auth(Abstractions+Ldap+ApiKeys+AspNetCore),ZB.MOM.WW.Theme,ZB.MOM.WW.Telemetry(+.Serilog),ZB.MOM.WW.Health,ZB.MOM.WW.Configuration,ZB.MOM.WW.Audit.
4. gRPC API surface
Gateway's own curated contract (ZB.MOM.WW.HistorianGateway.Grpc.V1), grouped by
concern — not a 1:1 SDK dump. The vendored ArchestrA.Grpc.Contract.* protos stay
internal; clients see only the gateway contract.
| Service | RPCs | Notes |
|---|---|---|
HistorianRead |
ReadRaw, ReadAggregate, ReadBlocks, ReadEvents (server-streaming); ReadAtTime |
ReadAggregate exposes all 15 retrieval modes |
HistorianWrite |
AddHistoricalValues, SendEvent, WriteLiveValues |
WriteLiveValues = SQL path (gRPC streaming is gated) |
HistorianTags |
BrowseTagNames (streaming), GetTagMetadata, EnsureTags, DeleteTags, RenameTags, AddTagExtendedProperties |
|
HistorianStatus |
Probe, GetConnectionStatus, GetStoreForwardStatus, GetSystemParameter |
|
GalaxyRepository |
Browse areas / objects / templates / instances / attributes (read-only) | canonical proto from the shared lib |
Authorization is via API-key scopes at the gateway (Approach A trust
boundary): historian:read, historian:write, historian:tags:write,
galaxy:read.
5. Connection & data flow
gRPC client ──(ZB API key)──► HistorianGateway ──┬─ pooled, pre-authed gRPC conn ──► AVEVA Historian (RemoteGrpc 2023R2)
├─ store-forward outbox (SQLite) ─ replays writes on reconnect
├─ redundant-write fan-out ──────► historian members (All/Any ack)
├─ SqlConnection ──► Runtime DB (live-value writes via aaAnalogTagInsert/History)
└─ ZB.MOM.WW.GalaxyRepository ──► Galaxy Repository SQL (read-only browse)
- Pooled connections: the expensive auth handshake (
ValidateClientCredential/ ECDHExchangeKey) runs once per connection on open, then is reused across requests; connections are health-checked with auto-reconnect. Write operations use the write-enabled session mode (0x401). - Store-forward: writes flow through the SDK's
HistorianStoreForwardWriter— on an unreachable historian, enqueue to durable SQLite; a background drain replays on reconnect. - Redundancy:
HistorianRedundantWriteResultfan-out to configured members under an All/Any ack policy; per-member result surfaced to the caller. - SQL live-write and Galaxy browse are independent SQL paths, each with its own validated connection config.
Configuration (all ZB.MOM.WW.Configuration-validated, aggregated by
ConfigPreflight at startup): historian (host, gRPC port 32565, transport=RemoteGrpc,
TLS, service identity/credentials), redundant members, store-forward path, Galaxy
Repository SQL connection string, Runtime DB connection string (SQL live-write),
Auth (LDAP + API-key pepper). Secrets live in the operator environment, never in repo.
6. Cross-cutting infrastructure + dashboard
- Auth (
ZB.MOM.WW.Auth): gRPC clients use peppered-HMAC API keys (keyId/Bearer), validated by a gRPC interceptor enforcing per-service scopes. Dashboard uses LDAP login (.Ldap+.AspNetCore), cookie auth,IGroupRoleMapper<TRole>, canonicalZbClaimTypes/ZbCookieDefaults, canonical-six roles, dev against the shared GLAuth (10.100.0.35:3893,dc=zb,dc=local).DisableLogindev/deploy switch. - Telemetry (
.Telemetry+.Serilog):AddZbTelemetry(Resourceservice.name=historian-gateway+ standard instrumentation + always-on Prometheus/metrics, OTLP opt-in) +AddZbSerilog. App Meters: read/write counts + latency, store-forward queue depth, pool connection state, redundancy ack outcomes. - Health (
.Health): three-tier ready/active/healthz + canonical JSON writer. Probes: historian gRPC (GrpcDependencyHealthCheck), Galaxy Repository SQL + Runtime DB (DatabaseHealthCheck), store-forward drain status. - Configuration (
.Configuration):OptionsValidatorBase/ValidationBuilder/AddValidatedOptions/ConfigPreflight(§5). - Audit (
.Audit, DEEP-adopt): canonicalAuditEvent+ SQLiteIAuditWriter(MxGateway-style). Audited: tag-config writes, historical/event writes, API-key admin, login/logout.Actorwired from the Auth principal viaIAuditActorAccessor. - Dashboard (Blazor,
.Theme): Technical-Light side-rail shell +LoginCard/login. Pages: Status (pool / store-forward / redundancy / version), Galaxy browser (read-only hierarchy tree), Historian console (query with raw/aggregate + mode picker + time range; role-gated write test for value insert / event send), API-key admin (list/create/revoke keys + scopes), Health.
7. Error handling
- gRPC status mapping:
ProtocolEvidenceMissingException(unsupported op/type — e.g. non-analog tag, non-string event property) →Unimplemented/FailedPreconditionwith a clear "not in reverse-engineered surface" message; auth →Unauthenticated/PermissionDenied; historian down →Unavailable; bad range / unknown tag →InvalidArgument/NotFound. - Gated ops: live streaming-sample writes (
AddS2) are not exposed (no RPC); live-value writes route through SQLWriteLiveValues. - Write resilience: with store-forward enabled, an unreachable historian returns
accepted + queued (not an error); otherwise
Unavailable. Redundancy surfaces a per-member result; All-policy fails if any member fails, Any-policy succeeds on ≥1 ack. - Pool: transient failures → reconnect + bounded retry; auth-handshake failure → fail fast with diagnostic. No secrets/real hostnames in errors or logs (histsdk safety rule).
8. Testing
- Unit: gRPC services against a faked historian-client seam + faked Galaxy provider; scope/auth interceptor; config validators; SDK-model ↔ proto mapping.
- Golden/protocol: carry over
histsdk's golden byte tests for the vendored client (historical "ON" buffer, event "OS" buffer, registration buffers) so the vendored copy stays faithful. - Integration (env-gated, live, CI/macOS-safe): real 2023 R2 historian + Galaxy
Repository SQL — read/write round-trips and browse via the self-cleaning
sandbox-tag lifecycle (
HISTORIAN_GRPC_WRITE_SANDBOX_TAG); skipped when env vars absent. - Dashboard: bUnit component tests. Smoke:
/healthz,/metrics, gRPCProbe.
9. Out of scope / non-goals
AddS2live streaming process-sample writes (GATED server-side; SQL path covers live values instead).- Non-analog tag creation, revision/edit writes, bit-faithful store-forward framing
(per
histsdkcapability matrix —BOUNDED/HARD/GATEDitems not selected). - A two-process / x86 worker split (not needed — no COM).
- Re-syncing or replacing the existing stale
scadaproj/ZB.MOM.WW.SPHistorianClientport (we vendorhistsdkinstead; the stale port is left as-is).
10. Implementation components (high level)
ZB.MOM.WW.GalaxyRepositoryshared lib (scadaproj) — extract frommxaccessgw, canonical proto + SQL browse provider + reusable gRPC service.- Vendor
histsdkAVEVA.Historian.Clientinto the new repo + carry its golden tests. - Repo scaffold + host + shared-package wiring (Auth/Telemetry/Health/
Configuration/Audit) + validated options +
ConfigPreflight. - gRPC contract + services (Read / Write / Tags / Status / GalaxyRepository).
- Connection layer — pooled pre-authed connections, store-forward, redundancy, SQL live-write path.
- Auth — API-key scope interceptor + LDAP dashboard auth + Audit wiring.
- Blazor dashboard pages (Theme).
- Telemetry + Health probes/meters.
- Tests — unit / golden / env-gated integration / bUnit.
- Docs + repo/gitea setup —
CLAUDE.md,README.md, gitea remote.
mxaccessgwadoption ofZB.MOM.WW.GalaxyRepositoryis a separate tracked follow-on, not part of the initial sidecar delivery.