Skip to content
Maple Docs
Open app
Browse the docs
On this page

Trace Strands Agents with OpenTelemetry

Send Strands Agents traces to Maple with the full transcript and one Agent Session per conversation.

Strands Agents has a built-in OpenTelemetry tracer, and Maple reads its spans without an extra instrumentation library. You set one environment variable so the transcript is recorded, and pass your conversation id as session.id on each agent.

You need strands-agents 1.51 or newer, or the TypeScript SDK @strands-agents/sdk 1.19 or newer.

Quick setup with a coding agent

Copy this prompt into a coding agent that can run shell commands, such as Claude Code, Codex or Cursor. It installs the maple-agent-tracing-strands skill and follows it.

Set up Maple agent tracing for Strands Agents in this project.

Install the skill with `npx skills add MapleTechLabs/maple/skills --skill maple-agent-tracing-strands -y`, then follow it.

My Maple ingest key is maple_pk_... and my organization is in the US region.

Your ingest key is in Settings → Ingestion. If your organization is in the EU region, change US to EU in the prompt.

Install and configure the exporter

Replace openai with your model provider’s extra (anthropic, litellm; Bedrock needs none).

pip install 'strands-agents[otel,openai]>=1.57'
uv add 'strands-agents[otel,openai]>=1.57'

Install the SDK with its OpenTelemetry packages:

npm install @strands-agents/sdk @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node @opentelemetry/resources @opentelemetry/exporter-trace-otlp-http @opentelemetry/sdk-metrics @opentelemetry/exporter-metrics-otlp-http
pnpm add @strands-agents/sdk @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node @opentelemetry/resources @opentelemetry/exporter-trace-otlp-http @opentelemetry/sdk-metrics @opentelemetry/exporter-metrics-otlp-http
bun add @strands-agents/sdk @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node @opentelemetry/resources @opentelemetry/exporter-trace-otlp-http @opentelemetry/sdk-metrics @opentelemetry/exporter-metrics-otlp-http

Set these environment variables:

export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.maple.dev"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_SERVICE_NAME="support-agent"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=production"
export OTEL_SEMCONV_STABILITY_OPT_IN="gen_ai_latest_experimental,gen_ai_span_attributes_only"

For an EU organization, use https://ingest.eu.maple.dev. The exporter appends /v1/traces itself.

Without OTEL_SEMCONV_STABILITY_OPT_IN the transcript is empty. Strands reads it when the first Agent is created, so set it in the environment rather than in code.

To redact message content, append gen_ai_unredacted_attributes= with a ;-separated allowlist of attributes to keep; everything else becomes [REDACTED].

Then start the tracer once, before your first agent runs:

# telemetry.py: import this from your entry point before creating any Agent
from strands.telemetry import StrandsTelemetry

telemetry = StrandsTelemetry().setup_otlp_exporter()

If your app already sets up a global TracerProvider (for example opentelemetry-instrument or the ADOT distro on AgentCore), skip StrandsTelemetry() and add a BatchSpanProcessor(OTLPSpanExporter(...)) pointing at Maple to that provider.

Use the same OTEL_EXPORTER_OTLP_* variables, with OTEL_SEMCONV_STABILITY_OPT_IN="gen_ai_latest_experimental,gen_ai_span_attributes_only" (the SDK doesn’t support the third value). Then:

import { Agent, FileStorage, SessionManager } from "@strands-agents/sdk"
import { OpenAIModel } from "@strands-agents/sdk/models/openai"
import { setupTracer } from "@strands-agents/sdk/telemetry"

const provider = setupTracer({ exporters: { otlp: true } }) // reads OTEL_EXPORTER_OTLP_* env vars

Pass the conversation id as session.id

Pass the conversation id in trace_attributes:

from strands import Agent
from strands.models.openai import OpenAIModel
from strands.session.file_session_manager import FileSessionManager

model = OpenAIModel(model_id="gpt-4o-mini", params={"max_tokens": 600})


def handle_message(conversation_id: str, text: str) -> str:
    agent = Agent(
        name="support_agent",
        model=model,
        tools=[get_weather, calculate],
        system_prompt="You are a concise support assistant.",
        session_manager=FileSessionManager(session_id=conversation_id, storage_dir="./sessions"),
        trace_attributes={"session.id": conversation_id},
        callback_handler=None,
    )
    return str(agent(text))

The session manager restores history but doesn’t put its session_id on the spans, so you need both arguments. Use the conversation id your app already has, never a fresh UUID per request.

Create the agent per request, as above. A shared module-level Agent would carry one user’s id into everyone’s traces. Give every agent a name, or sub-agents merge into one lane.

With agent.as_tool() sub-agents, only the orchestrator needs session.id. For a Swarm, pass trace_attributes={"session.id": conversation_id} to the Swarm. For a Graph, set graph.trace_attributes = {"session.id": conversation_id} after builder.build().

const model = new OpenAIModel({ modelId: "gpt-4o-mini", params: { max_tokens: 600 } })

async function handleMessage(conversationId: string, text: string): Promise<string> {
	const agent = new Agent({
		name: "support_agent",
		model,
		tools: [getWeather],
		systemPrompt: "You are a concise support assistant.",
		traceAttributes: { "session.id": conversationId },
		sessionManager: new SessionManager({
			sessionId: conversationId,
			storage: { snapshot: new FileStorage("./sessions") },
		}),
		printer: false,
	})
	return String(await agent.invoke(text))
}

Create the agent per request here too.

Flush in scripts and jobs

A long-running server needs nothing extra. Scripts, notebooks and jobs lose their last spans unless they flush:

from telemetry import telemetry

try:
    handle_message(conversation_id, "What's the weather in Berlin?")
finally:
    telemetry.tracer_provider.force_flush()
    telemetry.tracer_provider.shutdown()

On AWS Lambda, call telemetry.tracer_provider.force_flush() at the end of each invocation and don’t call shutdown().

try {
	await handleMessage(conversationId, "What's the weather in Berlin?")
} finally {
	await provider.forceFlush()
	await provider.shutdown()
}

Check that it works

Run a conversation of two or three messages, including one that calls a tool, then open Agent Sessions in Maple. You should see one session per conversation id with framework Strands Agents, one turn per agent(...) call, and a transcript with the user messages, replies and tool calls.

Cost shows as unpriced because Strands doesn’t report it.

Troubleshooting

  • Transcript is empty, but tokens and tools show up. Add gen_ai_span_attributes_only and gen_ai_latest_experimental to OTEL_SEMCONV_STABILITY_OPT_IN, set before the first Agent is created.
  • Every message is its own session. Pass trace_attributes={"session.id": conversation_id} to the agent, or to the Swarm or Graph that runs it.
  • Two users’ messages land in one session. A shared Agent carries one trace_attributes dict for everyone. Create the agent per request.
  • Every model call appears twice. Another instrumentation (OpenLIT, OpenLLMetry, OpenInference, an OpenAI or Bedrock instrumentor) wraps the same calls. Remove it.
  • Nothing arrives from a script. The process exited before the batch was exported. Call force_flush() and shutdown() in a finally block.