Generate PromQL queries for calculating error rates, aggregating metrics across labels, creating histogram percentiles, writing recording rules, and building SLO burn-rate alerts following Prometheus best practices. Use when creating new PromQL queries, implementing monitoring and alerting rules, building observability dashboards, working with Prometheus metrics (counters, gauges, histograms, summaries), or applying RED (Rate, Errors, Duration) and USE (Utilization, Saturation, Errors) monitoring patterns.
87%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Use this skill when you need to:
assets/.promql-validator instead.up{job="..."} and the scrape config instead.CRITICAL: Always engage the user in collaborative planning before generating any query. Never skip the planning phase.
promql-validator. Display structured results (syntax, best practices, explanation). Fix any issues and re-validate until all checks pass.Ask vs. Infer: If the user's request already clearly specifies goal, use case, and context, acknowledge those details instead of re-asking. Only ask for missing or ambiguous information.
Always consult the relevant reference file before writing code.
| Scenario | Reference File |
|---|---|
| Histogram queries | references/metric_types.md (Histogram section) |
| Error/latency patterns | references/promql_patterns.md (RED section) |
| Resource monitoring | references/promql_patterns.md (USE section) |
| Optimization / anti-patterns | references/best_practices.md |
| Specific functions | references/promql_functions.md |
rate()/increase() on counters; *_over_time() or direct use for gauges; histogram_quantile() for histograms.by()/without() on all aggregations.level:metric:operations).# Request rate (counter)
sum(rate(http_requests_total{job="api-server"}[5m])) by (endpoint)
# Error rate ratio
sum(rate(http_requests_total{job="api-server", status_code=~"5.."}[5m]))
/ sum(rate(http_requests_total{job="api-server"}[5m]))
# P95 latency (classic histogram)
histogram_quantile(0.95,
sum by (le) (rate(http_request_duration_seconds_bucket{job="api-server"}[5m]))
)
# P95 latency (native histogram, Prometheus 3.x+)
histogram_quantile(0.95,
sum by (job) (rate(http_request_duration_seconds[5m]))
)
# Availability
(count(up{job="api-server"} == 1) / count(up{job="api-server"})) * 100
# Burn rate (99.9% SLO, 1h window)
(
sum(rate(http_requests_total{job="api", status_code=~"5.."}[1h]))
/ sum(rate(http_requests_total{job="api"}[1h]))
) / 0.001
# Multi-window burn-rate alert (page: 2% budget in 1h, burn rate 14.4)
(
sum(rate(http_requests_total{job="api", status_code=~"5.."}[1h]))
/ sum(rate(http_requests_total{job="api"}[1h]))
) > 14.4 * 0.001
and
(
sum(rate(http_requests_total{job="api", status_code=~"5.."}[5m]))
/ sum(rate(http_requests_total{job="api"}[5m]))
) > 14.4 * 0.001For complete SLO patterns, Native Histogram functions (histogram_count, histogram_sum, histogram_fraction), subqueries, offset/@ modifiers, vector matching, and Kubernetes patterns: see the assets/ files.
After generating, invoke promql-validator and display results in this format:
## PromQL Validation Results
### Syntax Check
- Status: ✅ VALID / ⚠️ WARNING / ❌ ERROR
- Issues: [list any syntax errors]
### Best Practices Check
- Status: ✅ OPTIMIZED / ⚠️ CAN BE IMPROVED / ❌ HAS ISSUES
- Issues: [list problems found]
- Suggestions: [list optimizations]
### Query Explanation
- What it measures: [plain English]
- Output labels: [label list or "None (scalar)"]
- Expected result structure: [instant vector / scalar / etc.]Fix all issues and re-validate until clean.
Check the shipped rule files, or your own, with promtool before loading them into Prometheus:
promtool check rules assets/alerting_rules.yaml
promtool check rules assets/recording_rules.yamlRun a candidate query against a live server to confirm it returns series:
curl -s 'http://localhost:9090/api/v1/query' --data-urlencode 'query=sum by (job) (rate(http_requests_total[5m]))'Query a recorded series instead of repeating its expression:
job:http_requests:rate5m{job="api-server"}Use *_over_time() for a gauge rather than rate():
avg_over_time(node_memory_MemAvailable_bytes[5m])Browse the reusable query templates with a plain listing:
grep -n "^# " assets/common_queries.promql# Alerting rule with for clause
alert: HighErrorRate
expr: |
(
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
) > 0.05
for: 10m
# Recording rule (naming: level:metric:operations)
- record: job:http_requests:rate5m
expr: sum by (job) (rate(http_requests_total[5m]))Each entry below pairs a reason with a BAD and a GOOD query. See references/best_practices.md for further performance and cardinality guidance.
rate() on a gauge metricWHY: rate() computes per-second rate of increase and assumes monotonically increasing counters. Applied to a gauge, it produces nonsensical results because gauges can decrease.
BAD: rate(node_memory_MemFree_bytes[5m]): memory is a gauge
GOOD: node_memory_MemFree_bytes (direct use) or delta(node_memory_MemFree_bytes[5m]) for change over time
avg() across quantile labelsWHY: Summary metrics expose pre-aggregated {quantile="0.95"} labels that cannot be re-aggregated across instances. Using avg() on them produces statistically meaningless results.
BAD: avg(http_request_duration_seconds{quantile="0.95"})
GOOD: histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m]))): use histogram type with histogram_quantile()
by() without filtering firstWHY: Aggregating by user_id, request_id, or pod without label filters produces thousands of series, overwhelming dashboards and recording rules.
BAD: sum by (user_id) (rate(http_requests_total[5m]))
GOOD: sum by (job, status_code) (rate(http_requests_total{job="api"}[5m])): filter and aggregate on stable, low-cardinality labels
increase() for alerting thresholdsWHY: increase() is extrapolated and can return non-integer values on sparse counters. For alerting, rate() produces stable per-second thresholds that are scrape-interval independent.
BAD: increase(http_requests_total{status=~"5.."}[5m]) > 10
GOOD: rate(http_requests_total{status=~"5.."}[5m]) > 0.033 (2 errors/minute = 0.033/second)
for clause on alert rulesWHY: Alerts without for fire immediately on a single evaluation, causing false positives from transient spikes. The for clause requires the condition to be true for a sustained period.
BAD: alert: HighErrorRate with expr: error_rate > 0.05 and no for
GOOD: Add for: 5m to require the condition to hold for 5 minutes before firing
WHY: Raw error counts rise with traffic, so a busy service looks worse than a failing quiet one. A ratio of rated errors over rated totals is comparable across load.
BAD: sum(rate(http_requests_total{job="api-server", status_code=~"5.."}[5m])) used as the alert expression.
GOOD: sum(rate(http_requests_total{job="api-server", status_code=~"5.."}[5m])) / sum(rate(http_requests_total{job="api-server"}[5m]))
WHY: Guessing the metric name, type or window produces a query that parses but answers the wrong question. The planning workflow exists to catch this before code is written.
BAD: Generate rate(http_requests_total[5m]) straight from "show me errors".
GOOD: Present the plain-English plan at stage 4 and wait for confirmation, then generate.
WHY: Stage 6 of the workflow catches syntax errors and best-practice issues before the user pastes the query into a dashboard or rule file.
BAD: Hand over the first draft and stop.
GOOD: Run the validator, show the Validation Checklist output, fix issues and re-validate until clean.
WHY: Equality matchers are cheaper and unambiguous, while a loose regex such as =~".*" can match far more series than intended. Key Rule 4 prefers exact matches.
BAD: http_requests_total{job=~"api.*"} when the job is api-server.
GOOD: http_requests_total{job="api-server"}
WHY: Each copy is evaluated separately and the copies drift apart over time. A recording rule computes it once and gives every consumer the same series.
BAD: Pasting sum by (job) (rate(http_requests_total[5m])) into several panels and alert rules.
GOOD: Define - record: job:http_requests:rate5m once and query job:http_requests:rate5m everywhere.
irate() window for alert thresholdsWHY: irate() only looks at the last two samples, so a single noisy scrape can trip or clear an alert. rate() over a window smooths this out.
BAD: irate(http_requests_total{job="api-server", status_code=~"5.."}[1m]) > 0.05
GOOD: rate(http_requests_total{job="api-server", status_code=~"5.."}[5m]) > 0.05
WHY: An error rate that divides a 5m rate by a 1h rate compares unlike quantities and gives a misleading percentage.
BAD: sum(rate(errors_total[5m])) / sum(rate(requests_total[1h]))
GOOD: sum(rate(errors_total[5m])) / sum(rate(requests_total[5m]))
prometheus, then fetch docs with the relevant topic."Prometheus PromQL [function/operator] documentation [version] examples"| Symptom | Likely Cause | Fix |
|---|---|---|
| Empty results | Wrong label filters or metric not scraped | Check up{job="..."}, verify label values |
| Too many series | High cardinality | Add label filters, aggregate, use recording rules |
| Wrong values | Wrong function for metric type | rate() on counters; direct or *_over_time() on gauges |
| Slow queries | Large range vectors or missing filters | Narrow time range, add filters, use recording rules |
Internal:
for clauseslevel:metric:operations namingpantheon-org/tekhne
c8416e9
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.