English | δΈζ
Real-time analytics for Amazon Bedrock β monitor token usage, costs, and performance across AWS accounts.
β οΈ This sample is for demonstration purposes only and is not intended for production use. Use at your own risk.
- Summary cards: invocations, input/output tokens, cache tokens, estimated cost, avg latency, avg TPOT
- Token usage & cost by model and by caller (chart / pie / table views) with per-token-type cost breakdown (input / output / cache read / cache write)
- Performance: latency by model (min/avg/max) + latency trend with model selector
- TPOT by model (min/avg/max) + TTFT trend from CloudWatch (avg/p99)
- Usage trend over time with paired model/caller filter
- "Data up to" timestamp in header β reflects actual data freshness (L2 checkpoint), not UI refresh time
- Auto refresh (10s / 30s / 1min / 5min)
- Time-aware pricing: correct historical costs even if prices change; separate 5min vs 1h prompt cache rates
- Pricing settings page: view/edit model pricing with history, weekly auto-sync from LiteLLM
- Ad-hoc Athena queries on the raw Iceberg event log for deep investigation
- Multi-account, multi-region support (sidebar selector, friendly account names from
config.yaml) - Login authentication (configurable via
config.yaml) - Responsive layout (desktop & mobile)
Screenshot
βββ deploy/
β βββ cdk.json # CDK config
β βββ app.py # CDK app entry (hub/spoke routing)
β βββ hub_stack.py # Primary account stack
β βββ spoke_stack.py # Spoke account stack
β βββ lambda/
β βββ parse_log.py # L1: S3 event β normalized JSON β Firehose
β βββ compute_cost.py # L2: Athena (Iceberg) β pricing β DynamoDB
β βββ aggregate_stats.py # Rollup: HOURLY β DAILY β MONTHLY
β βββ sync_pricing.py # Weekly pricing sync from LiteLLM
β βββ process_log.py # (legacy V2 path, retained for reference)
βββ webui/
β βββ main.py # Entry point (ui.run)
β βββ dashboard.py # Dashboard page
β βββ pricing.py # Pricing settings page
β βββ data.py # DynamoDB data access
βββ scripts/
β βββ seed_pricing.py # Seed pricing from LiteLLM
βββ config.example.yaml # Multi-account deployment config
βββ deploy.sh # CDK deploy script (hub/spoke/all/destroy)
βββ start-webui.sh # WebUI launch script (reads .env.deploy)
βββ pyproject.toml # Dependencies (managed by uv)
Two-stage pipeline: L1 (parse) turns every Bedrock call into a structured event in an Iceberg table; L2 (compute) rolls those events up by pricing into DynamoDB for the dashboard to read.
ASCII version (for AI/text access)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Primary Account (Hub) β
β β
β S3 logs βββ EventBridge βββ Lambda: parse_log βββ Firehose βββ S3 Tables β
β (Bedrock) [L1: structure only] (60s buffer) (Iceberg) β
β β β
β ββββββββββββββββββββββββββ€ β
β β β β
β βΌ every 5 min β β
β Lambda: compute_cost β β
β [L2: pricing + aggregation] β β
β β β β
β βΌ βΌ β
β DynamoDB: usage-stats Athena: ad-hoc query β
β (serving layer for UI) (via Glue federation) β
β β β
β βΌ β
β WebUI β
β β
β DynamoDB: model-pricing βββ Lambda: sync_pricing (weekly) β
β Lambda: aggregate_stats (HOURLY β DAILY β MONTHLY, daily/monthly) β
β IAM Role: SpokeWriteRole (assumed by spokes to write hub Firehose) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β² assume role + cross-account firehose:PutRecord
β
ββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Spoke Account(s) β
β β
β S3 logs βββ EventBridge βββ Lambda: parse_log βββ Hub Firehose β
β (Bedrock) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
How it works:
- Bedrock logs land in each account's own S3 bucket (Bedrock requires same-account/region sinks).
- L1
parse_log(per-account Lambda, triggered by S3 events) normalizes each record into a flat JSON event (account, region, model, caller, token counts, cache split, latency, error code) and hands it to Hub's Firehose viaPutRecordBatch. Spoke Lambdas assume a cross-account role. - Firehose β S3 Tables: Firehose buffers ~60s then upserts into the Iceberg table
bedrock_analytics.usage_events, usingrequest_idas the unique key β this is the source of truth for every Bedrock call, also directly queryable via Athena for ad-hoc investigation. - L2
compute_cost(hub only, EventBridge every 5 min) reads new events from Iceberg via Athena, looks up time-aware pricing in DynamoDB, computes cost (splitting 5m vs 1h prompt cache), and aggregates into DynamoDB viaTransactWriteItemswith a dedup guard. Separating compute from parse means fixing a pricing bug or changing aggregation logic re-runs L2 on historical events β no re-parsing raw S3. aggregate_statsrolls hourly β daily β monthly on schedule.sync_pricingpulls the latest model prices from LiteLLM weekly.- WebUI reads DynamoDB (sub-second) as a serving-layer cache. The header shows "Data up to X" based on L2's checkpoint, so users can tell real-time data freshness apart from UI refresh time.
- AWS CDK CLI (
npm install -g aws-cdk) - uv (Python package manager)
- AWS credentials configured (
aws configureor~/.aws/credentials)
Copy config.example.yaml to config.yaml and fill in your AWS profiles, regions, and account names. The account marked primary: true deploys the full hub stack (DynamoDB, Iceberg, Firehose, WebUI); others deploy a lightweight spoke that forwards events to the hub.
# Install dependencies
uv sync
# Deploy primary account (auto-bootstraps CDK if needed)
./deploy.sh hub
# Deploy spoke account(s)
./deploy.sh spoke # all spokes
./deploy.sh spoke lab # specific spoke
# Deploy everything (recommended for updates)
./deploy.sh allNote: After code updates, use
./deploy.sh allto ensure both hub and spoke Lambdas are updated.
For existing buckets, enable S3 EventBridge notifications:
aws s3api put-bucket-notification-configuration --bucket YOUR_BUCKET \ --notification-configuration '{"EventBridgeConfiguration": {}}'
Primary account (Hub):
| Resource | Purpose |
|---|---|
| S3 Bucket (optional) | Raw Bedrock invocation logs (encrypted, lifecycle) |
| Custom Resource | Configures Bedrock invocation logging |
| DynamoDB Γ 2 | usage-stats (serving layer + DEDUP + META) and model-pricing (time-aware) |
| S3 Tables bucket + namespace + Iceberg table | usage_events β source of truth, queryable via Athena |
| Glue Data Catalog (federated) | s3tablescatalog pointing at the S3 Tables bucket |
| Lake Formation settings | Registers CDK deploy role as admin (required for pure-IAM access to Iceberg) |
| Firehose delivery stream | S3 Tables destination, 60s buffer, request_id upsert key |
| Athena workgroup | For compute_cost and ad-hoc queries |
| Lambda Γ 6 | parse_log (L1), compute_cost (L2), aggregate_stats, sync_pricing, process_log (legacy), bedrock-invocation-setup (Custom Resource handler) |
| EventBridge Γ 5 | S3 trigger, v3 S3 trigger, L2 schedule, daily & monthly rollup, weekly pricing sync |
| IAM Roles | Firehose delivery role (pre-created by deploy.sh to avoid IAM-propagation race), Lambda execution roles, SpokeWriteRole trusted by spoke accounts |
Spoke accounts:
| Resource | Purpose |
|---|---|
| S3 Bucket (optional) | Raw Bedrock logs |
| Custom Resource | Configures Bedrock invocation logging |
| Lambda Γ 2 | parse_log (L1, assumes hub role to firehose:PutRecord) and process_log (legacy) |
| EventBridge Γ 2 | Active v3 S3 trigger and disabled legacy trigger |
| SQS DLQ | Dead-letter queue for failed processing |
Pricing data is sourced from LiteLLM (286+ Bedrock models):
AWS_DEFAULT_REGION=us-west-2 python3 scripts/seed_pricing.py \
BedrockInvocationAnalytics-model-pricing YOUR_PROFILE./start-webui.shOpen http://localhost:8060 in your browser.
./deploy.sh destroy # destroy hub stackDynamoDB tables and S3 bucket are retained after stack deletion (RemovalPolicy: RETAIN).
| Service | Pricing | Notes |
|---|---|---|
| Lambda | $0.20/M requests (ARM/Graviton) | L1 parse_log + L2 compute_cost + rollups |
| Firehose | $0.029/GB ingested + small per-record fee | Buffered then Iceberg upsert |
| S3 Tables (Iceberg) | ~$0.023/GB + $0.20/M requests | Partitioned by account_id / year / month / day |
| DynamoDB | Pay-per-request | Now only L2 aggregates write (β13Γ fewer writes than V2 since L1 skips DDB) |
| Athena | $5/TB scanned | L2 scans one partition per run; ad-hoc queries extra |
| S3 (raw logs) | ~$0.023/GB/month | Auto-transitions to IA after 90 days |
Monthly estimate (1M Bedrock invocations, Anthropic models with caching):
- Lambda: ~$0.20 (L1) + ~$0.05 (L2, every 5 min)
- Firehose + S3 Tables: ~$1 (few hundred MB of Iceberg data)
- DynamoDB: ~$0.30 (L2 TransactWriteItems, ~3 items/event)
- Athena: ~$0.10 (L2 scans small hourly partitions; ad-hoc extra)
- S3 (raw logs): ~$1
- Total: ~$3/month
Costs scale sub-linearly with invocations β Firehose buffering amortizes per-record overhead, and Iceberg partition pruning keeps Athena scans small as history grows.
See CONTRIBUTING for more information.
This library is licensed under the MIT-0 License. See the LICENSE file.

