OTEL & ADOT: Auto vs. Manual
Runtime auto-instrumentation and instrumenting everything else
- Explain why a Runtime-hosted agent deployed via the AgentCore CLI is auto-instrumented with OpenTelemetry and needs no extra libraries or config for baseline telemetry
- Instrument an agent running outside Runtime by adding `aws-opentelemetry-distro`, creating a log group, setting the OTEL env vars, and launching with `opentelemetry-instrument`
- Distinguish what OTEL auto-captures in both modes from the custom spans, metrics, logs, and attributes you add yourself
- Identify the supported instrumentation libraries — framework-native (Strands/LangChain/CrewAI) plus OpenInference, OpenLLMetry, OpenLIT, and Traceloop
- Route a Runtime-hosted agent's telemetry to a non-AWS backend with `DISABLE_ADOT_OBSERVABILITY=true` and the right requirements
- Emit custom metrics to the `bedrock-agentcore` namespace via EMF and attach custom attributes/baggage to spans
AgentCore Observability emits OpenTelemetry-compatible telemetry into Amazon CloudWatch. There are exactly two ways to get it: deploy an agent through the AgentCore CLI and it is auto-instrumented (baseline traces, sessions, and metrics with zero extra libs or config), or run your agent anywhere else and instrument it yourself with the AWS Distro for OpenTelemetry (ADOT). This lesson draws the line between the two paths, shows what OTEL captures for free versus what you add by hand, and explains how to route a Runtime-hosted agent's telemetry to a third-party backend like Datadog or Langfuse.
- 1Two instrumentation paths, one telemetry model
- 2Path A — Runtime-hosted: auto-instrumented, zero config
- 3Path B — Outside Runtime: instrument it yourself with ADOT
- 4What OTEL captures for free vs. what you add
- 5Supported instrumentation libraries
- 6Routing Runtime-hosted agents to a third-party backend
Two instrumentation paths, one telemetry model
AgentCore Observability is the operations pillar: trace, debug, and monitor agents in production with per-step visualizations. It is powered by Amazon CloudWatch and emits OTEL-compatible telemetry, so it plugs into whatever you already run. Metrics, spans, traces, and logs all land in CloudWatch.
The single most important distinction in this lesson is where your agent runs, because that decides who instruments it:
- A) Runtime-hosted (auto-instrumented). You deploy through the AgentCore CLI. The platform wires up OpenTelemetry for you — no extra libraries, no extra config — and you get baseline traces, sessions, and metrics immediately.
- B) Outside Runtime (you instrument). Your agent runs on EKS, ECS, Lambda, or your laptop. AgentCore is not in the request path, so you add the AWS Distro for OpenTelemetry (ADOT), create a log group, set OTEL env vars, and start the process under
opentelemetry-instrument.
Both paths produce the same shape of data and feed the same CloudWatch backend. The difference is purely who does the wiring.
| Runtime-hosted (auto) | Outside Runtime (manual) | |
|---|---|---|
| Who instruments | The platform, at deploy | You, in your image/process |
| Extra deps | None for baseline | aws-opentelemetry-distro>=0.10.0 |
| Launch | agentcore deploy / agentcore invoke | opentelemetry-instrument python agent.py |
| Log group | Auto-created by Runtime | You create it |
| Config | None | OTEL env vars (see below) |
Watch out
Transaction Search is a hard prerequisite for both
Before spans are searchable in either mode you must enable CloudWatch Transaction Search once per account/region (CloudWatch -> Settings -> Account -> X-Ray traces -> Transaction Search -> Enable, then set the index sampling percentage). As of writing AWS lets you index a baseline percentage (commonly cited as the first 1%) at no charge before per-indexed-span charges apply — confirm the current free allotment on the live CloudWatch pricing page. It can take roughly 10 minutes before spans show up. Forgetting this is the number-one reason traces are missing even though instrumentation is correct.
Path A — Runtime-hosted: auto-instrumented, zero config
When you deploy a Runtime-hosted agent through the AgentCore CLI, OpenTelemetry instrumentation is applied for you. You write a normal agent, wrap it with the SDK, deploy, and the baseline telemetry appears in CloudWatch with no extra libs or config.
# agent.py — a plain Runtime agent. Nothing here is observability code.
from bedrock_agentcore import BedrockAgentCoreApp
from strands import Agent
app = BedrockAgentCoreApp()
agent = Agent() # Strands
@app.entrypoint
def invoke(payload):
return agent(payload["prompt"]).message
if __name__ == "__main__":
app.run() # binds 0.0.0.0:8080, serves /invocations + /pingagentcore deploy # package + deploy to Runtime (auto-instrumented)
agentcore invoke '{"prompt":"hi"}' # pass a runtimeSessionId to drive the Sessions viewThat is the entire setup for path A. The CLI provisions the Runtime endpoint, auto-creates the log group, and turns on OTEL. The InvokeAgentRuntime span (with attributes like session.id, latency_ms, and error_type) flows to aws/spans, and Runtime metrics land in the bedrock-agentcore namespace. Pass a runtimeSessionId on invoke so the Sessions View can group activity by session.
A practical caveat: because the platform already instruments the agent, do not also wrap it in your own opentelemetry-instrument or a second auto-instrumentor — double-instrumenting a Runtime agent produces duplicate/confusing spans.
Tip
Your framework still has to emit traces
Auto-instrumentation hooks the agent loop, but the underlying framework must actually produce OTEL spans. For Strands that means the OTEL extra — strands-agents[otel] — is present, and more generally that aws-opentelemetry-distro and the relevant auto-instrumentors are available. If a framework emits nothing, there are no spans to capture.
Path B — Outside Runtime: instrument it yourself with ADOT
If your agent runs anywhere other than Runtime, AgentCore is not intercepting calls, so you take over instrumentation. The recipe is four steps: add ADOT, create a log group, set OTEL env vars, launch via opentelemetry-instrument.
1) Add the distro to your dependencies:
# requirements.txt
aws-opentelemetry-distro>=0.10.0
strands-agents[otel] # if you use Strands; emits OTEL spans2) Create the CloudWatch log group your agent will write to (Runtime would have done this for you; outside Runtime it does not exist yet):
aws logs create-log-group --log-group-name agents/my-agent-logs3) Set the OTEL environment variables. These are the exact keys AgentCore expects:
export AGENT_OBSERVABILITY_ENABLED=true
export OTEL_PYTHON_DISTRO=aws_distro
export OTEL_PYTHON_CONFIGURATOR=aws_configurator # Python only
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_TRACES_EXPORTER=otlp
export OTEL_RESOURCE_ATTRIBUTES="service.name=my-agent,aws.log.group.names=agents/my-agent-logs"
export OTEL_EXPORTER_OTLP_LOGS_HEADERS="x-aws-log-group=agents/my-agent-logs,..."4) Launch the process under the auto-instrumentor instead of running Python directly:
opentelemetry-instrument python agent.pyIn a container, that becomes the entrypoint command:
CMD ["opentelemetry-instrument", "python", "main.py"]That is the whole difference from path A: you supply the dependency, the log group, the env vars, and the launch wrapper that the Runtime would otherwise have supplied.
Note
Session IDs outside Runtime
Runtime derives session.id from the X-Amzn-Bedrock-AgentCore-Runtime-Session-Id header (boto3 runtimeSessionId). Outside Runtime there is no such header, so set it via OTEL baggage in code: baggage.set_baggage("session.id", session_id). Without it, the Sessions View has nothing to group on.
Watch out
Env var keys and versions are volatile
The exact ADOT env var keys, the aws-opentelemetry-distro minimum version, and the OTEL log-group suffix (docs show both runtime-logs and otel-rt-logs) move over time. Treat the values here as 'correct as of writing' and confirm against the live observability-configure page before shipping.
What OTEL captures for free vs. what you add
Regardless of which path you took, OpenTelemetry auto-captures the agent's whole execution shape:
- Agent invocation sequences — the end-to-end request path.
- LLM calls and responses — model invocations and their outputs.
- Tool invocations and results — every tool the agent calls and what it returned.
- Error paths — failures classified into throttle / system / user (
error_typeon the span). - Framework integration — Strands, LangChain, and the other supported frameworks are stitched in automatically.
What the platform does not invent for you — the things specific to your domain — you add explicitly:
- Custom spans / traces to mark business-meaningful units of work.
- Custom metrics emitted via EMF into the
bedrock-agentcorenamespace. - Custom logs for your own application events.
- Custom attributes / baggage to enrich spans (e.g. a tenant id, a feature flag, the session id).
from opentelemetry import trace, baggage
tracer = trace.get_tracer(__name__)
# A custom span with custom attributes
with tracer.start_as_current_span("retrieve_policy_docs") as span:
span.set_attribute("tenant.id", tenant_id)
span.set_attribute("doc.count", len(docs))
# baggage propagates across the trace (e.g. for session grouping)
baggage.set_baggage("session.id", session_id)Custom metrics use the CloudWatch Embedded Metric Format (EMF), written to the bedrock-agentcore namespace. Note that the execution role must allow cloudwatch:PutMetricData scoped with the condition cloudwatch:namespace=bedrock-agentcore.
Watch out
APPLICATION_LOGS capture full payloads — filter PII
APPLICATION_LOGS capture the full request_payload and response_payload along with trace_id / span_id / session_id. That is great for debugging and terrible for compliance if a payload carries secrets or personal data. Strip or redact sensitive fields before they are emitted; CloudWatch will durably store whatever you send it.
Supported instrumentation libraries
AgentCore Observability does not lock you into a single instrumentation source. The supported families are:
- Framework-native instrumentation — Strands, LangChain, and CrewAI emit OTEL spans directly. For Strands that capability rides on the
strands-agents[otel]extra. - OpenInference (from Arize).
- OpenLLMetry (from Traceloop).
- OpenLIT.
- Traceloop.
Because everything reduces to OpenTelemetry, these libraries coexist: a Strands agent can emit its native spans while an OpenInference or OpenLLMetry instrumentor adds LLM-semantic-convention detail on top. The collector and exporter config (the OTEL env vars from path B, or the platform's wiring in path A) is what funnels them all into CloudWatch.
# requirements.txt — mixing framework-native + an OTEL instrumentor
strands-agents[otel]
aws-opentelemetry-distro>=0.10.0
openinference-instrumentation-langchain # example add-on instrumentorKey insight
Same traces feed Evaluations
These OTEL traces are not just for dashboards. AgentCore Evaluations read the very same traces (with GenAI semantic conventions) to score agents — online evaluation samples a configurable percentage of traces and grades them with no code changes. Good, well-attributed instrumentation pays off twice: once for debugging, once for automated quality scoring.
Routing Runtime-hosted agents to a third-party backend
By default a Runtime-hosted agent ships its telemetry into AWS via ADOT. If your organization standardizes on Datadog, Dynatrace, LangSmith, or Langfuse, you can redirect a Runtime agent's telemetry to that backend instead.
The switch is a single environment variable:
DISABLE_ADOT_OBSERVABILITY=trueSetting this on the Runtime tells AgentCore to stand down its default ADOT export so your own exporter / OTLP configuration takes over and points at the third-party collector. Two things people forget:
- Keep the OTEL machinery in your dependencies. Disabling the AWS export does not mean you can drop instrumentation — your image still needs the framework's OTEL support and the distro, e.g.
strands-agents[otel]andaws-opentelemetry-distroinrequirements.txt, so spans are actually produced and exportable. - Configure the destination yourself. With ADOT disabled you supply the OTLP endpoint, headers, and credentials for Datadog/Langfuse/etc. via the standard OTEL exporter env vars; AgentCore is no longer doing that for you.
# requirements.txt for a Runtime agent exporting to a non-AWS backend
strands-agents[otel]
aws-opentelemetry-distro# On the Runtime: turn off the AWS export, then point OTEL at your backend
DISABLE_ADOT_OBSERVABILITY=true
OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-backend-collector>
OTEL_EXPORTER_OTLP_HEADERS="<auth header for your backend>"The net effect: you keep AgentCore Runtime's hosting and session isolation, but your traces land in the observability tool your team already lives in.
Tip
Don't double-instrument when redirecting
When you set DISABLE_ADOT_OBSERVABILITY=true, you are replacing one exporter, not adding a second pipeline. Avoid layering a manual opentelemetry-instrument wrapper on top of the platform's auto-instrumentation — that is the same double-instrumentation gotcha as path A and produces duplicated spans in your third-party backend.
Try it: Instrument the same agent two ways and redirect to a backend
Goal: prove you understand both instrumentation paths and the third-party redirect by running the same agent under each.
0. Prereq (once). In the CloudWatch console, enable Transaction Search: CloudWatch -> Settings -> Account -> X-Ray traces -> Transaction Search -> Enable, and set the index sampling percentage (a baseline percentage is free as of writing — check the live CloudWatch pricing page). Wait ~10 minutes before expecting spans.
1. Path A — auto. Write a minimal Strands agent wrapped in BedrockAgentCoreApp with an @app.entrypoint. Deploy it with agentcore deploy and invoke it with agentcore invoke '{"prompt":"summarize AgentCore observability"}', passing a runtimeSessionId. In the CloudWatch GenAI Observability dashboard (#gen-ai-observability -> Bedrock AgentCore tab), confirm you see the InvokeAgentRuntime span and that the Sessions View groups by your session id. Note that you wrote zero observability code.
2. Path B — manual. Take the SAME agent and run it outside Runtime (locally is fine). Add aws-opentelemetry-distro>=0.10.0 and strands-agents[otel] to requirements.txt. Create a log group: aws logs create-log-group --log-group-name agents/lab-agent. Export the OTEL env vars (AGENT_OBSERVABILITY_ENABLED=true, OTEL_PYTHON_DISTRO=aws_distro, OTEL_PYTHON_CONFIGURATOR=aws_configurator, OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf, OTEL_TRACES_EXPORTER=otlp, and the resource attributes pointing at your log group). Launch with opentelemetry-instrument python agent.py. In code, set baggage.set_baggage("session.id", session_id) and confirm the Sessions View groups by it.
3. Add custom telemetry. In the agent, wrap a unit of work in a custom span with tracer.start_as_current_span(...), attach a custom attribute (e.g. tenant.id), and emit one custom metric via EMF into the bedrock-agentcore namespace. Verify the span attribute appears in the Trace View and the metric appears in CloudWatch Metrics.
4. Redirect (conceptual + config). For the Runtime-hosted version, set DISABLE_ADOT_OBSERVABILITY=true and configure the OTLP exporter env vars to point at a third-party backend (Langfuse/Datadog/etc.) — keep strands-agents[otel] and aws-opentelemetry-distro in requirements. Even if you don't have a live backend, write down: why deleting the distro would break this, and why you must NOT also wrap the agent in opentelemetry-instrument when redirecting.
5. Reflect (3 sentences). What did path B make you supply that path A supplied automatically? Where did session.id come from in each case? What would you redact from APPLICATION_LOGS before this ran in production?
Key takeaways
- 1Two paths, same telemetry model: Runtime-hosted agents deployed via the AgentCore CLI are auto-instrumented with OpenTelemetry (baseline traces/sessions/metrics, no extra libs or config); agents outside Runtime you instrument yourself.
- 2Outside Runtime, the recipe is: add `aws-opentelemetry-distro>=0.10.0`, create a log group, set the OTEL env vars (`AGENT_OBSERVABILITY_ENABLED=true`, `OTEL_PYTHON_DISTRO=aws_distro`, `OTEL_PYTHON_CONFIGURATOR=aws_configurator`, `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`, `OTEL_TRACES_EXPORTER=otlp`, ...), and launch with `opentelemetry-instrument python agent.py`.
- 3Auto-captured in both modes: agent invocation sequences, LLM calls/responses, tool invocations/results, error paths, and framework integration (Strands/LangChain/etc.).
- 4You add by hand: custom spans/traces, custom metrics (EMF into the `bedrock-agentcore` namespace), custom logs, and custom attributes/baggage — including `session.id` baggage outside Runtime.
- 5Supported instrumentation libraries: framework-native (Strands/LangChain/CrewAI) plus OpenInference, OpenLLMetry, OpenLIT, and Traceloop — all funneled through OpenTelemetry.
- 6Route a Runtime agent to Datadog/Dynatrace/LangSmith/Langfuse with `DISABLE_ADOT_OBSERVABILITY=true`, but keep `strands-agents[otel]` / `aws-opentelemetry-distro` in requirements and configure your own OTLP destination.
- 7Enable CloudWatch Transaction Search once per account/region before either path; never double-instrument a Runtime agent; and filter PII out of `APPLICATION_LOGS`, which capture full request/response payloads.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.You deploy a Strands agent to AgentCore Runtime using `agentcore deploy` and want baseline traces, sessions, and metrics. What extra observability setup is required in your agent code or image?
2.Your agent runs on EKS, not on Runtime. Which sequence correctly instruments it for AgentCore Observability?
3.Which of these does AgentCore Observability NOT auto-capture, requiring you to add it yourself?
4.Your org standardizes on Langfuse. You want a Runtime-hosted agent's telemetry to flow there instead of into AWS. What do you do?
Go deeper
Hand-picked sources to keep learning
The pillar overview: OTEL-compatible telemetry into CloudWatch, what's captured per resource type.
Authoritative source for the exact ADOT env var keys and the manual instrumentation steps — verify the volatile values here.
The one-time CloudWatch Transaction Search enablement that both instrumentation paths require.
Where spans/logs/metrics land and the runtime-logs vs otel-rt-logs suffix nuance.
Deep dive on auto vs manual instrumentation and routing to third-party backends.
Framework-native OTEL spans via the strands-agents[otel] extra — the most common Runtime framework.