Skip to content

fix(go/observability): control-plane telemetry pipeline fixes#271

Merged
shuaijinchao merged 10 commits into
masterfrom
fix/obs-hot-reload
Jul 17, 2026
Merged

fix(go/observability): control-plane telemetry pipeline fixes#271
shuaijinchao merged 10 commits into
masterfrom
fix/obs-hot-reload

Conversation

@shuaijinchao

Copy link
Copy Markdown
Collaborator

Fixes a chain of issues found while bringing up the default data-plane + control-plane telemetry flow (OpenAI Responses client → Nyro → Anthropic Messages/Zhipu). Each is a separate commit.

What was broken

  1. Gateway never applied control-plane obs config (--config-server mode). The OTel provider was built once at startup from the still-empty config cache → resolved to the stdout default and was never rebuilt when the first config-sync snapshot arrived. Result: request logs went to the gateway's stdout; nothing reached the control plane.
  2. OTLP exports 404'd. The seeded endpoint is a base URL (http://host:port) shared by all three signals; WithEndpointURL does not append the default /v1/<signal> path when it is empty, so exports POSTed to / → admin NotFound.
  3. WebUI logs/metrics/traces empty. The admin's parquet sinks only flushed on their size trigger (maxRows=50000) or at shutdown — no time trigger — so low-traffic data never became queryable until restart.
  4. Input tokens always 0. Zhipu/GLM reports the real usage only in the final message_delta; the Anthropic decoder read input_tokens only from message_start (which GLM sends as 0).
  5. Logs Export Interval ignored. The configured interval was wired for metrics (PeriodicReader) and traces (batch timeout) but dropped for logs (fixed 1s SDK default).
  6. Error type casing. Framework error envelope types lowercased.

Changes (per commit)

  • fix(go/anthropic): adopt input/cache token counts from message_delta when present (real Anthropic unchanged); goldens updated (they had codified the bug).
  • refactor(go): gateway_errorGATEWAY_ERROR, auth_errorAUTH_ERROR. AiErrorKind protocol values stay snake_case (OpenAI/Anthropic wire contract).
  • fix(go/observability): hot-reload — SwappableProvider behind the once-registered hooks + ConfigCache OnSwap callback + obsManager that rebuilds the provider (and prometheus server) on obs-config change.
  • fix(go/observability): append per-signal /v1/<signal> path to base OTLP endpoints; honor the logs export interval.
  • feat(go/observability): per-signal time-triggered parquet flush via obs_<signal>_flush_interval (admin-local settings, sibling of retention, default 5s, editable in the WebUI Local Telemetry card).

Verification

  • Full go build ./... && go vet ./... && go test ./... green (protocoltest goldens regenerated).
  • WebUI npm run lint + npm run build + settings-layout tests green.
  • End-to-end reproduced with a mock Anthropic/Zhipu SSE upstream: after the fixes, /logs and /stats populate and show input_tokens=11, output_tokens=81.

Notes for reviewers

  • obs_<signal>_flush_interval and retention are admin-local (not pushed to the data plane) and applied at admin boot — the WebUI card notes "Restart Admin to apply".
  • Flush interval trades query freshness against parquet file count (one file per non-empty flush); default 5s, configurable per signal.

Zhipu/GLM's Anthropic-compatible streaming reports the real usage
(input+output) only in the final message_delta and leaves message_start's
usage all-zero. The decoder only read output_tokens from message_delta, so
input_tokens was always 0 in metrics/logs. Adopt input/cache counts from
message_delta when present; real Anthropic omits them there (input stays
from message_start), so its behavior is unchanged.

Golden files updated: they had codified the input_tokens=0 bug (the
anthropic stream cassette carries input_tokens in message_delta).
gateway_error -> GATEWAY_ERROR (proxy handlers and the admin WebUI
NotFound) and auth_error -> AUTH_ERROR (admin unauthorized). The
AiErrorKind protocol values (authentication_error, server_error, ...)
stay snake_case: they are the OpenAI/Anthropic wire error.type contract,
not framework errors.
In --config-server mode the gateway built its OTel provider once from the
still-empty config cache (resolving to the fixed stdout default) and never
rebuilt it, so the admin-seeded otlp settings that arrive with the first
config-sync snapshot never took effect: logs went to the gateway's stdout
and nothing reached the control plane.

Introduce SwappableProvider behind the once-registered phase hooks, add a
ConfigCache OnSwap callback, and an obsManager that re-resolves the obs
config on each snapshot and atomically rebuilds the provider (and
reconciles the prometheus scrape server) when it changes.
…erval

The admin-seeded OTLP endpoint is a base URL (scheme://host:port) shared
across the three signals; otlploghttp/otlpmetrichttp/otlptracehttp's
WithEndpointURL uses the URL path verbatim and does NOT append the default
/v1/<signal> when it is empty, so exports hit "/" and the receiver 404'd.
Append the per-signal path (otlpSignalURL), leaving an explicit path
untouched.

Also thread the configured "interval" into the logs BatchProcessor
(WithExportInterval); it was dropped, so the WebUI Export Interval had no
effect for logs (fixed 1s SDK default) while metrics and traces honored it.
The parquet sinks flushed only on their size trigger (maxRows) or at
shutdown, so a low-traffic admin never persisted rows to a file and /logs
and /stats stayed empty until restart. Add the missing time trigger: the
receiver runs one flusher per signal, cadence from the admin-local
obs_<signal>_flush_interval settings (siblings of the retention settings,
default 5s), editable in the WebUI's Local Telemetry card. Size-or-time,
whichever fires first.
Left over from removing the startMetricsServer tests; `make go-check`
(golangci-lint fmt) flagged it.
Two fixes to what the WebUI logs table shows, both from the same report:

- Protocol column was blank. Logs stored the versioned endpoint id
  ("anthropic-messages/2023-06-01"), but the UI resolves display names
  against the base protocol ids. Store the base Protocol
  ("anthropic-messages") instead — the version is an internal routing
  detail, not part of the protocol's identity — so the column resolves.

- Key column showed the key preview, not a name. The consumer key's Name
  was never propagated to the data plane (config-sync only carried
  preview+hash). Add ConsumerKeyRef.name to the config-sync contract, thread
  it through the snapshot/standalone paths, and log it as nyro.api_key_name
  (falling back to the preview for unnamed keys). Also emit
  nyro.api_key_preview so the UI can show the name with the preview beneath.
…sked

The request-log table showed the raw key_preview (12 leading + 6 trailing
chars, no mask), inconsistent with the API-keys page which renders it masked
via formatKeyPreview. Move formatKeyPreview into lib/format and reuse it in
both the API-keys page and the logs table, so a preview reads the same
everywhere ("sk-abcdef****…****klmnop") instead of looking like a full key.
The request-log key column showed the masked preview on a second line under
the consumer key name. Move it into a hover tooltip on the name instead —
the same affordance as the nodes list' connection-time tooltip — so the row
stays compact. Unnamed keys (whose name falls back to the preview) render
the masked preview directly, and rows with no key still show a dash.
golangci-lint errcheck flagged the deferred prov.Shutdown; discard its
return explicitly.
@shuaijinchao
shuaijinchao merged commit 0f0d601 into master Jul 17, 2026
5 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