Monitor

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 });
ExporterStrategy factoryShips to
AgentCore ObservabilityagentcoreObservability()CloudWatch Logs, GenAI Observability schema
OpenTelemetryotelObservability()any OTLP backend (Collector → X-Ray / AgentCore / Honeycomb / …)
CloudWatch (generic)cloudwatchObservability()CloudWatch Logs
X-RayxrayObservability()AWS X-Ray traces
Audit bundleauditExport() + 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 for otelObservability when 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 exporters

2. 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 asc

The 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-http

2. 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 compliance

Or 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

On this page