AWS Builder Center

Turn Kiro's usage telemetry into answers, not just numbers

Kiro can export per-user usage metrics to any OpenTelemetry backend. This post is about the part that happens after the export lands.

Kiro can export per-user usage metrics to any OpenTelemetry backend. This post is about the part that happens after the export lands. You get five raw daily counters. On their own they are just numbers. The work is turning them into the answers an engineering org actually asks: what are we getting for the spend, who has adopted this and who has gone quiet, and who is about to hit their credit cap.
Turning on the export takes a few minutes. The value is in the queries you build on top of it.

What Kiro gives you

Since September 2026, a Kiro account administrator can send daily usage metrics to an OpenTelemetry-compatible backend such as Amazon CloudWatch, Datadog, Dynatrace, Elastic, Honeycomb, OpenSearch, or a self-hosted collector. Kiro delivers the metrics server-side, once a day at 02:00 UTC. Developers install nothing and configure nothing.
You get five metrics, all in the kiro.daily.* namespace:
MetricWhat it counts
kiro.daily.creditsCredits used
kiro.daily.overage_creditsOverage credits used
kiro.daily.messagesTotal messages
kiro.daily.conversationsChat conversations
kiro.daily.model_messagesMessages per model (carries kiro.model.name)
Each datapoint carries attributes you can group and filter on. The user comes through as kiro.user.id, and kiro.user.email when it resolves. The client is kiro.client.type, one of KIRO_IDE, KIRO_CLI, or KIRO_WEB. You also get kiro.user.new, kiro.subscription.tier, kiro.usage.limit, kiro.overage.enabled, kiro.overage.cap, and the activity date. Every export stamps a resource attribute service.name = kiro-enterprise, which is how you isolate Kiro data from everything else in your backend.
That is the raw material. Five counters and a handful of labels. Nobody makes a renewal decision by staring at a credits counter, so the rest of this post is the analytical layer.

Two facts about the data that change how you query it

Two properties of the feed decide whether your numbers are right.
The first is that the cadence is daily and the temporality is delta. Kiro sends one batch per day, and each datapoint is that day's own value, not a running total. A "total to date" is therefore a sum across daily points, not the latest reading of a counter. Get this wrong and your dashboards either flatline or double.
The second is that there is no backfill. Kiro retries a failed export up to three times for a given activity date. If all three fail, that day is gone. Kiro does not resend it later, and it does not deduplicate if a date somehow exports twice. Treat a missing day as missing, and read any "total" as the total of the days that made it.
A short mental model for writing the queries:
You wantWrap the metric like this
A total over a windowsum_over_time(metric[30d]), which adds every daily point
The current state right nowlast_over_time(metric[25h]), which reaches back past the 24h gap to the latest day
A trend to plotthe raw metric, no wrapping
The [30d] window is a placeholder. Set it to the range you care about. The [25h] window is deliberate, not a typo. It is just wider than the 24-hour gap between exports, so a "latest state" query always catches the most recent day.
The examples below are plain PromQL, which CloudWatch's Metrics V2 store and any Prometheus-compatible backend understand. Prometheus backends rename the metrics. Dots become underscores and counters gain a _total suffix, so kiro.daily.credits becomes kiro_daily_credits_total. The labels follow the same rule, so kiro.user.email becomes kiro_user_email.

Question 1: what are we getting for the spend?

The credits counter is the cost signal. On its own it is a number. Divided by the right denominator, it becomes efficiency.
Start with the raw total for the org:
1
sum(sum_over_time(kiro_daily_credits_total[30d]))
Break it down by who is spending, so you can see whether cost is concentrated or spread out:
1
topk(10, sum by (kiro_user_email) (sum_over_time(kiro_daily_credits_total[30d])))
Break it down by model, which is usually where the cost actually goes:
1
sum by (kiro_model_name) (sum_over_time(kiro_daily_credits_total[30d]))
Now the number that matters more than the total. Cost per active user tells you whether spend tracks with usage or with headcount:
1
2
3
sum(sum_over_time(kiro_daily_credits_total[30d]))
/
count(count by (kiro_user_email) (sum_over_time(kiro_daily_messages_total[30d]) > 0))
Cost per message is your credit-efficiency baseline over time:
1
2
3
sum(sum_over_time(kiro_daily_credits_total[30d]))
/
sum(sum_over_time(kiro_daily_messages_total[30d]))
Track cost per message per model and you can see which models earn their credits:
1
2
3
sum by (kiro_model_name) (sum_over_time(kiro_daily_credits_total[30d]))
/
sum by (kiro_model_name) (sum_over_time(kiro_daily_model_messages_total[30d]))
Read together, these tell a story a raw total cannot. A high total means little if cost per active user is flat and cost per message is dropping. That is a tool being used more efficiently, not a budget problem.

Question 2: who has actually adopted it?

