Resource library

QA How-To

Grafana dashboards for test metrics: Step by Step (2026)

Build Grafana dashboards for test metrics with Prometheus, runnable Python, PromQL panels, variables, provisioning, alerting, governance, and interview Q&A.

24 min read | 2,932 words

TL;DR

Send bounded test-run counters, duration histograms, and last-run timestamps to a Prometheus-compatible store, then query them from Grafana. Keep high-cardinality test detail in JUnit, logs, or an analytics store and link to it from actionable panels.

Key Takeaways

  • Start with release and operational decisions, then choose panels that answer them.
  • Keep Prometheus labels bounded and store test names, run IDs, and stack traces elsewhere.
  • Use counters for outcomes, histograms for duration, and timestamp gauges for staleness.
  • Show counts and definitions beside rates so percentages cannot hide missing volume.
  • Use variables and deployment annotations to add context without duplicating dashboards.
  • Provision validated dashboards as code with stable UIDs and a clear source of truth.
  • Alert on actionable cross-run or pipeline conditions, not every ordinary test failure.

Grafana dashboards for test metrics should answer operational questions, not decorate a wall with green percentages. A useful dashboard shows whether required suites ran, what failed, how long feedback took, where instability is rising, and which release or environment was affected.

This guide builds a defensible metrics path from automated test execution to Prometheus and Grafana. You will define a low-cardinality metric contract, run a local emitter, configure a Prometheus data source, write PromQL for actionable panels, add variables and annotations, provision dashboards as code, and design alerts without punishing teams for honest failures.

TL;DR

Question Metric or evidence Recommended view
Are required suites running? Last completion and last success timestamps Stat plus staleness alert
Did quality change? Run and case outcomes over time Time series with counts and rate
Is feedback slowing? Run duration histogram p50 and p95 time series
Is instability growing? Final flaky or retry outcome Trend, not a single percentage
Where did it change? Project, suite, branch, environment labels Dashboard variables
Which test failed? JUnit or log store, linked from dashboard Detail system, not a metric label
What deployed during the change? Deployment annotations Shared timeline

The key design is aggregation. Keep project, suite, environment, branch class, and outcome as bounded labels. Do not put test name, commit SHA, exception text, URL, or run ID into Prometheus labels because unbounded cardinality harms query and storage performance.

1. Grafana Dashboards for Test Metrics: Start with Decisions

Begin with the audience and decision. A pull request author needs immediate failure details. A QA lead needs suite health and instability trends. A release manager needs evidence that required gates ran on the candidate. A platform engineer needs queue, runner, and environment health. One dashboard can provide navigation, but it should not collapse all four purposes into one score.

Define questions before panels:

  • Did the critical suite run for the expected branch and environment?
  • Is the current failure isolated or part of a trend?
  • Did duration increase after a deployment or infrastructure change?
  • Are retries hiding unstable tests?
  • Is missing data caused by no test, a broken exporter, or an unavailable environment?
  • Which evidence system contains test-level details?

Avoid a universal "automation health 87%" score. Its weighting is subjective, and teams often optimize the number rather than the product. Show component measures with definitions and owners.

Grafana is a visualization and alerting layer over data sources. It does not discover tests or repair a broken result pipeline. Prometheus is useful for bounded time-series metrics. JUnit, an analytics database, object storage, logs, or a test-management system is better for individual test names, stack traces, attachments, and long-term run records.

The test metrics that matter guide can help align measures with release and improvement decisions before implementation.

2. Design the Test Metrics Data Flow

A production design usually has four parts:

CI job or test runner
        |
        v
Result adapter or event collector
        |
        +--> detailed JUnit, logs, traces, object storage
        |
        v
Prometheus-compatible metrics endpoint
        |
        v
Grafana dashboards and alert rules

The adapter reads a stable result format, classifies the run, updates counters and histograms, and exposes metrics for scraping. A long-running collector retains counter state between scrapes. If short CI jobs push batch metrics, use a supported gateway or remote-write design with explicit lifecycle rules. Do not create one permanent series per run.

