Skip to content

feat(natsclient): the shared NATS client, with private inboxes, micro endpoints, requests and metrics (ENG-443) - #3

Merged
malpou merged 1 commit into
mainfrom
malthe/eng-443-nats-client
Oct 19, 2026
Merged

malpou merged 1 commit into
mainfrom
malthe/eng-443-nats-client

Conversation

@malpou

@malpou malpou commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

The NATS client half of ENG-443: one natsclient package every Go service on the platform uses, in place of each service hand-rolling nats.go connect options.

What is in it

  • Connect(ctx, Config, *slog.Logger, ...Option): the connect semantics of the platform's gatewayclient.Connect, preserved: the projected token is re-read on every (re)connect (nats.UserInfoHandler), reconnect forever with nats.go's paced sweep, RetryOnFailedConnect, IgnoreAuthErrorAbort, a 20s ping interval, every connection event logged (expiry Info, disconnect/reconnect Info, closed Error), connection named after the service. FromRuntime(rt.Config) maps NATS_URL, NATS_TOKEN_FILE, SERVICE_IDENTITY and OTEL_SERVICE_NAME. Connected() is a readiness check, Drain(ctx) the graceful stop.
  • Private reply inbox per identity: nats.CustomInboxPrefix("_INBOX_<ns>_<sa>"), dots escaped to _ as gatewayclient.SubjectID does. InboxPrefix(identity) is exported so the grant generator can derive the same name. WithSharedInbox() keeps nats.go's _INBOX prefix for an identity whose grant does not yet include the private one.
  • Request(ctx, subject, data, hdr): refuses a context without a deadline (ErrNoDeadline), injects W3C traceparent (propagation.TraceContext + HeaderCarrier), wraps a client span, and reports ErrNoResponders (wraps nats.ErrNoResponders) and ErrTimeout (wraps context.DeadlineExceeded) distinctly. An endpoint's micro error response comes back as *ServiceError{Code, Description}.
  • nats_requests_total{service,endpoint,outcome} (ok|no_responders|timeout|error) and nats_request_duration_seconds with the same attributes, from otel.Meter("github.com/MetsaApp/platform-kit/natsclient"), so they land on /metrics through the runtime's Prometheus exporter.
  • Service(name, version) over nats.go's micro package: AddEndpoint(endpoint, handler, ...EndpointOption) serves <name>.svc.<endpoint> (ADR 0006) in the queue group <name> by default (WithQueueGroup overrides), extracts the caller's trace into a server span, and turns a returned error into a micro error response (*ServiceError picks the code, anything else is 500). $SRV.PING/INFO/STATS discovery comes with micro.
  • EndpointSubject(service, endpoint) keeps the subject convention in one place.
  • README: a natsclient section and "Conventions for Rust and .NET" (projected token re-read on connect, connection name, _INBOX_<ns>_<sa> via ConnectOptions::custom_inbox_prefix / NatsOpts.InboxPrefix, ADR 0006 subjects, deadlines with distinct no-responders handling, W3C traceparent, the same metric names).

Acceptance criteria

Tests run on an embedded nats-server:

  • private inbox isolation (TestPrivateInboxIsolation): three users with publish/subscribe permissions shaped like the auth callout's grants; identity B's subscription to A's inbox prefix is refused (permissions violation logged), A's request still gets its reply, B's own request works;
  • queue group sharing (TestQueueGroupSharing): two Service instances with one name split a burst of 40 requests, an endpoint in another group sees all 40;
  • deadlines and no-responders (TestRequestTimeoutAndNoResponders, TestRequestNeedsDeadline): a slow endpoint yields ErrTimeout, an unserved subject ErrNoResponders, each errors.Is-distinguishable from the other and from the wrapped nats.go/context errors;
  • trace propagation (TestTracePropagation): the handler's context carries the caller's trace id and the exported spans form caller -> client -> server;
  • metrics (TestRequestMetrics): a manual reader asserts the counter and histogram with service, endpoint and outcome;
  • plus TestRequestServiceError, TestInboxPrefix, TestConnectNeedsIdentityForPrivateInbox.

go build ./... && go vet ./... && go test -race ./... and golangci-lint v2.11.3 are clean.

Deferred

  • Migrating the platform's gatewayclient callers onto this package is a later PR in the platform repo.
  • The private inbox grant (_INBOX_<ns>_<sa>.>) arrives with ENG-447; until then a service connects with WithSharedInbox(). Today's auth callout grants only _INBOX.>.
  • No server-side request metric: the ticket names only the client-side nats_requests_total; micro's $SRV.STATS has per-endpoint counts and latency in the meantime.

Dependencies

github.com/nats-io/nats.go v1.54.0; github.com/nats-io/nats-server/v2 v2.15.1 for tests only.

@malpou
malpou marked this pull request as ready for review October 19, 2026 11:06
@malpou
malpou merged commit 6a6d492 into main Oct 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant