Skip to content

Cloud Action Costs Dashboard - #81

Open
lainecsmith wants to merge 9 commits into
masterfrom
lainecsmith/cloud-action-costs
Open

Cloud Action Costs Dashboard#81
lainecsmith wants to merge 9 commits into
masterfrom
lainecsmith/cloud-action-costs

Conversation

@lainecsmith

Copy link
Copy Markdown

What was changed

Adds a new community Grafana dashboard for Temporal Cloud, sourced entirely from the OpenMetrics endpoint, plus a companion alert rules file:

temporal_cloud_action_costs.json — dashboard with two sections:

  • Summary: 1-day / 7-day / month-to-date Action totals.
  • Action Spike Detection: current Action rate vs. the same time 24h and 7d ago, percent-change indicators, a breakdown by Workflow Type and Action Type, and a ranked table of top Workflow Types by Action volume.

temporal_cloud_action_costs_alerts.yaml — optional Grafana unified-alerting provisioning file with 2 rules (action rate spike vs 24h ago, vs 7d ago).

Scope is intentionally limited to Action volume — Temporal Cloud doesn't expose price or credit-ledger balance via metrics, so this dashboard doesn't show cost in dollars or track spend against a commitment. That's called out explicitly in the dashboard's intro panel, with a pointer to Billing Center / the Billing API for anyone who needs authoritative $ figures.

Why?

Two of the most common cost-support conversations with customers are (1) someone ships a change or a new workflow that quietly drives a lot more Actions than expected, and nobody notices until the bill, and (2) there's no proactive signal — you only find out by going and looking. This gives customers a self-serve starting point for both: a dashboard to see it, and alert rules so it can page/notify instead of requiring someone to be staring at Grafana when it happens.

Checklist

How was this tested:

  • Created a Temporal Cloud Service Account with the Metrics Read-Only role and generated an API key; verified the OpenMetrics endpoint directly with curl before touching Grafana.
  • Ran Prometheus in a homelab Docker Compose stack, added a scrape job against metrics.temporal.io with Bearer auth, and confirmed the temporal-cloud job showed UP in Prometheus's own Targets page.
  • Imported the dashboard into a homelab Grafana instance against that Prometheus data source; confirmed the temporal_namespace variable populates from label_values(), and confirmed (separately, via a direct endpoint query) that namespaces without recent Action traffic simply don't appear — expected behavior given OpenMetrics' sparse reporting, not a bug — and added a note about this to the dashboard's intro panel so it's not mistaken for one.
  • Verified the account has 85 namespaces total but only 2 have recent traffic; confirmed the dashboard's namespace variable correctly surfaces those 2 and that switching to "All" sums correctly against real data.
  • Sanity-checked the Actions MTD figure against the Cloud UI's own Usage Dashboard for the same namespace/timeframe — same order of magnitude (exact match not expected; different counting methodology, called out in the docs).

Any docs updates needed?
Updated cloud/README.md in this PR with a link to the new dashboard the alerts.
No docs.temporal.io changes made as part of this PR.

@lainecsmith
lainecsmith requested a review from a team as a code owner July 23, 2026 15:24
Comment thread cloud/temporal_cloud_action_costs.json Outdated
"uid": "${datasource}",
"type": "prometheus"
},
"description": "Percent change in current action rate vs the same time yesterday. Large positive swings flag an unannounced usage spike. Paired with an alert rule of the same name in temporal_cloud_alert_rules.yaml.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this file temporal_cloud_alert_rules.yaml exist?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, except I renamed it and forgot to update here! Fixed, thanks!

