ZuploZuplo
LoginSign Up
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
Development
Policies
Handlers
API Keys
MCP Server
MCP Gateway
    IntroductionBetaQuickstartQuickstart (Local Dev)How it works
    Connect MCP clients
    Authentication
    Configuration
    Observability
      AnalyticsLogging
    ReferenceTroubleshooting
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Observability

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
context.log.info( { event: "mcp_auth_downstream_token_issued", subjectId, operationId }, "Gateway issued an OAuth access token to an MCP client", );

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 (debug and info) 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 the virtualServerName in 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 reasonCode matches the reasonCode field on the corresponding log entry (e.g., missing_token, invalid_audience, connect_required).
  • The analytics virtualServerName is the log entry's operationId.
  • The analytics subjectId is the log entry's subjectId.
  • The analytics upstreamServerName is the log entry's upstreamServerId.

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
import { RuntimeExtensions } from "@zuplo/runtime"; import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway"; import { OpenTelemetryPlugin } from "@zuplo/runtime"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addPlugin(new McpGatewayPlugin()); runtime.addPlugin( new OpenTelemetryPlugin({ url: process.env.OTLP_TRACES_ENDPOINT, logUrl: process.env.OTLP_LOGS_ENDPOINT, authorization: process.env.OTLP_AUTHORIZATION, }), ); }

Grafana Cloud is one common destination — define the endpoint and credentials in .env:

TerminalCode
GRAFANA_OTLP_ENDPOINT=https://otlp-gateway-prod-<region>.grafana.net/otlp GRAFANA_OTLP_INSTANCE_ID=<instance-id> GRAFANA_OTLP_API_TOKEN=glc_<token>

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).
Edit this page
Last modified on May 27, 2026
AnalyticsReference
On this page
  • Structured-first by design
  • The three audiences
  • Fields attached to every request log
  • What the gateway never logs
  • Cross-referencing with analytics
  • Routing the logs to your provider
  • OpenTelemetry
  • Related
TypeScript
TypeScript