Logging and OpenTelemetry
The Zuplo MCP Gateway emits structured logs alongside its analytics events. Each
log entry is keyed by an event string, carries auto-attached fields
identifying the route and authenticated user, and is shaped to cross-reference
the analytics dashboard one-to-one. This page explains the
log model — what's emitted, what's deliberately excluded, and how the
identifiers line up across analytics and logs.
For the list of supported destinations and how to enable a log plugin, see Logging in the platform docs.
Structured-first by design
The gateway writes every log entry in the structured form, with a stable event
key plus contextual fields:
Code
The event field is the searchable key. Names use snake-case throughout with
mcp_ as the family prefix — mcp_auth_downstream_token_issued,
mcp_auth_upstream_connection_established, mcp_capability_invoked, and so on.
The human-readable message is for log readers; the event field and the
structured properties are what dashboards, alerts, and queries run against.
The three audiences
Log entries serve three distinct audiences, and the gateway uses severity to distinguish them:
- Audit entries (
info) record OAuth lifecycle moments — token issuance, consent approval, upstream connection established, token revocation. They exist so a security or compliance team can answer "what auth events happened, when, and to whom?" without scanning request logs. - Visibility entries (
debugandinfo) record request acceptance, response shaping, and other engineering-facing operational detail. They exist for the platform team running the gateway. - Error entries (
error) include flattened error-chain fields —errName,errMessage,causeName,causeMessage, up to four cause levels — so the original failure context survives rethrows. The chain shape means a single log entry usually carries enough context to root-cause a failure without correlating multiple lines.
Fields attached to every request log
Three identifying fields are attached to every log entry emitted during an MCP request:
operationId— the route identity, the same value used as thevirtualServerNamein analytics.upstreamServerId— the upstream id from the token exchange policy, populated once the upstream is resolved.subjectId— the authenticated user's stable subject id, populated once the bearer token is validated.
These fields make it trivial to filter a log provider by route, upstream, or user without scanning message bodies. The same three identifiers also appear on the corresponding analytics events, which is what lets an operator pivot from a Portal analytics view to the underlying log entries with the same filter values.
Additional custom log properties can be attached from a policy or handler using
context.log.setLogProperties(). See
Logging → Custom log properties
for the platform-level reference.
What the gateway never logs
The gateway is deliberately strict about credentials and secrets. The following are never written to any log entry:
- Bearer tokens (downstream or upstream)
- OAuth authorization codes
- PKCE code verifiers
- Client secrets
- Raw signed JWTs (state, session, browser-ticket)
- Full redirect URIs (only the host is logged, via a
safeHost()helper) - Customer request bodies
If a credential or body needs inspection for debugging, it's done through purpose-built tooling — the gateway's log surface is intentionally narrow, and widening it would defeat the audit guarantees.
Cross-referencing with analytics
The gateway uses the same identifiers in logs and analytics, so an operator can pivot between the two with the same filter values:
- An analytics event's
reasonCodematches thereasonCodefield on the corresponding log entry (e.g.,missing_token,invalid_audience,connect_required). - The analytics
virtualServerNameis the log entry'soperationId. - The analytics
subjectIdis the log entry'ssubjectId. - The analytics
upstreamServerNameis the log entry'supstreamServerId.
When the Analytics dashboard surfaces an interesting slice —
say, a spike of connect_required reason codes for a specific upstream — the
same dimensions filter the log provider directly. The two data surfaces are
designed to reinforce each other, not duplicate each other.
Routing the logs to your provider
The MCP Gateway logs flow through the standard Zuplo logging pipeline. Enabling
a log plugin in modules/zuplo.runtime.ts is independent of the MCP Gateway —
once the plugin is wired up, gateway logs appear alongside the rest of the
project's logs.
Supported destinations include AWS CloudWatch, Datadog, Dynatrace, Google Cloud Logging, Loki, New Relic, Splunk, Sumo Logic, and VMware Log Insight. For destinations not in that list, the custom logging plugin pattern applies the same way.
OpenTelemetry
The gateway integrates with Zuplo's
OpenTelemetry plugin to export traces and
logs in OTLP format. Traces include spans for the request, every inbound policy
(mcp-*-inbound), the handler, and the upstream fetch; logs export the same
structured entries described above with their event field preserved.
Registering both plugins is a small addition to modules/zuplo.runtime.ts:
modules/zuplo.runtime.ts
Grafana Cloud is one common destination — define the endpoint and credentials in
.env:
Code
Use the base OTLP endpoint that ends in /otlp — the runtime appends
/v1/traces and /v1/logs itself. Other OTLP-compatible destinations
(Honeycomb, Dynatrace, New Relic, Tempo, self-hosted Jaeger) work the same way;
substitute the endpoint and credential headers.
Metrics export isn't supported by the current OpenTelemetry plugin. The plugin exports traces and logs only. For metrics, use a dedicated metrics plugin.
Related
- Analytics — the dashboard view of the same underlying events.
- Logging — Zuplo's general logging guide, including custom log properties and the full plugin list.
- OpenTelemetry — trace and log export configuration, including the available options on the plugin.
- Metrics plugins — metrics destinations (separate from the OTel plugin).