Prometheus stores numeric samples with labels. Grafana queries Prometheus with PromQL. Detailed evidence remains elsewhere and is linked through dashboard data links or a run-summary system. This split keeps time-series queries fast while preserving diagnosis.

Choose one source of truth for every measure. If both the CI API and a JUnit parser publish run totals, the dashboard can double-count. Decide how cancelled, skipped, retried, quarantined, and infrastructure-failed outcomes are classified. Publish a schema document with examples.

Time semantics matter. Record completion when the run reaches a terminal state. Record duration from the same start and end boundaries. Use UTC timestamps and let Grafana render the viewer's timezone. For release evidence, include a bounded environment or release-channel label, but put high-cardinality release identifiers in annotations or detail links.

Design for pipeline outages. A flat line can mean perfect stability or no data. Last-seen and last-success gauges make absence visible.

3. Create a Low-Cardinality Metric Contract

A practical first contract might expose:

Metric Type Suggested bounded labels Meaning
qa_test_runs_total Counter project, suite, status, branch Completed suite runs
qa_test_cases_total Counter project, suite, outcome Final test-case outcomes
qa_test_run_duration_seconds Histogram project, suite, branch End-to-end suite duration
qa_test_last_completion_timestamp_seconds Gauge project, suite, branch Last terminal run time
qa_test_last_success_timestamp_seconds Gauge project, suite, branch Last successful run time
qa_test_queue_duration_seconds Histogram project, runner_class CI queue delay if available

Counters only increase while the collector process lives. Use rate() or increase() in PromQL. Histograms expose bucket counters, a sum, and a count, enabling percentile estimation across series. Gauges represent current values such as a Unix timestamp.

Keep label values bounded. suite might be unit, api, e2e-smoke, and e2e-full. status might be passed, failed, cancelled, and infrastructure_error. Define whether a retry-pass run is passed plus a separate instability measure, or a distinct passed_with_retry outcome. Consistency matters more than the label spelling.

Never label metrics with:

  • Test case name.
  • Commit SHA or run ID.
  • Error message or stack trace.
  • Pull request number in a high-volume repository.
  • Full branch name if branches are unbounded.
  • Browser version with every patch when major coverage is enough.
  • User, tenant, URL, or free-form tag.

Those belong in detailed events or logs. Prometheus creates a time series for each label combination, so unbounded labels create uncontrolled series growth.

4. Run a Local Metrics Emitter

The following Python program is a runnable learning adapter. It exposes Prometheus metrics on port 9108, records one illustrative suite run, and stays alive for scraping. A production adapter would receive authenticated result events, validate idempotency, persist state appropriately, and classify real results.

Install the client library:

python -m venv .venv
source .venv/bin/activate
pip install prometheus-client

Create test_metrics_demo.py:

import time
from prometheus_client import Counter, Gauge, Histogram, start_http_server

RUNS = Counter(
    "qa_test_runs_total",
    "Completed automated test suite runs",
    ["project", "suite", "status", "branch"],
)
CASES = Counter(
    "qa_test_cases_total",
    "Final automated test case outcomes",
    ["project", "suite", "outcome"],
)
DURATION = Histogram(
    "qa_test_run_duration_seconds",
    "Automated test suite duration in seconds",
    ["project", "suite", "branch"],
    buckets=(1, 5, 10, 30, 60, 120, 300, 600, 1200),
)
LAST_COMPLETION = Gauge(
    "qa_test_last_completion_timestamp_seconds",
    "Unix timestamp of the last completed suite run",
    ["project", "suite", "branch"],
)
LAST_SUCCESS = Gauge(
    "qa_test_last_success_timestamp_seconds",
    "Unix timestamp of the last successful suite run",
    ["project", "suite", "branch"],
)

