Stop hand-formatting log lines
agentcore-dashboard-metrics is a small Python package that
logs exactly the metrics line your Grafana dashboards need — for agents on
Bedrock AgentCore built with LangGraph, Strands, or CrewAI.
The problem it solves
CloudWatch Logs Insights' parse command is a literal,
positional string matcher — it has no notion of a "field," only fixed text tokens in a
fixed order. A dashboard panel built to read a specific shape returns a silent
"No data" the moment any agent's log line drifts from it: a swapped
field, a quoted number, one extra space. No error, no warning — just an empty chart.
This is the exact string every dashboard panel expects, and the one this package emits for you on every turn:
success or error.Install
Zero required runtime dependencies — the framework wrappers are fully duck-typed
against whatever graph / agent /
crew object you pass them, so installing this package never
pulls in LangChain, Strands, or CrewAI on your behalf.
Quickstart, per framework
Pick your agent's framework. Each call replaces an entire streaming loop, TTFT/latency timing, and token accumulation — plus the log line itself.
from agentcore_dashboard_metrics import run_agent_turn async def invoke(payload, context): # ...build graph, messages... output, turn_id = await run_agent_turn( graph, messages, log, user_id=user_id, session_id=session_id, ) return {"result": output, "turn_id": turn_id}
from agentcore_dashboard_metrics import run_strands_turn async def invoke(payload, context): agent = get_or_create_agent() output, turn_id = await run_strands_turn( agent, payload.get("prompt"), log, user_id=user_id, session_id=session_id, ) return {"result": output, "turn_id": turn_id}
from agentcore_dashboard_metrics import run_crewai_turn async def invoke(payload, context): crew = get_or_create_crew() output, turn_id = await run_crewai_turn( crew, {"prompt": payload.get("prompt")}, log, user_id=user_id, session_id=session_id, ) return {"result": output, "turn_id": turn_id}
What happens under the hood: streams the agent, marks the first
output chunk for TTFT, sums token usage across however many model calls the turn makes,
sets status to success or error, and emits the log line —
on an exception, the line is still logged with status="error"
and the exception is re-raised unchanged.
Strands multi-agent, CrewAI Flows, or a custom loop
Drop to the framework-agnostic context manager. It handles timing and the log line; you just report usage as you get it.
from agentcore_dashboard_metrics import TurnMetrics with TurnMetrics(log, user_id=user_id, session_id=session_id) as m: result = my_own_agent_call(...) m.add_usage(input_tokens=result.input_tokens, output_tokens=result.output_tokens) m.mark_first_token() # optional — only if you stream # the log line is emitted automatically on exit, including on # an exception (status is forced to "error"; nothing is swallowed)
Already computed everything yourself? Skip straight to the formatter:
from agentcore_dashboard_metrics import log_turn_metrics log_turn_metrics( log, user_id=user_id, session_id=session_id, turn_id=turn_id, status="success", input_tokens=412, output_tokens=88, ttft_ms=640.12, latency_ms=1820.55, )
Rules that keep every panel populated
The package enforces these for you — sanitizing bad input rather than corrupting the line — but it's worth knowing what it's protecting against if you ever log this by hand.
- 01 Line starts with
Published metrics —(em dash, not a hyphen) — every panel filters on it. - 02 Fields stay in order: UserID → SessionID → TurnID → Status → InputTokens → OutputTokens → TTFTMs → LatencyMs.
- 03 Separator is exactly
" | "— space, pipe, space. - 04 Numbers are bare and unquoted —
stats sum()/avg()need real numerics. - 05
Statusis one lowercase word, no spaces or pipes. - 06 Exactly one line per turn — double-logging breaks every
count()panel. - 07 IDs are always non-empty, stable strings — they double as click-through link values.
TTFT is honest, not universal. ttft_ms
is only meaningful when the framework streams individual chunks. CrewAI's common
non-streaming kickoff_async call has no token-level stream
to time, so run_crewai_turn reports -1
there — the same convention used for any turn with no streamed output.
Four dashboards, one log group
Once your agent logs this shape, these dashboards read it directly — no code changes, no per-agent wiring.
Home
Top sessions/users by token usage, system error rate, latency at a glance. Start here.
Session-based
Everything that happened in one conversation — invocation rate, TTFT, success ratio, owner.
User-based
One user's usage across all sessions — token spend, invocation trend, session list.
Turn-based
The exact detail of one exchange — latency, TTFT, tokens, status — plus a browsable turn table.
Click-through navigation
Every ID rendered as a link jumps to the related dashboard with that value pre-filled and the time range carried over:
API
| Function | Use for |
|---|---|
run_agent_turn(graph, messages, log, *, user_id, session_id, turn_id=None) | LangGraph create_react_agent |
run_strands_turn(agent, prompt, log, *, user_id, session_id, turn_id=None) | Strands Agent |
run_crewai_turn(crew, inputs, log, *, user_id, session_id, turn_id=None) | CrewAI Crew |
TurnMetrics(log, *, user_id, session_id, turn_id=None) | Context manager for anything else |
log_turn_metrics(log, *, user_id, session_id, turn_id, status, input_tokens, output_tokens, ttft_ms, latency_ms) | Raw formatter |
new_turn_id() | Generates a fresh turn id |
All three run_*_turn() functions
are async def and return (output_text,
turn_id).
"No data" checklist
- Is your agent actually logging? Confirm
Published metricslines are reaching CloudWatch at all. - Does the line match the contract in this doc? Copy one real line out of CloudWatch and run it through the dashboard's
parsepattern manually. - Is the time range wide enough? Dashboards default to
now-24h→now. - Is the variable value exact?
$SessionID/$UserID/$TurnIDdo exact string matches.