Exporters: AgentCore & OTEL
Step-by-step — ship the agent's typed event trace to AWS AgentCore Observability (CloudWatch GenAI) and to OpenTelemetry. Observability is a port; pick an exporter strategy and mount it with agent.enable.observability().
agentfootprint emits a typed AgentfootprintEvent stream for every run. Where that stream
goes is a port — you pick an exporter strategy and mount it. The agent
doesn't change; only the destination does.
const stop = agent.enable.observability({ strategy /* , detach */ });
// … run the agent …
stop(); // unsubscribe (the call returns a stop fn)Multi-exporter is just multiple calls — each subscribes independently:
agent.enable.observability({ strategy: agentcore });
agent.enable.observability({ strategy: otel });| Exporter | Strategy factory | Ships to |
|---|---|---|
| AgentCore Observability | agentcoreObservability() | CloudWatch Logs, GenAI Observability schema |
| OpenTelemetry | otelObservability() | any OTLP backend (Collector → X-Ray / AgentCore / Honeycomb / …) |
| CloudWatch (generic) | cloudwatchObservability() | CloudWatch Logs |
| X-Ray | xrayObservability() | AWS X-Ray traces |
| Audit bundle | auditExport() + verifyAuditBundle() | tamper-evident hash-chained file |
Which exporter? If you are on AWS, start with
xrayObservability— one optional peer dependency, one IAM permission (xray:PutTraceSegments), a region, and no collector to run. Reach forotelObservabilitywhen the destination is not AWS (Honeycomb, Grafana, Datadog) or when you already run an OTLP collector — it is backend-agnostic precisely because it exports nothing itself.
All ship from agentfootprint/observe. This is the Monitoring-side how-to;
for the full AgentCore picture see AWS Bedrock AgentCore and the
adapter pattern; for the event taxonomy + recorders see
Observability.
Keep the loop unblocked
Exporters that do network I/O (CloudWatch, OTLP) should be detached so a slow backend
never stalls the agent: pass detach: { driver: microtaskBatchDriver, mode: 'forget' }, and
drain on shutdown with await handle.flush() — or await agent.shutdown(), which drains and
releases everything enabled on the agent (8.12.0).
Connect AWS AgentCore Observability
Ships the trace to CloudWatch Logs in AgentCore's GenAI Observability schema (the dashboard + X-Ray correlation light up from there).
1. Install the peer
npm install @aws-sdk/client-cloudwatch-logs # optional peer for the CloudWatch exporters2. Mount the strategy
import { agentcoreObservability } from 'agentfootprint/observe';
import { microtaskBatchDriver } from 'footprintjs/detach';
const agentcore = agentcoreObservability({
region: 'us-west-2',
logGroupName: '/agentfootprint/workflow-assistant',
logStreamName: `${process.env.HOSTNAME ?? 'local'}/${Date.now()}`,
});
const stop = agent.enable.observability({
strategy: agentcore,
detach: { driver: microtaskBatchDriver, mode: 'forget' }, // non-blocking
});3. See it
The run's events land in the log group; the CloudWatch GenAI Observability dashboard
renders the agent/tool/LLM spans over them. The log group must exist (or the role must allow
logs:CreateLogGroup) — provisioning is control-plane (CDK/SDK), same as the rest of the
AgentCore setup. As of 8.11.0 the log stream is created for you on
first delivery (the adapters call CreateLogStream, so the role needs logs:CreateLogStream);
only the group is the operator's job.
Which CONVERSATION an event belongs to — meta.sessionId (9.4.0)
Every shipped event carries meta.runId, and a runId is per run() /
resume(). A session outlives both, so a log group could answer "what
happened in this run?" and not "what happened in this conversation?" — which
is the question a session-oriented host like AgentCore is built around, and one
you cannot reconstruct from the events afterwards.
standingAgent now threads the caller's own session id onto every event the
run emits:
// nothing to configure — the host already knows the session
await standingAgent({ agent, sessions: agentCoreSessions({ store: 'memory', memoryId }), host });fields @timestamp, meta.sessionId, type
| filter meta.sessionId = 'conv-77'
| sort @timestamp ascThe CloudWatch and AgentCore adapters serialize the whole envelope, so this
arrives without their knowing it exists. Outside a host, set it yourself with
agent.run(input, { sessionId }).
Absent when there is no session, never invented. An anonymous request has
none; a bare agent.run() has none. Neither gets a fabricated id, and neither
gets the runId wearing a session's name — so meta.sessionId being present is
itself evidence the run really was session-bound.
Connect OpenTelemetry
otelObservability() turns the event stream into a GenAI span tree on a tracer you
provide (so you control the OTLP exporter + backend — Collector, X-Ray, AgentCore
Observability's OTEL ingestion, Honeycomb, …).
1. Install OTEL
npm install @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/exporter-trace-otlp-http2. Stand up a tracer (once, at process start)
import { NodeTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { trace } from '@opentelemetry/api';
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter({ url: process.env.OTLP_ENDPOINT })));
provider.register();
const tracer = trace.getTracer('workflow-assistant');3. Mount the strategy
import { otelObservability } from 'agentfootprint/observe';
const otel = otelObservability({
serviceName: 'workflow-assistant',
tracer,
genAiSpanNames: true, // spec span names ('invoke_agent …', 'chat …', 'execute_tool …') + gen_ai.* attributes
});
const stop = agent.enable.observability({ strategy: otel });Spans then flow wherever your OTLP pipeline points — including AWS X-Ray and AgentCore Observability, both of which ingest OTEL.
Both at once
You don't have to choose — attach as many exporters as you want; each is independent:
agent.enable.observability({ strategy: agentcore, detach: { driver: microtaskBatchDriver, mode: 'forget' } });
agent.enable.observability({ strategy: otel });
agent.enable.observability({ strategy: auditExport({ /* … */ }) }); // tamper-evident copy for complianceOr fan out from a single subscription with composeObservability, which takes one array
(no varargs, no options object) and returns a strategy whose exportEvent, flush and stop
all fan out to every child:
import {
composeObservability,
xrayObservability,
cloudwatchObservability,
} from 'agentfootprint/observe';
import { microtaskBatchDriver } from 'footprintjs/detach';
const telemetry = composeObservability([
xrayObservability({ region: 'us-east-1', serviceName: 'workflow-assistant' }),
cloudwatchObservability({ region: 'us-east-1', logGroupName: '/agentfootprint/workflow-assistant' }),
]);
const handle = agent.enable.observability({
strategy: telemetry,
detach: { driver: microtaskBatchDriver, mode: 'forget' },
});
// 8.12.0 — the handle IS the unsubscribe, and it drains in the right order:
// detached queue, then every composed child's buffer, then release.
process.on('SIGTERM', async () => { await agent.shutdown(); });Errors are isolated per child — one bad exporter doesn't stop the others. (composeObservability
and the vendor adapters all come from agentfootprint/observe. Through 8.x they were also
reachable at agentfootprint/strategies and agentfootprint/observability-providers; 9.0.0
removed both of those paths.)
Wrapping or debugging a strategy
The hot path is exportEvent, not onEvent (doc comments through 8.10.0 said onEvent, so
your IDE may still show it on an older install — there has never been a method by that name; the
code is the truth). If you wrap a strategy, always spread the original:
{ ...inner, exportEvent: (e) => { log(e); inner.exportEvent(e); } }. A rewrap that lists only
name and exportEvent drops flush and stop, and the batching exporters (CloudWatch, X-Ray)
then never drain their last batch and never clear their timer. It also drops
relevantEventTypes, which makes your wrapper receive every event the adapter had declared it
didn't want. Note the empty array is not "no filter": relevantEventTypes: [] delivers zero
events, silently — omit the field entirely to receive everything. capabilities affects nothing
about delivery; the attach filter never reads it. Delivery errors are surfaced through
_onError, which is a property you assign after construction (or, on the AWS adapters since
8.11.0, an onError option in the factory). Since 8.12.0 the framework DOES call flush() and
stop() — through the handle, agent.shutdown(), and a closing standingAgent — which is
another reason a rewrap must spread the original: a wrapper that drops flush makes every one of
those doors a no-op.
Next steps
- Observability — the typed event taxonomy + recorders the exporters consume.
- AWS Bedrock AgentCore · AgentCore: step by step — the full AgentCore integration (this is Step 7 of it).
- Ports & adapters — why "where the trace goes" is a one-line swap.
Observability
Typed event streams, recorders, and tier-3 enable.* helpers — observe what the agent did without shaping what it does. 59 events emitted during DFS traversal, no instrumentation.
Context engineering recorder
Filter the firehose of context.injected events into engineered (RAG / Skills / Memory / Instructions / Steering / Facts) vs baseline (user / tool-result / assistant). The first-class handle on what your context engineering is actually doing.
