OpenTelemetry with Mocapi¶
Drop in mocapi-otel plus a backend-specific exporter and every tool / prompt / resource invocation produces a two-layer trace carrying OpenTelemetry MCP, GenAI, and JSON-RPC semantic-convention attributes.
What you get¶
Span hierarchy per invocation:
http POST /mcp (Spring MVC — SERVER kind)
└─ jsonrpc.server (ripcurl-o11y; mocapi-o11y enriches with mcp.* tags)
rpc.system.name=jsonrpc
jsonrpc.protocol.version=2.0
jsonrpc.request.id=42
rpc.response.status_code=OK (or JSON-RPC error code on failure)
error.type=… (on failure)
mcp.method.name=tools/call (added by mocapi-o11y's McpObservationFilter)
mcp.protocol.version=2026-07-28
mcp.client.name=… (from the _meta envelope)
└─ mcp.handler.execution (mocapi-o11y per-handler span)
mcp.handler.kind=tool
gen_ai.operation.name=execute_tool
gen_ai.tool.name=my-tool
For tools/call, prompts/get, and resources/* the inner mcp.handler.execution span fires with handler-specific GenAI / resource-URI attrs. For dispatch-only methods (tools/list, server/discover, notifications) only the outer jsonrpc.server span appears.
Inbound W3C trace context joins automatically: when the request's _meta envelope carries traceparent (plus optional tracestate/baggage), the jsonrpc.server span is parented to the caller's trace instead of starting a new one.
Metrics: two histogram meters — jsonrpc.server.duration (ripcurl-o11y) and mcp.handler.execution.duration (mocapi-o11y) — produced automatically by Micrometer's default meter observation handler whenever a MeterRegistry is in the context.
Attributes align with: - OTel MCP semconv - OTel JSON-RPC semconv - OTel GenAI semconv
Baseline setup¶
<dependency>
<groupId>com.callibrity.mocapi</groupId>
<artifactId>mocapi-otel</artifactId>
</dependency>
This transitively pulls:
- mocapi-o11y — MCP handler observations + the McpObservationFilter that enriches the outer JSON-RPC span with mcp.* tags
- ripcurl-o11y (via mocapi-o11y) — JSON-RPC observations with OTel JSON-RPC semconv attrs
- spring-boot-starter-opentelemetry — OTel SDK + Micrometer Observation → OTel tracing bridge + autoconfig
- spring-boot-micrometer-observation (via mocapi-o11y) — the ObservationRegistry bean Micrometer needs
mocapi-otel does not bundle an exporter — that's deployment-specific. Pick one of the recipes below.
Recipes¶
OTLP → Jaeger, Tempo, or an OTel Collector¶
<dependency>
<groupId>com.callibrity.mocapi</groupId>
<artifactId>mocapi-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
management.otlp.tracing.endpoint=http://localhost:4318/v1/traces
management.tracing.sampling.probability=1.0
Jaeger-specific note. Jaeger's all-in-one accepts traces at /v1/traces but not metrics at /v1/metrics. Without disabling OTLP metrics, Spring's OtlpMeterRegistry will log 404s every minute. Set:
management.otlp.metrics.export.enabled=false
This is not a default in mocapi-otel — Grafana Cloud, Tempo-with-OTel-Collector, and similar backends accept OTLP metrics fine, and forcing the default off would silently drop metrics for them. Only set it when the backend you're targeting is traces-only.
Azure Monitor / Application Insights¶
<dependency>
<groupId>com.callibrity.mocapi</groupId>
<artifactId>mocapi-otel</artifactId>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-monitor</artifactId>
</dependency>
The Azure monitor starter reads APPLICATIONINSIGHTS_CONNECTION_STRING from the environment and plugs its exporter into Spring Boot's OpenTelemetry bean. Uses the OTel SDK path (not the App Insights Java agent), so it's compatible with GraalVM native-image.
# Optional: tune sampling
management.tracing.sampling.probability=1.0
No endpoint property needed — the Azure starter picks up the destination from APPLICATIONINSIGHTS_CONNECTION_STRING.
Datadog via OTLP¶
<dependency>
<groupId>com.callibrity.mocapi</groupId>
<artifactId>mocapi-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
management.otlp.tracing.endpoint=http://localhost:4318/v1/traces # Datadog Agent OTLP receiver
management.tracing.sampling.probability=0.1
Requires the Datadog Agent running locally with an OTLP receiver enabled. See Datadog's OTLP ingest documentation for the agent-side configuration.
Optional: OTel-correlated logs¶
If you want every log line to flow to the same OTel backend as your spans — correlated by trace and span ID — add the Logback appender explicitly (not transitively via mocapi-otel):
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-logback-appender-1.0</artifactId>
<!-- Pick the instrumentation release that matches YOUR Spring Boot's managed
opentelemetry version — see the version warning below. -->
<version>2.24.0-alpha</version>
</dependency>
Version warning — match the appender to Boot's managed OTel, not to "latest".
mocapi-bommanages no OpenTelemetry versions, and Spring Boot's own BOM (imported first) wins dependency-management ordering over anything the instrumentation BOM declares. So the appender's code must be compatible with theopentelemetry-apithat Boot pins (opentelemetry.versionin the Boot BOM —1.55.0for Boot 4.0.x). A newer appender compiled against a newer API can produceNoSuchMethodErrorat logging time — e.g.2.26.1-alphacalls the stableLogRecordBuilder.setException(...)unguarded, which does not exist in 1.55.0; the error then fires from inside the logging subsystem itself, the worst possible place (it detonates precisely while an exception is being reported). Choose the instrumentation release whose transitiveopentelemetry-apiequals Boot's managed version, or explicitly alignopentelemetry.versionfirst.2.24.0-alphaand earlier use the guarded incubator path and degrade gracefully on 1.55.0.
Then configure a logback-spring.xml with the OpenTelemetryAppender (see OTel docs).
Why isn't this in mocapi-otel?
- It's Logback-specific; Log4j2 users need a different bridge.
- Log shipping is often a separate pipeline from tracing (stdout → Fluent Bit, syslog, etc.) — baking a specific path into a feature module is presumptuous.
- The OTel Java Instrumentation appender is -alpha stability; pulling alpha artifacts into a stable feature module's transitive set isn't safe.
Opt in explicitly when you want it.
Sampling¶
Spring Boot defaults to 10% sampling. For dev / demo:
management.tracing.sampling.probability=1.0
For production, 0.01–0.10 is typical. Our standing performance baseline measures 100% sampling — production throughput is typically 5–10% higher at 0.05 sampling, not a regression.
Known gaps¶
- Span kind is
INTERNAL, notSERVER. Both thejsonrpc.serverandmcp.handler.executionspans emit asINTERNALkind. Upgrading toSERVERkind requires MicrometerReceiverContextplumbing and hasn't been prioritized — the ambient HTTP span already carriesSERVERkind via Spring MVC, so filtering by kind still finds the inbound request. tool_errorattribute not emitted. When a tool returnsCallToolResult.isError=true(a spec-defined structured-error response rather than a thrown exception), we currently don't seterror.type=tool_erroron the span. DetectingisErrorrequires deserializing the tool's result; deferred to a follow-up.rpc.response.status_codeis on the outer JSON-RPC span only. The innermcp.handler.executionspan uses the standarderror.typeattr on failure;rpc.*attrs belong at the transport layer above.