def record_run(project, suite, status, branch, duration, outcomes):
    labels = {"project": project, "suite": suite, "branch": branch}
    RUNS.labels(status=status, **labels).inc()
    DURATION.labels(**labels).observe(duration)
    now = time.time()
    LAST_COMPLETION.labels(**labels).set(now)
    if status == "passed":
        LAST_SUCCESS.labels(**labels).set(now)
    for outcome, count in outcomes.items():
        CASES.labels(project=project, suite=suite, outcome=outcome).inc(count)

if __name__ == "__main__":
    start_http_server(9108)
    record_run(
        project="checkout",
        suite="e2e-smoke",
        status="passed",
        branch="main",
        duration=42.7,
        outcomes={"passed": 118, "failed": 0, "skipped": 1, "flaky": 2},
    )
    while True:
        time.sleep(3600)

Run it and inspect http://localhost:9108/metrics. The values are explicitly illustrative, not industry benchmarks.

5. Connect Prometheus and Grafana

Configure Prometheus to scrape the demo endpoint. When Prometheus runs on the same host, a minimal prometheus.yml is:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: qa-test-metrics
    static_configs:
      - targets:
          - localhost:9108

If Prometheus runs in a container, localhost refers to that container. Use the correct service name or host gateway for your environment. Confirm target health in Prometheus before opening Grafana.

Grafana includes a built-in Prometheus data source. In the UI, add Prometheus and set the server URL, such as http://prometheus:9090. Test the connection. For reproducible environments, provision the data source:

apiVersion: 1

datasources:
  - name: Prometheus
    uid: qa-prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false
    jsonData:
      httpMethod: POST
      timeInterval: 15s

Store this file in Grafana's provisioning data-source directory according to the deployment package or container mount. Secrets such as authentication passwords belong in supported secure provisioning fields or a secret manager, not dashboard JSON.

Use Grafana Explore before building panels. Query qa_test_runs_total and inspect labels. If no series appears, troubleshoot scrape target, network, metric name, and time range. Building a dashboard before confirming data adds unnecessary variables.

For remote Prometheus-compatible systems such as Mimir or Thanos, the same Grafana data-source model can apply, but retention, tenancy, authentication, and query behavior need platform-specific configuration.

6. Grafana Dashboards for Test Metrics: Build Core Panels

Create a dashboard folder for QA engineering and add a small number of panels in decision order.

Pass percentage over the selected range

100 *
sum(increase(qa_test_runs_total{project=~"$project", suite=~"$suite", status="passed"}[$__range]))
/
clamp_min(
  sum(increase(qa_test_runs_total{project=~"$project", suite=~"$suite"}[$__range])),
  1
)

Display it as a stat, but also show the underlying run count. A 100 percent result from one run is not equivalent to 100 percent from fifty runs.

Run outcomes by status

sum by (status) (
  increase(qa_test_runs_total{project=~"$project", suite=~"$suite"}[$__rate_interval])
)

Use a time series or stacked view. Name the panel "Completed runs by outcome" rather than "Quality" because it measures execution results, not total product quality.

p95 suite duration

histogram_quantile(
  0.95,
  sum by (le, suite) (
    rate(qa_test_run_duration_seconds_bucket{project=~"$project", suite=~"$suite"}[$__rate_interval])
  )
)

Use a time series with seconds as the unit. Add p50 when typical behavior matters. Histogram percentiles are estimates shaped by bucket boundaries, so choose buckets around useful duration ranges.

Seconds since last successful run

time() -
max by (project, suite, branch) (
  qa_test_last_success_timestamp_seconds{
    project=~"$project",
    suite=~"$suite",
    branch=~"$branch"
  }
)

A staleness panel detects missing execution, which pass percentages cannot. Set thresholds from the expected schedule for each suite.

Final flaky outcomes

sum by (suite) (
  increase(qa_test_cases_total{project=~"$project", suite=~"$suite", outcome="flaky"}[$__range])
)

Show counts beside a denominator. Do not interpret retries as harmless passes. The flaky test root cause guide can support the remediation workflow behind the chart.

7. Add Variables, Annotations, and Diagnostic Links

Variables make one dashboard reusable without duplicating it for every project. Add query variables using the Prometheus data source. Choose the Label values query type, select the relevant metric, and return bounded labels such as project, suite, and branch.

Use variables in PromQL with regex matchers when multiple selection or All is enabled:

qa_test_runs_total{
  project=~"$project",
  suite=~"$suite",
  branch=~"$branch"
}

Do not use the deprecated double-bracket variable syntax. Prefer $variable or ${variable} where braces improve parsing. Set useful defaults, and ensure All does not query an unsafe volume.

Annotations place deployments, incidents, environment changes, or test configuration updates on the same time line. A duration increase immediately after a deployment is easier to investigate when the event is visible. Use bounded tags and link to the authoritative deployment or change record.

Data links can navigate from a project or suite panel to a CI pipeline, test analytics system, or runbook. Do not attempt to place a run ID in every Prometheus label solely to build a link. A separate recent-run table from a SQL or log data source can carry detailed identifiers without exploding time-series cardinality.

Dashboard descriptions should define numerator, denominator, exclusions, timezone, data delay, and owner. Panel descriptions should state what action a threshold suggests. A graph with no definition invites competing interpretations during a release call.

Use rows sparingly to group execution coverage, outcome, duration, and infrastructure. The most urgent staleness and failure signals belong near the top.

8. Provision Dashboards as Code

Once the dashboard is useful, export and version it. Provisioning makes data sources and dashboards reproducible across environments. A file provider can load dashboard JSON from a mounted directory:

apiVersion: 1

providers:
  - name: qa-test-dashboards
    orgId: 1
    folder: QA Engineering
    type: file
    disableDeletion: false
    allowUiUpdates: false
    updateIntervalSeconds: 30
    options:
      path: /var/lib/grafana/dashboards

Place the exported dashboard JSON under that path. Give the dashboard a stable UID so links remain consistent across instances. Do not reuse a UID for a different dashboard.

With allowUiUpdates: false, the file is the source of truth. Even when UI saving is allowed, a later provisioning update can overwrite database changes. Establish one workflow: edit through reviewed source, or export reviewed UI edits back to source before rollout.

Keep environment-specific data-source references stable through provisioned UIDs or appropriate variables. Do not commit encrypted-looking secret fields from an export and assume they are portable. Use supported provisioning and secret injection.

Review JSON diffs carefully because dashboard exports can contain noise. Use consistent Grafana versions in development and deployment, and validate imports in a test instance. Grafana's evolving dashboard resource formats may differ across versions, so choose the format supported by the target and follow its official export guidance.

Dashboard as code adds responsibility. Assign reviewers who understand PromQL, QA semantics, and Grafana behavior. A syntactically valid query can still divide the wrong populations.

9. Design Alerts and Thresholds Responsibly

Alert on an actionable condition with an owner and runbook. Useful test-system alerts include no successful critical suite within its expected window, a sustained rise in infrastructure-error runs, no collector samples, or a release gate that stopped publishing.

Avoid paging a team for one ordinary functional assertion failure if the CI system already notifies the responsible change author. Grafana alerts should address cross-run or operational risk, not duplicate every red pipeline.

Staleness requires careful logic. A nightly suite may be late on a weekend by design. A pull request suite has no fixed global schedule. Alert rules should reflect expected cadence, branch class, maintenance windows, and data-source health. Distinguish "no successful run" from "no completed run" and "metrics pipeline unavailable."

Thresholds should come from service expectations and baselines. A fixed 95 percent pass target can reward teams for deleting hard tests or rerunning failures. Pair rate alerts with counts, time windows, and classifications. Exclude neither quarantined nor infrastructure failures silently. Show them as separate outcomes.

Use a for duration to avoid alerting on a single transient scrape gap where appropriate. Test the no-data and error behavior of each rule. A broken query should not silently become OK. Route notifications by project or platform ownership without allowing unbounded labels to create notification storms.

Every alert runbook should include metric definition, dashboard link, detailed evidence location, validation steps, common failure classes, and escalation. Review alert usefulness after incidents and remove rules that produce no action.

10. Validate, Govern, and Evolve the Dashboard

Validate the full chain with controlled events. Record one passing run, one failing run, one retry-classified case, and one delayed run in a safe project. Confirm counters, histograms, timestamps, panels, variables, links, annotations, and alerts. Verify time zones and range behavior.

Test idempotency. If the CI job retries its publishing step, the adapter must not count the same run twice. A production event collector should use a run identifier internally for deduplication even though that identifier is not a Prometheus label. Track rejected and duplicate events as bounded operational metrics.

Monitor the metrics pipeline itself:

  • Collector request failures and processing latency.
  • Prometheus target health and scrape duration.
  • Series count and label cardinality.
  • Remote-write or storage errors.
  • Grafana query failures and dashboard load time.
  • Time since last result per expected suite.
  • Schema version and adapter deployment.

Review metric definitions with teams when test strategy changes. Splitting one suite into three can create an artificial jump in run counts. A new retry policy can change flaky classification. Backfill and dashboard annotations may be needed for honest trend interpretation.

Control access. Test dashboards can reveal release names, internal services, incidents, and customer-like data. Use synthetic labels, least privilege, and appropriate folders. Treat shared snapshots carefully.

Finally, measure whether the dashboard changes decisions. If nobody uses a panel during triage, release, or improvement planning, simplify or remove it. Good test observability reduces time to notice, understand, and act.

Interview Questions and Answers

Q: Which test automation metrics belong in Grafana?

I put bounded time-series measures there: completed runs by status, final case outcomes, duration histograms, queue time, and last completion or success. Individual test names, stack traces, and run IDs belong in a detail store. Grafana links summary trends to that evidence.

Q: Why is label cardinality important?

Prometheus creates a series for each unique label combination. Labels such as test name, commit SHA, run ID, and exception text grow without a stable bound and can harm ingestion, storage, and queries. I keep labels limited to controlled dimensions and store detail elsewhere.

Q: How would you calculate test pass percentage?

I divide passed completed runs by all completed runs over the same selected range and protect the denominator from zero. I show the count beside the percentage and define treatment of cancelled and infrastructure outcomes. A percentage without volume and classification is misleading.

Q: Why use a histogram for test duration?

A histogram supports aggregated rate and percentile queries across instances and time. It lets me show typical and tail duration rather than only an average. Bucket boundaries must reflect meaningful duration ranges because percentile results are estimated from them.

Q: How do you detect that tests stopped running?

I publish last completion and last success timestamp gauges and chart the seconds since those events. Alert thresholds follow the suite's expected schedule. I separately monitor collector and scrape health so missing data is not mistaken for a test result.

Q: How do you prevent duplicate metrics when CI retries publishing?

The collector accepts an internal event or run identifier and applies idempotent processing before incrementing counters. That identifier is used for deduplication storage, not as a Prometheus label. I test repeated delivery and monitor rejected duplicates.

Q: What makes a test dashboard actionable?

Every panel maps to a question, has a definition and owner, and links to detailed evidence or a runbook. Thresholds reflect an agreed service expectation. Deployment annotations and bounded filters provide enough context to move from signal to diagnosis.

Common Mistakes

  • Building panels before defining the release or operational decisions they support.
  • Calling run pass percentage a complete product quality score.
  • Putting test names, run IDs, commit SHAs, and exception messages in Prometheus labels.
  • Publishing from both CI APIs and JUnit adapters and double-counting runs.
  • Treating retries as clean passes and losing the first failure.
  • Showing a percentage without count, time range, and denominator definition.
  • Ignoring missing data and interpreting a flat line as stability.
  • Using an average where tail duration or distribution matters.
  • Calculating histogram percentiles with inappropriate buckets or aggregation.
  • Duplicating dashboards for every project instead of using bounded variables.
  • Saving UI changes to a provisioned dashboard without updating source.
  • Alerting every test failure in both CI and Grafana.
  • Leaving no-data and query-error alert behavior untested.
  • Exposing secrets or customer-like identifiers through labels and annotations.
  • Tracking dashboards without monitoring the collector and scrape pipeline.

Conclusion

Grafana dashboards for test metrics become valuable when the metric contract is small, bounded, and connected to real decisions. Use counters for completed outcomes, histograms for duration, timestamp gauges for staleness, and a separate evidence store for test-level detail.

Run the local emitter, confirm Prometheus scraping in Explore, then build the pass, outcome, duration, staleness, and flake panels. Add variables, deployment annotations, reviewed provisioning, and only actionable alerts. The result is not merely a dashboard, it is an observable test-delivery system with definitions and owners.

Interview Questions and Answers

Design an architecture for automated test metrics in Grafana.

A result adapter receives terminal test events, applies consistent classification and idempotency, and exposes bounded counters, histograms, and gauges to Prometheus. Grafana queries those metrics for trends and alerts. JUnit, traces, and logs remain in detail stores linked from the dashboard.

How do you control Prometheus cardinality for QA metrics?

I allow only controlled dimensions such as project, suite, environment, branch class, and outcome. I reject free-form labels and monitor series growth. Run ID, commit, test name, URL, and exception text belong in an event or log system.

How would you display flaky tests?

I publish a defined final flaky or retry outcome and chart counts plus a denominator over time. I do not turn a retry-pass into a clean pass. The dashboard links to the test-level system where owners, history, and failure evidence live.

What is the limitation of a p95 duration panel?

With Prometheus histograms, p95 is estimated from bucket counts, so bucket design and aggregation matter. Sparse samples can also make the trend weak. I show sample volume, choose meaningful buckets, and compare the time series with deployment events.

How would you test a Grafana dashboard?

I inject controlled pass, fail, retry, delay, duplicate, and missing-data cases in a safe environment. I verify queries, units, variables, time ranges, links, annotations, alert states, and permissions. I also test dashboard provisioning in the target Grafana version.

How do you distinguish no tests from no telemetry?

I expose last result timestamps from the collector and monitor collector plus Prometheus scrape health separately. If the target is down, telemetry health fails. If telemetry is healthy but the expected suite timestamp is stale, execution is missing.

What governance does a QA dashboard need?

Each metric has a definition, classification rules, owner, schema version, and data-retention expectation. Dashboard source and alerts are reviewed. Changes in suite topology or retry policy are annotated because they can alter trend meaning.

Frequently Asked Questions

What test automation metrics should I show in Grafana?

Start with completed runs by status, test outcomes, suite duration percentiles, queue time, and time since last completion or success. Add environment or project filters only when their values are bounded and useful.

Can Grafana read JUnit XML directly?

Grafana normally queries a supported data source rather than treating JUnit files as time-series storage. Parse result events into a metrics or analytics store, retain the JUnit file for detail, and link the dashboard to the evidence.

Why should test names not be Prometheus labels?

Test names can grow and change continuously, creating a large number of unique time series. Store them in JUnit, logs, or a test analytics database. Use bounded labels such as project, suite, environment, and outcome for Prometheus metrics.

How do I calculate pass rate in PromQL?

Use `increase()` over the same range for passed and total completed runs, divide passed by a denominator protected with `clamp_min`, and multiply by 100. Show the run count and define how cancelled or infrastructure failures are treated.

How can Grafana detect that a test suite did not run?

Publish last completion and last success timestamps as gauges. Query `time()` minus the timestamp and alert when the result exceeds the suite's expected schedule, while separately checking collector and Prometheus target health.

Should Grafana alert on every failed automated test?

Usually no. CI should notify the change owner about an individual failed run. Grafana alerts are better for sustained instability, missing critical suites, exporter outages, or cross-run operational conditions with a clear owner.

How do I manage Grafana test dashboards as code?

Export the supported dashboard format, assign a stable UID, store it in version control, and use a file or platform provisioning workflow. Decide whether UI updates are allowed and ensure source is updated before provisioning overwrites database changes.

Related Guides