◆ Developer guide

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:

Published metrics — UserID: nakul | SessionID: 826fc1dc-… | TurnID: 3f9c2e1a-… | Status: success | InputTokens: 412 | OutputTokens: 88 | TTFTMs: 640.12 | LatencyMs: 1820.55
UserID / SessionID
Who, and which conversation. Drive Home & the per-user/session dashboards.
TurnID
One value per invocation. Auto-generated if you don't supply one.
Status
Single word, no spaces — success or error.
InputTokens / OutputTokens
Summed across every model call made during the turn.
TTFTMs / LatencyMs
Time-to-first-token and total wall-clock time, in milliseconds.
Getting started

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.

pip install agentcore-dashboard-metrics
Plug and play

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}

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.

Anything else

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,
)
The contract

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 Status is 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.

On the Grafana side

Four dashboards, one log group

Once your agent logs this shape, these dashboards read it directly — no code changes, no per-agent wiring.

OVERVIEW

Home

Top sessions/users by token usage, system error rate, latency at a glance. Start here.

$SessionID

Session-based

Everything that happened in one conversation — invocation rate, TTFT, success ratio, owner.

$UserID

User-based

One user's usage across all sessions — token spend, invocation trend, session list.

$TurnID

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:

Home→user_id User-based→session_id Session-based→turn_id Turn-based
Reference

API

FunctionUse 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).

When a panel is empty

"No data" checklist

  1. Is your agent actually logging? Confirm Published metrics lines are reaching CloudWatch at all.
  2. Does the line match the contract in this doc? Copy one real line out of CloudWatch and run it through the dashboard's parse pattern manually.
  3. Is the time range wide enough? Dashboards default to now-24h → now.
  4. Is the variable value exact? $SessionID/$UserID/$TurnID do exact string matches.