Google ADK Integration
Instrument Google Agent Development Kit (ADK) agents to emit OpenTelemetry GenAI traces to Monte Carlo Agent Observability.
Overview
Google's Agent Development Kit (ADK) emits OpenTelemetry traces for every agent run. Monte Carlo reads those traces from your warehouse to power Agent Observability — prompts, completions, tool calls, token usage, and conversation grouping.
Both ADK runtimes are supported:
- Python —
google-adk - Go —
google.golang.org/adk("adk-go")
This page covers the ADK-specific setup. Traces go either to Monte Carlo's hosted collector or to your own collector — see Agent Monitors Overview for the end-to-end pipeline.
The one thing to configure
Monte Carlo ingests OpenTelemetry traces and reads prompts and completions from gen_ai.* span attributes. Out of the box, neither ADK runtime puts content there — Python needs the settings below to route it onto span attributes, and Go emits content only on the OpenTelemetry logs signal, which isn't ingested. So out of the box your traces arrive with the agent's structure but without the prompts and completions.
Getting content onto span attributes is the core of ADK onboarding, and the steps differ by runtime.
Span structure (agent, LLM, and tool spans) is emitted natively by ADK with no extra configuration. The steps below are specifically about capturing prompt/completion content and grouping runs into conversations.
Python ADK
Choose your setup
How you instrument depends on what starts the process, because OpenTelemetry's global tracer provider can be set only once and whoever sets it first wins.
| You deploy with | What starts the process | Setup |
|---|---|---|
adk web, adk api_server, adk deploy cloud_run, adk deploy gke | ADK's own server | Environment variables (no code), or the Monte Carlo SDK in the agent module |
Your own main.py calling get_fast_api_app(), or an embedded Runner | Your code | mc.setup() first |
| Agent Runtime (formerly Vertex AI Agent Engine) | Google's runtime | An AdkApp subclass |
GKE and other container platforms follow the first or second row, depending on whether the container runs ADK's server or your own entrypoint.
The examples below use Monte Carlo's hosted collector, https://integrations.getmontecarlo.com/otel/v1/traces, with the x-mcd-id / x-mcd-token credentials issued at integration setup, the same key id and token the Monte Carlo SDK reads from MCD_DEFAULT_API_ID / MCD_DEFAULT_API_TOKEN. If you run your own collector, use its endpoint instead, ending in /v1/traces; no Monte Carlo credentials are required (access is network-restricted).
Capture prompts and completions
Every setup needs both of these environment variables. Either one alone yields spans with token counts but no message content:
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLYOTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimentalopts into OpenTelemetry's GenAI semantic conventions, which are still experimental and off by default. Opting in is what makes ADK emit prompts and completions as thegen_ai.input.messages/gen_ai.output.messages/gen_ai.system_instructionsattributes Monte Carlo reads.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLYwrites captured content onto span attributes (rather than log events).
Also set OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=1024. GenAI spans exceed OpenTelemetry's default limit of 128 attributes, and attributes past the limit are dropped silently. mc.setup() sets this for you.
These variables set the capture mode, not the export. Each setup below also wires an exporter.
ADK server
When adk web, adk api_server, or an adk deploy target runs your agent, ADK's server exports traces itself once an OTLP endpoint variable is set. No code changes are needed.
-
Install the OTLP exporter. ADK imports it for this path, but since ADK 2.0
google-adkdoesn't depend on it (it moved to thegcpandallextras). Installingmontecarlo-opentelemetryalso brings in the exporter.pip install "google-adk[gcp]" # or: pip install opentelemetry-exporter-otlp-proto-httpadk deploy cloud_runandadk deploy gkeinstall onlygoogle-adkin the image, so addopentelemetry-exporter-otlp-proto-httpto the agent folder'srequirements.txt. Without it, the server exits at startup withModuleNotFoundError: No module named 'opentelemetry.exporter'. -
Set these in the environment that launches the server, plus the capture variables above:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://integrations.getmontecarlo.com/otel/v1/traces OTEL_EXPORTER_OTLP_TRACES_HEADERS=x-mcd-id=<key id>,x-mcd-token=<token> OTEL_SERVICE_NAME=<your agent's name> OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=1024Use
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, notOTEL_EXPORTER_OTLP_ENDPOINT. The generic variable also turns on ADK's metrics and logs exporters, which Monte Carlo rejects, so your logs fill withFailed to export ... code: 403errors. The traces variable is used exactly as given, so it must end in/v1/traces. Likewise, useOTEL_EXPORTER_OTLP_TRACES_HEADERSrather thanOTEL_EXPORTER_OTLP_HEADERSso the credentials go only to trace exporters; the generic variable is sent by every OTLP exporter in the process that doesn't set its own headers.ADK reads these variables when the server starts. An agent's
.envfile is loaded later, on the first request, so variables set only there have no effect: traces aren't exported, or the agent appears asunknown_serviceor with no name.
Prefer the Monte Carlo SDK? Instead of the endpoint variables, add the Monte Carlo OpenTelemetry SDK to the agent module. ADK has already created the tracer provider by the time it imports your agent, so attach to that provider rather than creating one:
import montecarlo_opentelemetry as mc
mc.add_processor()
mc.add_exporter("https://integrations.getmontecarlo.com/otel/v1/traces") # reads MCD_DEFAULT_API_ID / MCD_DEFAULT_API_TOKENThis adds the processor that copies montecarlo.workflow / montecarlo.task baggage onto spans, plus the SDK's configuration warnings. Don't also set an OTEL_EXPORTER_OTLP_*ENDPOINT variable, or every span is sent twice.
On Cloud Run
When deploying with adk deploy cloud_run:
- Keep credentials in Secret Manager. With the environment-variable setup, the whole
OTEL_EXPORTER_OTLP_TRACES_HEADERSvalue is the secret. Pass it after--as--set-secrets OTEL_EXPORTER_OTLP_TRACES_HEADERS=<secret>:latest. - Keep
.envout of the image.adk deploycopies the agent folder into the image and honors only ignore files inside that folder, so add a.gcloudignorethere that lists.env. - Override the model location if needed.
adk deploysetsGOOGLE_CLOUD_LOCATIONto the deploy region, so models served only fromglobalreturn 404 unless you pass--env GOOGLE_CLOUD_LOCATION=global. - Removing a variable needs
--remove-env-vars.adk deployadds variables with--update-env-vars, so switching an existing service fromOTEL_EXPORTER_OTLP_ENDPOINTto the traces variable leaves the old one set.
Your own entrypoint
If your code starts the process (a custom FastAPI app or an embedded Runner), call mc.setup() before ADK builds its app, so Monte Carlo's tracer provider becomes the global one. ADK instruments itself, so you don't pass any instrumentors:
import montecarlo_opentelemetry as mc
mc.setup(
agent_name="<your agent's name>",
otlp_endpoint="https://integrations.getmontecarlo.com/otel/v1/traces",
)
from google.adk.cli.fast_api import get_fast_api_app # after mc.setup()
app = get_fast_api_app(agents_dir="<folder containing your agent>", web=False)Order matters: if get_fast_api_app() runs first, ADK's provider wins and mc.setup() logs that Monte Carlo's tracer provider was not installed. The same applies to an embedded Runner: call mc.setup() before you create it. mc.setup() reads the credentials (MCD_DEFAULT_API_ID, MCD_DEFAULT_API_TOKEN) when it runs, so they must be in the launch environment.
Agent Runtime
On Agent Runtime, Google's AdkApp.set_up() configures OpenTelemetry in each worker, wiring in Google's exporter so traces go to Cloud Trace. To send them to Monte Carlo as well, add Monte Carlo's processor and exporter to the provider Google created.
The catch is timing. set_up() runs after your code is imported and replaces the whole span pipeline, so anything attached at import time is wiped with no error: traces reach Cloud Trace but never Monte Carlo. Attach Monte Carlo after set_up() runs by subclassing AdkApp:
# mc_instrumented_app.py
import os
import montecarlo_opentelemetry as mc
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from vertexai.agent_engines import AdkApp
class McAdkApp(AdkApp):
def set_up(self):
super().set_up() # Google replaces the span pipeline here, so attach Monte Carlo after it
endpoint = "https://integrations.getmontecarlo.com/otel/v1/traces"
if isinstance(trace.get_tracer_provider(), TracerProvider):
mc.add_processor() # Google's telemetry on
mc.add_exporter(endpoint)
else: # telemetry turned off: no provider exists
mc.setup(agent_name=os.environ["OTEL_SERVICE_NAME"], otlp_endpoint=endpoint)# deploy.py
import vertexai
from google.cloud.aiplatform_v1.types import SecretRef
from vertexai import agent_engines
from mc_instrumented_app import McAdkApp
from my_agent.agent import root_agent
vertexai.init(
project="<project>",
location="<region>",
staging_bucket="gs://<bucket>", # create() uploads the app to this bucket
)
agent_engines.create(
McAdkApp(agent=root_agent),
requirements=["google-adk", "google-cloud-aiplatform[adk,agent-engines]", "montecarlo-opentelemetry"],
extra_packages=["mc_instrumented_app.py", "my_agent/__init__.py", "my_agent/agent.py"],
env_vars={
"GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY": "true",
"OTEL_SERVICE_NAME": "<your agent's name>",
"OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT": "1024",
"OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental",
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "SPAN_ONLY",
"MCD_DEFAULT_API_ID": "<key id>",
"MCD_DEFAULT_API_TOKEN": SecretRef(secret="<token secret>", version="latest"),
},
)- Pass configuration through
env_vars=. Agent Runtime doesn't read.envfiles. - Turn on Google's telemetry.
GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY=truemakesset_up()build the tracer provider thatMcAdkAppattaches to, and makes Agent Runtime flush it after every query. It also sends traces, including prompts and responses, to Cloud Trace, and logs to Cloud Logging. - Set
OTEL_SERVICE_NAME. Without it, the agent appears in Monte Carlo under its numeric engine ID. - Give the subclass's module a distinctive name, not
app.py. Shipped modules land in the worker's working directory, and a module namedapp.pymakes the worker exit at startup with no error logged. - Grant secret access. The Agent Runtime service agent (
service-<project number>@gcp-sa-aiplatform-re.iam.gserviceaccount.com) needsroles/secretmanager.secretAccessoron the token secret. This service agent exists only after the project's first Agent Runtime deployment, so the first deploy may fail withPERMISSION_DENIEDuntil you grant it.
When Google's telemetry is on, Agent Runtime flushes the tracer provider after every query, so the last spans of a conversation aren't lost. With telemetry off, McAdkApp falls back to mc.setup() and nothing flushes after a query: spans are exported on the batch timer, so the last spans can be lost if the instance is throttled.
Troubleshooting
| Symptom | Cause |
|---|---|
ADK's server exits at startup with ModuleNotFoundError: No module named 'opentelemetry.exporter' | The OTLP exporter isn't installed. Install google-adk[gcp] or opentelemetry-exporter-otlp-proto-http (on Cloud Run, in the agent folder's requirements.txt). |
Failed to export logs batch / metrics batch code: 403 in the agent's logs | OTEL_EXPORTER_OTLP_ENDPOINT is set. Use OTEL_EXPORTER_OTLP_TRACES_ENDPOINT instead. |
| Nothing reaches Monte Carlo; exports are rejected as an invalid key | The x-mcd-id / x-mcd-token values are quoted inside the headers variable (for example x-mcd-id="..." in a .env file), so the quotes are sent as part of the value. Remove them. The Monte Carlo SDK warns about this; the environment-variable setup doesn't. |
| Agent Runtime: traces in Cloud Trace, nothing in Monte Carlo, no errors | Monte Carlo was attached before set_up() ran (at import time, or before super().set_up()), so Google's setup wiped it. |
| Agent Runtime: agent appears in Monte Carlo as a long number | OTEL_SERVICE_NAME isn't in env_vars. |
Agent appears as unknown_service, or with no name | OTEL_SERVICE_NAME is only in the agent's .env, which ADK's server loads after creating the tracer provider. |
| Every span appears twice | Both an OTEL_EXPORTER_OTLP_*ENDPOINT variable and the Monte Carlo SDK's exporter are configured. |
Go ADK (adk-go)
adk-go does not emit message content on spans under any configuration — its content path is Go-side logs, which Monte Carlo does not ingest. Instead, set the content and grouping attributes directly on the active span at your shared model boundary (for example, your model.LLM implementation or AI gateway), where the span is still recording:
import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// ctx is the context passed into your model.LLM call.
span := trace.SpanFromContext(ctx)
if span.IsRecording() {
span.SetAttributes(
attribute.String("gen_ai.input.messages", inMsgsJSON),
attribute.String("gen_ai.output.messages", outMsgsJSON),
attribute.String("gen_ai.system_instructions", sysJSON),
attribute.String("gen_ai.conversation.id", sessionID),
)
}gen_ai.input.messagesandgen_ai.output.messagesare each a JSON string holding an array of messages. Monte Carlo accepts two shapes: a flat[{"role": "...", "content": "..."}]array, or the standard semantic-convention[{"role": "...", "parts": [{"type": "...", "content": "..."}]}](content nested underparts). The flat shape is enough; the parts shape also works for text and tool-call parts.- A tool call the model makes is a part
{"type": "tool_call", "id": "...", "name": "...", "arguments": {...}}—argumentsis a nested JSON object (a JSON-encoded string works too), and Monte Carlo reads either form. - A tool result carried into the next turn should put the result text in the message's
content(a flat{"role": "tool", "content": "..."}) — atool_call_responsepart carries its payload elsewhere and won't render in the message view. gen_ai.system_instructionsis a bare array of{"type": "text", "content": "..."}objects (norolewrapper).- One edit at a shared model/gateway boundary covers every ADK agent behind it.
- The model name shown in Monte Carlo comes from what your model client reports — adk-go records it as
gen_ai.request.model. If that is blank (common behind an OpenAI-compatible gateway, where the client's name is empty), setgen_ai.request.modelyourself in theSetAttributescall above so runs are labeled with the model. Token counts likewise appear only when your client reports usage. - Tool calls need nothing extra — adk-go already emits tool input and output natively.
- Set these attributes before your model client yields its first non-partial response — adk-go ends the
generate_contentspan at that point, and attributes set after a span ends are dropped.
Wiring the exporter (Go)
adk-go and any HTTP instrumentation share the global TracerProvider, so a single OTLP/HTTP exporter carries every span. Where you build your provider today:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
// ctx is your startup context.
mcExp, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpointURL("https://<your-endpoint>/v1/traces"),
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
// Set the credentials as headers on THIS exporter — not via
// OTEL_EXPORTER_OTLP_HEADERS, which applies to every OTLP exporter in
// the process and would attach them to your other export paths.
otlptracehttp.WithHeaders(map[string]string{
"x-mcd-id": mcdID,
"x-mcd-token": mcdToken,
}),
)
if err != nil {
// handle the error
}
// The SDK default span attribute-count limit is 128 and overflow is dropped
// silently; raise it at provider construction.
limits := sdktrace.NewSpanLimits()
limits.AttributeCountLimit = 1024
tp := sdktrace.NewTracerProvider(
// ... your existing options: a resource that sets service.name — this is
// the agent's identity in Monte Carlo — plus any existing span processors ...
sdktrace.WithBatcher(mcExp, sdktrace.WithMaxExportBatchSize(128)),
sdktrace.WithRawSpanLimits(limits),
)
otel.SetTracerProvider(tp) // adk-go resolves the global provider- Endpoint — OTLP over HTTP/protobuf (no gRPC). Only traces are ingested; logs and metrics — including adk-go's
gen_ai.*log records — are not. - Check the endpoint URL — if
WithEndpointURLcan't parse the URL it keeps the defaultlocalhost:4318instead of returning an error, and the parse failure is only logged through OpenTelemetry's internal error logging. A bad endpoint can therefore silently export your traces to the wrong destination (or drop them) rather than failing your setup code. - Credentials — for Monte Carlo's hosted collector (
https://integrations.getmontecarlo.com/otel/v1/traces— note the/otelpath), set thex-mcd-id/x-mcd-tokenheaders on this exporter (above). A collector on your own network needs none — see Agent Observability Data Platform. service.name— set it on the resource; it is the agent's identity in Monte Carlo.- Attribute limit — raise it to 1024 as shown (or
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT=1024, ignored if your code already sets span limits). The default of 128 drops attributes with no error. - Batching — export requests are rejected above ~4 MB of uncompressed span data; the 128-span batch (vs the OTel default of 512) keeps typical batches under it. Enable gzip.
- Already initializing ADK telemetry with
telemetry.New()/SetGlobalOtelProviders()? adk-go emits its spans through the globalTracerProvider, so register exactly one — either build your own with the Monte Carlo exporter (above), or keep ADK's helper and attach the exporter through its options. Registering both is unreliable: adk-go binds to whichever provider is set first, so a laterotel.SetTracerProvider(...)may never capture adk-go's own spans (it's first-wins, not last-wins).
Grouping runs into conversations
Runs are grouped into a single conversation by gen_ai.conversation.id, and the conversation view threads together every turn that shares one:
- Python — set automatically from your ADK session id, so each session becomes one conversation. No extra work is required.
- Go — adk-go sets it on the
invoke_agentspan (ADK's top-level span) from the ADK session id, but not on the content spans, and it only threads turns together if that id is stable across them. Set your own stable conversation id (for example your session store's id) on the content spans in theSetAttributescall shown above, so every turn groups.
Optionally set montecarlo.workflow (the flow handling the request) and montecarlo.task (the step within it) on your spans to group and filter runs by workflow and step.
Monte Carlo reads these grouping attributes per span, with no inheritance from parent to child — so set gen_ai.conversation.id (and any montecarlo.workflow / montecarlo.task) on every span you want grouped or filtered: the root, LLM, and tool spans alike, not just one.
In Python, the Monte Carlo SDK does this for montecarlo.workflow / montecarlo.task. Wrap the work in mc.create_span_with_workflow() / mc.create_span_with_task() (or the mc.trace_with_workflow / mc.trace_with_task decorators), and the SDK's processor, added by mc.add_processor() or mc.setup(), copies the value onto every span started inside it, including ADK's. The environment-variable-only ADK server setup doesn't include that processor.
See Trace and Conversation Structure for how traces and conversations are assembled.
What gets captured
Once content is on spans and traces are flowing to your warehouse:
| Signal | Captured from |
|---|---|
| Prompts & completions | gen_ai.input.messages / gen_ai.output.messages on the LLM spans |
| System prompt | gen_ai.system_instructions |
| Tool calls (name, input, output) | The tool-execution spans — emitted natively by both runtimes |
| Token usage | gen_ai.usage.input_tokens / output_tokens (when your model reports it) |
| Conversation grouping | gen_ai.conversation.id |
Next steps
- Confirm traces are landing in your warehouse and select your trace table on the Agent Observability settings page.
- Create Agent Evaluation, Agent Metric, Agent Trajectory, and Agent Validation monitors over your ADK agents.
Updated 4 days ago
