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) 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. It assumes you have already deployed an OTLP collector and configured warehouse ingestion β€” see Agent Monitors Overview 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.

πŸ“˜

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

Set both environment variables where your agent runs. 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_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).
🚧

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.

Exporting from an embedded Runner

If you call ADK's Runner in-process, wire the exporter with the Monte Carlo OpenTelemetry SDK, 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:

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 with OpenTelemetry Collector.
  • Monte Carlo's hosted collector β€” use https://integrations.getmontecarlo.com/otel/v1/traces with the x-mcd-id / x-mcd-token credentials issued when you register the data store. 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": ...})). See OpenTelemetry Data Store.

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

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 with OpenTelemetry Collector.
  • 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 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 for how traces and conversations are assembled.

What gets captured

Once content is on spans and traces are flowing to your warehouse:

SignalCaptured from
Prompts & completionsgen_ai.input.messages / gen_ai.output.messages on the LLM spans
System promptgen_ai.system_instructions
Tool calls (name, input, output)The tool-execution spans β€” emitted natively by both runtimes
Token usagegen_ai.usage.input_tokens / output_tokens (when your model reports it)
Conversation groupinggen_ai.conversation.id

Next steps

  1. Confirm traces are landing in your warehouse and select your trace table on the Agent Observability settings page.
  2. Create Agent Evaluation, Agent Metric, Agent Trajectory, and Agent Validation monitors over your ADK agents.


Did this page help you?