Comment thread cloud/temporal_cloud_action_costs.json Outdated
"sortBy": [
{
"desc": true,
"displayName": "Actions/sec"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be Estimated Actions?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, changed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new community Grafana dashboard (plus optional unified-alerting provisioning) to help Temporal Cloud users monitor Action volume month-to-date and detect sudden Action-rate spikes using the OpenMetrics endpoint.

Changes:

  • Added a new Grafana dashboard for Action MTD totals and spike-detection breakdowns.
  • Added Grafana unified-alerting provisioning rules to alert on action-rate spikes vs 24h/7d baselines.
  • Updated cloud README to link the new dashboard and sample alerts.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
cloud/temporal_cloud_action_costs.json New Grafana dashboard for Action MTD totals, spike comparisons vs 24h/7d ago, and workflow/action-type breakdowns.
cloud/temporal_cloud_action_costs_alerts.yaml New Grafana unified-alerting provisioning file with two spike-detection alert rules.
cloud/README.md Adds links to the new dashboard and sample alert rules.
Suppressed comments (3)

cloud/temporal_cloud_action_costs.json:206

  • This panel description says "billable" actions, but the query uses temporal_cloud_v1_total_action_count (not ..._billable_action_count). Either switch the query to the billable metric or adjust the description to avoid implying it is billable.
      "description": "Estimated total billable actions over the last 7 days",

cloud/temporal_cloud_action_costs.json:614

  • This percent-change expression can divide by zero when the offset value is 0 (or missing), producing +Inf/NaN in the Stat panel and making the paired alerting behavior noisy. Clamp the denominator to a tiny positive value to keep the expression finite.
          "expr": "(sum(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"}) - sum(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"} offset 7d)) / sum(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"} offset 7d) * 100",

cloud/temporal_cloud_action_costs_alerts.yaml:115

  • This alert expression can divide by zero when the offset value is 0 (or missing), producing +Inf/NaN and making the alert noisy. Clamp the denominator to a tiny positive value to keep the evaluation finite.
                (sum(temporal_cloud_v1_total_action_count{temporal_namespace=~".*"})
                - sum(temporal_cloud_v1_total_action_count{temporal_namespace=~".*"} offset 7d))
                / sum(temporal_cloud_v1_total_action_count{temporal_namespace=~".*"} offset 7d) * 100

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cloud/temporal_cloud_action_costs.json Outdated
Comment thread cloud/temporal_cloud_action_costs.json
Comment thread cloud/temporal_cloud_action_costs.json Outdated
Comment thread cloud/temporal_cloud_action_costs.json
Comment thread cloud/temporal_cloud_action_costs.json Outdated
Comment thread cloud/temporal_cloud_action_costs_alerts.yaml Outdated
Comment thread cloud/temporal_cloud_action_costs_alerts.yaml Outdated
lainecsmith and others added 6 commits August 11, 2026 14:45
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
"type": "prometheus"
},
"editorMode": "code",
"expr": "sum(avg_over_time(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"}[1d]) * 86400)",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from an AI review: avg_over_time divides by samples present, not minutes elapsed, and Cloud omits datapoints for idle minutes — so totals over-report by 1/duty_cycle, always upward, worst on the least-active namespaces.

Measured against known truth: a namespace reporting 1h/day reads 8,640,000 vs a true 360,000 (24x); account total +53%.

The sibling's sum_over_time(m[1d])/1440*86400 is exact at 60s scrape but double-counts at 30s (Temporal's recommended interval), so don't copy that either. This is correct at both:

sum(sum_over_time(temporal_cloud_v1_total_action_count{temporal_namespace=~"$temporal_namespace"}[1d:1m])) * 60

Same fix on lines 261, 331, 962.

"type": "prometheus"
},
"editorMode": "code",
"expr": "topk(20, sum by (temporal_workflow_type, temporal_namespace) (avg_over_time(temporal_cloud_v1_billable_action_count{temporal_namespace=~\"$temporal_namespace\"}[$__range]) * $__range_s))",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from an AI agent: Same avg_over_time bug, but here it inverts the ranking — a workflow type reporting 1h/day is scored as if it ran all week. On my data the true #4 (2,520,000) displays as #1 (60,480,000), above the real leader (54,432,000).

topk(20, sum by (temporal_workflow_type, temporal_namespace) (
  sum_over_time(temporal_cloud_v1_billable_action_count{temporal_namespace=~"$temporal_namespace"}[$__range:1m])
) * 60)

Also: the footer sums only the top-20 rows but reads as a total.

"type": "prometheus"
},
"editorMode": "code",
"expr": "(sum(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"}) - sum(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"} offset 1d)) / clamp_min(sum(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"} offset 1d), 1e-9) * 100",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from an ai agent: clamp_min doesn't fix this — the denominator is absent, not zero, and clamp_min can't create a missing sample. Verified: on a namespace with no data 24h ago it still returns an empty vector, so the tile is blank.