A seat count is what you bought. Active users are what you got. The messages counter, grouped by user and thresholded, turns a license number into an adoption curve.
Daily, weekly, and monthly active users come from the same shape with different windows. DAU uses last_over_time because "today" is a single snapshot, while the weekly and monthly figures sum across their windows:
1
count(count by (kiro_user_email) (last_over_time(kiro_daily_messages_total[25h]) > 0))
1
count(count by (kiro_user_email) (sum_over_time(kiro_daily_messages_total[7d]) > 0))
1
count(count by (kiro_user_email) (sum_over_time(kiro_daily_messages_total[30d]) > 0))
An average hides the two cohorts you care about. Power users have found real value:
1
count(count by (kiro_user_email) (sum_over_time(kiro_daily_messages_total[30d]) > 250))
At-risk users signed in, sent a handful of messages, and drifted. An enablement nudge can still bring them back:
1
2
3
4
5
6
count(
count by (kiro_user_email) (
sum_over_time(kiro_daily_messages_total[30d]) > 0
and sum_over_time(kiro_daily_messages_total[30d]) < 20
)
)
Tune those thresholds, 250 and 20, to your own daily volumes. They are starting points, not universals. Plot adoption by client type as a raw trend to see whether people land in the IDE, the CLI, or the plugin:
1
sum by (kiro_client_type) (kiro_daily_messages_total)

Question 3: how deeply are people using it?

Message and conversation counts on their own are volume. Divide one by the other and you get depth, which is a better proxy for whether the tool has become part of the workflow or is just being poked at:
1
2
3
sum(sum_over_time(kiro_daily_messages_total[30d]))
/
sum(sum_over_time(kiro_daily_conversations_total[30d]))
A rising messages-per-conversation number means people are having longer, more involved sessions rather than one-shot queries. Pair it with model mix to see where that depth is going:
1
2
3
100 * sum by (kiro_model_name) (sum_over_time(kiro_daily_model_messages_total[30d]))
/ ignoring(kiro_model_name) group_left
sum(sum_over_time(kiro_daily_model_messages_total[30d]))

Question 4: who is about to hit a wall?

Overage is the one signal where the current state matters more than the total, because a hard cap stops work. This is where last_over_time earns its place. Total overage over a window is a finance question:
1
sum(sum_over_time(kiro_daily_overage_credits_total[30d]))
But "who is close to their cap right now" is an operational one, and you want the latest snapshot, not a historical sum:
1
2
3
4
count(
last_over_time(kiro_daily_overage_credits_total[25h])
/ last_over_time(kiro_overage_cap[25h]) > 0.8
)
That query counts users past 80 percent of their configured cap. It is the difference between learning someone is blocked from a support ticket and seeing it on a dashboard the day before.

Filtering to one person

Every query above works for a single user by adding a label matcher. This is useful when someone asks what their team's spend has been, or when you are debugging one account:
1
sum(sum_over_time(kiro_daily_credits_total{kiro_user_email="alex@example.com"}[30d]))

Setting up the export

The analytical payoff needs the pipe in place first. An administrator does this once, in the Kiro console. The full walkthrough is in the Kiro docs. The shape of it:
  1. Pick your destination's protocol, endpoint, and single auth header. Kiro supports OTLP over gRPC (the default) or HTTP with protobuf encoding.
  2. Create a customer-managed AWS KMS key and grant the q.amazonaws.com service principal decrypt access through Secrets Manager. The default aws/secretsmanager key will not work, because Kiro reads the secret from another account.
  3. Store the endpoint and auth header in an AWS Secrets Manager secret with exactly two keys:json { "OTEL_EXPORTER_OTLP_ENDPOINT": "https://<collector-host>:443", "OTEL_EXPORTER_OTLP_HEADERS": "<Header-Name>=<token>" } Add a resource policy letting q.amazonaws.com call secretsmanager:GetSecretValue.
  4. In the Kiro console, go to Settings, then Kiro user activity reports, and Edit. Choose the OpenTelemetry format, paste the secret ARN, pick the protocol, and Enable.
Three things trip people up. The KMS key, the secret, and the Kiro profile must all be in the same AWS Region. The endpoint must be reachable from the public internet with a publicly trusted TLS certificate, so no self-signed certs, and use a hostname rather than an IP. And the protocol you pick has to match what your endpoint speaks. gRPC pointed at an HTTP-only endpoint fails silently.
Amazon CloudWatch has one extra wrinkle worth knowing before you pick it. Kiro cannot sign the export with SigV4, so CloudWatch needs a bearer API key, and the metrics land in the Metrics V2 store. You query them with PromQL. They do not appear in the classic metrics console or through ListMetrics.

Confirming it works

Kiro sends no success or failure notification, so verify at the destination. Query for kiro.daily.credits filtered on service.name = kiro-enterprise and expect one datapoint per active user and client type for the previous day, with one datapoint per model on kiro.daily.model_messages. One trap to know: Kiro counts a request the collector accepts as successful even if the collector later rejects the datapoints. Confirm the data is actually queryable, not just that the request went out.

The takeaway

The export is plumbing. Any team can turn it on in an afternoon. The difference between a dashboard people ignore and one people act on is the layer you build on top. Divide credits by active users instead of reading the raw counter. Split adoption into power and at-risk cohorts instead of reporting an average. Watch cap utilization as a live state instead of a monthly total. The five counters are the same for everyone. The questions you ask them are yours.

Configuration details in this post come from the Kiro OpenTelemetry exports changelog  and the Kiro enterprise OpenTelemetry documentation .
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article