---
updatedAt: 2026-09-30T21:28:23.000Z
agentTools:
  projectIndex: https://docs.getmontecarlo.com/llms.txt
---

# Google ADK Integration

Instrument Google Agent Development Kit (ADK) agents to emit OpenTelemetry GenAI traces into your warehouse for Monte Carlo Agent Observability.

## Overview

Google's [Agent Development Kit (ADK)](https://google.github.io/adk-docs/) emits OpenTelemetry traces for every agent run. Monte Carlo reads those traces from your warehouse to power [Agent Observability](https://docs.getmontecarlo.com/docs/agent-monitors-overview) — 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. It assumes you have already deployed an OTLP collector and configured warehouse ingestion — see [Agent Monitors Overview](https://docs.getmontecarlo.com/docs/agent-monitors-overview#instrumenting-your-agent) for that 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.

<Callout icon="📘" theme="info">
  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.
</Callout>

## Python ADK

Set both environment variables where your agent runs. Either one alone yields spans with token counts but no message content:

```bash
OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY
```

* `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental` opts into OpenTelemetry's GenAI semantic conventions, which are still experimental and off by default. Opting in is what makes the instrumentation emit prompts and completions as the `gen_ai.input.messages` / `gen_ai.output.messages` attributes Monte Carlo reads.
* `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=SPAN_ONLY` writes captured content onto span attributes (rather than log events).

<Callout icon="🚧" theme="warn">
  These variables set the capture **mode**, not the **export**. `adk web` / `adk api_server` wire an OTLP exporter only when you set `OTEL_EXPORTER_OTLP_ENDPOINT` (point it at your collector; for the hosted collector also set `OTEL_EXPORTER_OTLP_HEADERS` with your credentials). An embedded `Runner` (ADK called in-process) exports nothing until you set up an exporter yourself — see below.
</Callout>

### Exporting from an embedded Runner

If you call ADK's `Runner` in-process, wire the exporter with the [Monte Carlo OpenTelemetry SDK](https://pypi.org/project/montecarlo-opentelemetry/), which sets up the tracer provider and OTLP exporter for you. ADK instruments itself, so you don't pass any instrumentors — just call `mc.setup()` with your endpoint **before** you start the agent:

```python
import montecarlo_opentelemetry as mc

mc.setup(
    agent_name="my-adk-agent",
    otlp_endpoint="https://<your-collector>/v1/traces",
)

# Then run your ADK agent as usual — its spans export through this exporter.
```

The endpoint (and whether credentials are needed) depends on how your collector is deployed:

* **Your own collector** — point `otlp_endpoint` at it, ending in `/v1/traces`; no Monte Carlo credentials are required (access is network-restricted). See [Agent Observability Data Platform](https://docs.getmontecarlo.com/docs/ao-platform-overview).
* **Monte Carlo's hosted collector** — use `https://integrations.getmontecarlo.com/otel/v1/traces` with the `x-mcd-id` / `x-mcd-token` credentials issued at integration set up. With `mc.setup()`, pass them via `OTEL_EXPORTER_OTLP_HEADERS`. If you also run other OTLP exporters in the same process, that env var attaches the credentials to them too — instead, give `mc.setup()` a dedicated exporter with `span_processor=BatchSpanProcessor(OTLPSpanExporter(endpoint=..., headers={"x-mcd-id": ..., "x-mcd-token": ...}))`.

## 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:

```go
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.messages` and `gen_ai.output.messages` are 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 under `parts`). 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": {...}}` — `arguments` is 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": "..."}`) — a `tool_call_response` part carries its payload elsewhere and won't render in the message view.
* `gen_ai.system_instructions` is a bare array of `{"type": "text", "content": "..."}` objects (no `role` wrapper).
* 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), set `gen_ai.request.model` yourself in the `SetAttributes` call 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_content` span 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:

```go
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 `WithEndpointURL` can't parse the URL it keeps the default `localhost:4318` instead 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 `/otel` path), set the `x-mcd-id` / `x-mcd-token` headers on this exporter (above). A collector on your own network needs none — see [Agent Observability Data Platform](https://docs.getmontecarlo.com/docs/ao-platform-overview).
* `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&#x20;**`telemetry.New()`**&#x20;/&#x20;**`SetGlobalOtelProviders()`**?** adk-go emits its spans through the global `TracerProvider`, 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 later `otel.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_agent` span (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 the `SetAttributes` call 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.

See [Trace and Conversation Structure](https://docs.getmontecarlo.com/docs/exploring-agent-traces) 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

1. Confirm traces are landing in your warehouse and select your trace table on the <Anchor target="_blank" href="https://getmontecarlo.com/settings/agent-observability">Agent Observability</Anchor> settings page.
2. Create [Agent Evaluation](https://docs.getmontecarlo.com/docs/agent-evaluation-monitors), [Agent Metric](https://docs.getmontecarlo.com/docs/agent-metric-monitors), [Agent Trajectory](https://docs.getmontecarlo.com/docs/agent-trajectory-monitors), and [Agent Validation](https://docs.getmontecarlo.com/docs/agent-validation-monitors) monitors over your ADK agents.