(Scoped to All it's fine — the account denominator is present. This bites once the variable is narrowed to the new namespace.)

or vector(0) works (returns 5,000 on the same data):

(sum(M) - (sum(M offset 1d) or vector(0)))
  / clamp_min(sum(M offset 1d) or vector(0), 1) * 100
# M = temporal_cloud_v1_total_action_count{temporal_namespace=~"$temporal_namespace"}

Also 1e-9 as a floor yields 5e13% where it does apply; 1 gives 50,000%.

"type": "prometheus"
},
"editorMode": "code",
"expr": "(sum(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"}) - sum(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"} offset 7d)) / sum(temporal_cloud_v1_total_action_count{temporal_namespace=~\"$temporal_namespace\"} offset 7d) * 100",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from an AI agent: clamp_min from line 540 wasn't applied here, so the 24h and 7d panels now differ — same for the two alert rules (alerts.yaml:64 vs :115). Whatever form you pick should land on all four.

]
},
"time": {
"from": "now-7d",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from an AI agent: Default range is now-7d and this panel uses $__range, so "Actions Spent This Month (MTD)" and "7 Day Actions" render the same number. Suggest "time": {"from": "now/M", "to": "now"}.

Related: the intro text and this panel's description say to use "This month", but in Grafana that's now/M -> now/Mto rounds up to month end, so $__range_s is a full 31 days regardless of the date (~3x inflation mid-month). You want "This month so far".

{
"__inputs": [
{
"name": "DS_PROMETHEUS",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DS_PROMETHEUS is declared here. But I don't see any references anywhere. Is it needed?

Comment on lines +62 to +64
(sum(temporal_cloud_v1_total_action_count{temporal_namespace=~".*"})
- sum(temporal_cloud_v1_total_action_count{temporal_namespace=~".*"} offset 1d))
/ clamp_min(sum(temporal_cloud_v1_total_action_count{temporal_namespace=~".*"} offset 1d), 1e-9) * 100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From an AI agent: Bare sum() with no by (temporal_namespace) collapses the account to one number (=~".*" is a no-op matcher). NS-A steady at 10,000/sec plus NS-B jumping 10 -> 500/sec (~42M extra actions/day) = 4.9% account-wide, under the 50 threshold. Per-namespace that's 4900%.

by (temporal_namespace) gives one instance per namespace. Two caveats: or vector(0) won't survive it (no namespace label — leaves a stray {} = 0), and a brand-new namespace drops out of the result entirely, so cold start needs its own rule:

sum by (temporal_namespace) (temporal_cloud_v1_total_action_count) > 5
unless
sum by (temporal_namespace) (temporal_cloud_v1_total_action_count offset 1d)

That will also fire for an irregular namespace idle at that exact minute yesterday, so a windowed baseline may be better. See :540 re the clamp_min here.

Comment on lines +113 to +115
(sum(temporal_cloud_v1_total_action_count{temporal_namespace=~".*"})
- sum(temporal_cloud_v1_total_action_count{temporal_namespace=~".*"} offset 7d))
/ sum(temporal_cloud_v1_total_action_count{temporal_namespace=~".*"} offset 7d) * 100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From AI agent: clamp_min wasn't applied to the 7d rule, so it now differs from the 24h one. Same for the grouping point above.

category: temporal-cost
severity: warning
annotations:
summary: "Action rate is {{ $values.A }}% higher than the same time yesterday."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from an AI agent: {{ $values.A }} renders unrounded — a live rule gave me "Action rate is 257.14285714285717% higher...". A is an instant query so format it directly, but guard it (printf on a missing value renders %!f(<nil>)):

summary: '{{ if $values.A }}Action rate is {{ printf "%.1f" $values.A.Value }}% above{{ else }}Action rate change unknown vs{{ end }} the same time yesterday.'

.Value is required — $values.A is a struct. Add {{ $labels.temporal_namespace }} once grouping is in.

Minor: the description says "moved more than 50% away from", but gt is one-sided — a collapse never fires.

# Before use, replace every occurrence of PROM_DS_UID below with the UID of your
# own Prometheus data source (Connections > Data sources > your source > Settings).
#
# The thresholds here (50%, 75%, 80%) are reasonable starting points, not tuned to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cites three thresholds — 50%, 75%, 80% — but only 50 and 75 exist in the file. Looks like a leftover from a removed third rule.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants