christiangeorgelucas/promql-tools
v0.1.1GoThe promql-tools package provides deterministic, offline parsing and structural analysis of PromQL query strings and Prometheus rule files. It enables users to validate, pretty-print, and inspect PromQL queries and rule files without requiring access to a Prometheus server, ensuring robust error handling and detailed insights into query structure.
Published by Christian George Lucas· MIT
Use cases
- •Validate PromQL queries for syntax errors before deployment
- •Extract metric names and label matchers from complex queries
- •Pretty-print PromQL queries for better readability
- •Analyze alerting and recording rules in Prometheus rule files
- •Perform structural audits of Prometheus rule files
Nodes (15)
The DetectResultType node analyzes a PromQL query and determines its result type, which can be an instant vector, range vector, scalar, or string, based on the parsed abstract syntax tree (AST). This functionality is essential for understanding the output of PromQL queries in monitoring and alerting systems.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// DetectResultType infers a PromQL query's result type — instant vector,
// range vector, scalar, or string — from its parsed AST.
func DetectResultType(ctx context.Context, ax axiom.Context, input *gen.PromQLQuery) (*gen.ResultTypeInfo, error) {
a := pq.Analyze(input.GetQuery())
out := &gen.ResultTypeInfo{Valid: a.Valid, Error: toGenError(a.Error)}
if a.Valid {
out.ResultType = toGenResultType(a.ResultType)
out.TypeString = a.ResultType
}
return out, nil
}
The ExtractAggregations node analyzes a PromQL query to identify and return all aggregation expressions, such as sum, avg, and count_values, along with their associated grouping labels. It also indicates whether the query includes a grouping clause or not.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ExtractAggregations returns every aggregation expression in a PromQL
// query (sum, avg, topk, count_values, ...) with its by/without grouping
// labels. has_grouping distinguishes "no by/without clause at all" from an
// explicit, empty `by ()`.
func ExtractAggregations(ctx context.Context, ax axiom.Context, input *gen.PromQLQuery) (*gen.Aggregations, error) {
a := pq.Analyze(input.GetQuery())
return &gen.Aggregations{Valid: a.Valid, Error: toGenError(a.Error), Aggregations: toGenAggregations(a.Aggregations)}, nil
}
The ExtractFunctions node analyzes a PromQL query to identify and return every function call present, including the count of arguments for each call, in the order they appear. It specifically focuses on function calls like rate, sum, and histogram_quantile, while aggregations are handled by a separate node.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ExtractFunctions returns every function call in a PromQL query (rate,
// sum, histogram_quantile, etc.) with each call's argument count, in the
// order they appear.
func ExtractFunctions(ctx context.Context, ax axiom.Context, input *gen.PromQLQuery) (*gen.FunctionCalls, error) {
a := pq.Analyze(input.GetQuery())
return &gen.FunctionCalls{Valid: a.Valid, Error: toGenError(a.Error), Functions: toGenFunctions(a.Functions)}, nil
}
The ExtractLabelMatchers node analyzes a PromQL query and extracts all label matchers used in vector selectors, including the label name, operator, value, and the metric name associated with each selector. This allows users to identify the specific criteria used to filter metrics in the query.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ExtractLabelMatchers returns every label matcher used across every vector
// selector in a PromQL query — label name, operator (=, !=, =~, !~), value,
// and the metric name of the selector it came from. A matcher that merely
// spells out the selector's own literal metric name (the implicit
// `__name__="x"` behind `x{...}`) is not repeated here — see
// ExtractMetricNames for that.
func ExtractLabelMatchers(ctx context.Context, ax axiom.Context, input *gen.PromQLQuery) (*gen.LabelMatchers, error) {
a := pq.Analyze(input.GetQuery())
return &gen.LabelMatchers{Valid: a.Valid, Error: toGenError(a.Error), Matchers: toGenMatchers(a.Matchers)}, nil
}
The ExtractMetricNames node processes a PromQL query to identify and return all unique metric names referenced within it, including both standard metric selectors and explicit matchers, ensuring the results are presented in the order of their first occurrence.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ExtractMetricNames returns every metric name referenced in a PromQL
// query — both `metric_name{...}` selectors and an explicit
// `{__name__="metric_name"}` matcher — deduplicated, in first-occurrence
// order.
func ExtractMetricNames(ctx context.Context, ax axiom.Context, input *gen.PromQLQuery) (*gen.MetricNames, error) {
a := pq.Analyze(input.GetQuery())
return &gen.MetricNames{Valid: a.Valid, Error: toGenError(a.Error), Names: a.Metrics}, nil
}
The ExtractRanges node analyzes a PromQL query to identify and extract all range-vector and subquery durations, returning them in both canonical text format and as numerical seconds.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ExtractRanges returns every range-vector and subquery duration in a
// PromQL query — the `[5m]` in `rate(x[5m])`, or the `[30m:1m]` range+step
// pair in a subquery — as both canonical duration text and seconds.
func ExtractRanges(ctx context.Context, ax axiom.Context, input *gen.PromQLQuery) (*gen.RangeDurations, error) {
a := pq.Analyze(input.GetQuery())
return &gen.RangeDurations{Valid: a.Valid, Error: toGenError(a.Error), Ranges: toGenRanges(a.Ranges)}, nil
}
The ExtractRuleExpressions node processes a RuleFile to extract each rule's PromQL expression, associating it with its group, rule name, and alerting or recording type, while also validating the expression's syntax for further analysis.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ExtractRuleExpressions returns every rule's PromQL `expr`, tagged with
// its owning group, rule name, and alerting/recording kind, plus whether
// that expression itself parses — so each one can be fed independently
// into ParseQuery/ExtractMetricNames/etc. for per-expression analysis.
func ExtractRuleExpressions(ctx context.Context, ax axiom.Context, input *gen.RuleFile) (*gen.RuleExpressions, error) {
r := pq.ParseRuleFile(input.GetYaml())
out := &gen.RuleExpressions{Valid: r.Valid, Error: toGenError(r.Error)}
if !r.Valid {
return out, nil
}
for _, g := range r.Groups {
for _, rule := range g.Rules {
name := rule.Alert
if !rule.IsAlerting {
name = rule.Record
}
exprValid, exprErr := pq.ValidateExpr(rule.Expr)
out.Expressions = append(out.Expressions, &gen.RuleExpression{
Group: g.Name,
RuleName: name,
IsAlerting: rule.IsAlerting,
Expr: rule.Expr,
ExprValid: exprValid,
ExprError: toGenError(exprErr),
})
}
}
return out, nil
}
The ExtractRuleFileInventory node analyzes a given rule file to extract and union all metric names and label matchers referenced in the rules' expressions, ensuring they are deduplicated and sorted for auditing dependencies and coverage.
View source
package nodes
import (
"context"
"sort"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ExtractRuleFileInventory unions every metric name and label matcher
// referenced across every rule's expr in a rule file, deduplicated and
// sorted, for a dependency/coverage audit ("which metrics does this alert
// file depend on").
func ExtractRuleFileInventory(ctx context.Context, ax axiom.Context, input *gen.RuleFile) (*gen.RuleFileInventory, error) {
r := pq.ParseRuleFile(input.GetYaml())
out := &gen.RuleFileInventory{Valid: r.Valid, Error: toGenError(r.Error)}
if !r.Valid {
return out, nil
}
metricSet := map[string]struct{}{}
type matcherKey struct{ label, op, value, ctx string }
matcherSet := map[matcherKey]*gen.LabelMatcher{}
for _, g := range r.Groups {
for _, rule := range g.Rules {
if rule.Expr == "" {
continue
}
a := pq.Analyze(rule.Expr)
if !a.Valid {
continue
}
for _, m := range a.Metrics {
metricSet[m] = struct{}{}
}
for _, m := range a.Matchers {
k := matcherKey{m.Label, m.Op, m.Value, m.MetricContext}
if _, ok := matcherSet[k]; !ok {
matcherSet[k] = toGenMatcher(m)
}
}
}
}
for m := range metricSet {
out.MetricNames = append(out.MetricNames, m)
}
sort.Strings(out.MetricNames)
for _, m := range matcherSet {
out.LabelMatchers = append(out.LabelMatchers, m)
}
sort.Slice(out.LabelMatchers, func(i, j int) bool {
a, b := out.LabelMatchers[i], out.LabelMatchers[j]
if a.MetricContext != b.MetricContext {
return a.MetricContext < b.MetricContext
}
if a.Label != b.Label {
return a.Label < b.Label
}
if a.Op != b.Op {
return a.Op < b.Op
}
return a.Value < b.Value
})
return out, nil
}
The FormatQuery node takes a PromQL query as input and pretty-prints it into its canonical form using the Prometheus parser's Prettify function, ensuring stable whitespace and quoting while maintaining semantic equivalence to the original query.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// FormatQuery pretty-prints a PromQL query into its canonical form
// (github.com/prometheus/prometheus/promql/parser's own Prettify) — stable
// whitespace and quoting, semantically identical to the input.
func FormatQuery(ctx context.Context, ax axiom.Context, input *gen.PromQLQuery) (*gen.FormattedQuery, error) {
a := pq.Analyze(input.GetQuery())
return &gen.FormattedQuery{Valid: a.Valid, Error: toGenError(a.Error), Formatted: a.Formatted}, nil
}
The ListAlertingRules node extracts and returns all alerting rules defined in a rule file, including details such as alert name, expression, duration, and associated labels and annotations, organized by their respective groups.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ListAlertingRules returns every alerting rule (`alert:` set) across every
// group in a rule file: alert name, expr, for-duration, keep_firing_for,
// labels, and annotations, each tagged with its owning group.
func ListAlertingRules(ctx context.Context, ax axiom.Context, input *gen.RuleFile) (*gen.AlertingRules, error) {
r := pq.ParseRuleFile(input.GetYaml())
out := &gen.AlertingRules{Valid: r.Valid, Error: toGenError(r.Error)}
if !r.Valid {
return out, nil
}
for _, g := range r.Groups {
for _, rule := range g.Rules {
if !rule.IsAlerting {
continue
}
out.Rules = append(out.Rules, &gen.AlertingRule{
Group: g.Name,
Alert: rule.Alert,
Expr: rule.Expr,
ForDuration: rule.ForDuration,
KeepFiringFor: rule.KeepFiringFor,
Labels: rule.Labels,
Annotations: rule.Annotations,
})
}
}
return out, nil
}
The ListRecordingRules node extracts and returns all recording rules from a given rule file, including their names, expressions, and associated labels, organized by their respective groups.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ListRecordingRules returns every recording rule (`record:` set) across
// every group in a rule file: record name, expr, and labels, each tagged
// with its owning group.
func ListRecordingRules(ctx context.Context, ax axiom.Context, input *gen.RuleFile) (*gen.RecordingRules, error) {
r := pq.ParseRuleFile(input.GetYaml())
out := &gen.RecordingRules{Valid: r.Valid, Error: toGenError(r.Error)}
if !r.Valid {
return out, nil
}
for _, g := range r.Groups {
for _, rule := range g.Rules {
if rule.IsAlerting || rule.Record == "" {
continue
}
out.Rules = append(out.Rules, &gen.RecordingRule{
Group: g.Name,
Record: rule.Record,
Expr: rule.Expr,
Labels: rule.Labels,
})
}
}
return out, nil
}
The ParseQuery node analyzes a PromQL query, providing a detailed structural breakdown that includes its validity, a formatted representation, inferred result type, and various components such as metric names and label matchers. In case of syntax errors, it returns an error message along with the location of the error in the query.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ParseQuery parses a PromQL query into its full structural analysis:
// validity, a canonical pretty-printed form, the inferred result type, and
// every metric name, label matcher, function call, aggregation, and
// range/subquery duration found in the expression. On a syntax error, valid
// is false and error carries the message plus 1-based line/column.
func ParseQuery(ctx context.Context, ax axiom.Context, input *gen.PromQLQuery) (*gen.ParsedQuery, error) {
a := pq.Analyze(input.GetQuery())
out := &gen.ParsedQuery{
Query: input.GetQuery(),
Valid: a.Valid,
Error: toGenError(a.Error),
}
if !a.Valid {
return out, nil
}
out.Formatted = a.Formatted
out.ResultType = toGenResultType(a.ResultType)
out.MetricNames = a.Metrics
out.LabelMatchers = toGenMatchers(a.Matchers)
out.Functions = toGenFunctions(a.Functions)
out.Aggregations = toGenAggregations(a.Aggregations)
out.Ranges = toGenRanges(a.Ranges)
return out, nil
}
The ParseRuleFile node processes a Prometheus rule file in YAML format, extracting its groups and rules while validating each rule's PromQL expression. It identifies fatal decoding errors and reports rule-level issues without discarding valid groups and rules.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ParseRuleFile parses a Prometheus rule file (the YAML behind
// `rule_files:` / a PrometheusRule resource's `spec.groups`) into its
// groups and rules. Every rule's `expr` is itself PromQL-validated. A
// document that fails to decode at all (bad YAML, an unknown top-level
// field, or a missing `groups:` key) is reported as the fatal top-level
// error; a document that decodes but has a rule-level problem (a missing
// `expr`, neither/both of alert+record set, a bad duration) still returns
// every group and rule, with each problem listed in rule_errors.
func ParseRuleFile(ctx context.Context, ax axiom.Context, input *gen.RuleFile) (*gen.ParsedRuleFile, error) {
r := pq.ParseRuleFile(input.GetYaml())
out := &gen.ParsedRuleFile{Valid: r.Valid, Error: toGenError(r.Error)}
if !r.Valid {
return out, nil
}
out.Groups = toGenGroups(r.Groups)
out.RuleErrors = toGenErrors(r.RuleErrors)
return out, nil
}
The ValidateQuery node checks the syntactical correctness of a PromQL query and returns a validation result indicating whether the query is well-formed. If the query is not valid, it provides the location of the parse error in terms of line and column numbers.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ValidateQuery reports only whether a PromQL query is syntactically
// well-formed, with the parse error's 1-based line/column when it is not.
// Cheaper than ParseQuery when the caller does not need the full AST
// breakdown.
func ValidateQuery(ctx context.Context, ax axiom.Context, input *gen.PromQLQuery) (*gen.ValidationResult, error) {
valid, err := pq.ValidateExpr(input.GetQuery())
return &gen.ValidationResult{Valid: valid, Error: toGenError(err)}, nil
}
The ValidateRuleFile node checks the structure of a rule file, ensuring it decodes correctly, that each group has a unique name, and that every rule adheres to the required specifications. It returns a comprehensive list of any issues found within the file.
View source
package nodes
import (
"context"
"christiangeorgelucas/promql-tools/axiom"
gen "christiangeorgelucas/promql-tools/gen"
"christiangeorgelucas/promql-tools/internal/pq"
)
// ValidateRuleFile checks a rule file's structure: the document decodes,
// every group has a name (and no two groups share one), and every rule has
// exactly one of alert/record, a valid expr, and (for alerting rules) valid
// for/keep_firing_for durations and label names. Returns every problem
// found, not just the first.
func ValidateRuleFile(ctx context.Context, ax axiom.Context, input *gen.RuleFile) (*gen.RuleFileValidation, error) {
r := pq.ParseRuleFile(input.GetYaml())
if !r.Valid {
return &gen.RuleFileValidation{
Valid: false,
Errors: []*gen.ParseError{toGenError(r.Error)},
}, nil
}
ruleCount := 0
for _, g := range r.Groups {
ruleCount += len(g.Rules)
}
return &gen.RuleFileValidation{
Valid: len(r.RuleErrors) == 0,
Errors: toGenErrors(r.RuleErrors),
GroupCount: int32(len(r.Groups)),
RuleCount: int32(ruleCount),
}, nil
}
Messages (27)
Download .protoA collection of groups extracted from the rule file, each containing associated rules.
A list of errors related to specific rules, detailing issues such as missing expressions or invalid durations.
Use as a tool in any AI agent
christiangeorgelucas/promql-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.
claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.
MCP server setup & other clients →Use this package
Sign in to import christiangeorgelucas/promql-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.
Sign in to useOpen editor