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:| Metric | What it counts |
|---|---|
kiro.daily.credits | Credits used |
kiro.daily.overage_credits | Overage credits used |
kiro.daily.messages | Total messages |
kiro.daily.conversations | Chat conversations |
kiro.daily.model_messages | Messages 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 want | Wrap the metric like this |
|---|---|
| A total over a window | sum_over_time(metric[30d]), which adds every daily point |
| The current state right now | last_over_time(metric[25h]), which reaches back past the 24h gap to the latest day |
| A trend to plot | the 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:
- Pick your destination's protocol, endpoint, and single auth header. Kiro supports OTLP over gRPC (the default) or HTTP with protobuf encoding.
- Create a customer-managed AWS KMS key and grant the
q.amazonaws.comservice principal decrypt access through Secrets Manager. The defaultaws/secretsmanagerkey will not work, because Kiro reads the secret from another account. - 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 lettingq.amazonaws.comcallsecretsmanager:GetSecretValue. - 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 .
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article