api: replace match[] with expr parameter in /api/v1/info_labels

Replace the match[] parameter with an expr parameter that accepts any
PromQL expression. This enables autocomplete to work with complex
expressions like rate(http_requests_total[5m]) by evaluating the
expression and extracting identifying labels (job, instance) from
the result.

Changes:
- promql/infohelper: change ExtractDataLabels to accept pre-computed
  identifying label values instead of matchers
- web/api/v1: add expr parameter parsing and PromQL evaluation,
  add extractIdentifyingLabels helper function
- web/ui/codemirror-promql: update client to use expr parameter,
  extract full expression text instead of just metric name
- docs: update API documentation with new parameter and examples

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
Arve Knudsen 2026-01-27 08:44:50 +01:00
parent 5bc2e36bde
commit bf72404a5b
9 changed files with 236 additions and 158 deletions

View file

@ -541,10 +541,10 @@ URL query parameters:
- `metric_match=~.*_info` - regex match (`__name__=~".*_info"`)
- `metric_match=!=target_info` - negated exact match (`__name__!="target_info"`)
- `metric_match=!~build_info` - negated regex match (`__name__!~"build_info"`)
- `match[]=<series_selector>`: Repeated series selector argument that filters
which info metrics are considered based on matching identifying labels.
For example, `match[]={job="prometheus"}` returns labels only from info
metrics that have `job="prometheus"`. Optional.
- `expr=<string>`: PromQL expression. If provided, the expression is evaluated
and identifying labels (job, instance) are extracted from the result to filter
which info metrics are returned. This allows filtering by complex expressions
like `rate(http_requests_total[5m])`. Optional.
- `start=<rfc3339 | unix_timestamp>`: Start timestamp. Optional.
- `end=<rfc3339 | unix_timestamp>`: End timestamp. Optional.
- `limit=<number>`: Maximum number of values per label. Optional. 0 means disabled.
@ -588,10 +588,10 @@ curl 'localhost:9090/api/v1/info_labels?metric_match=~.*_info'
}
```
This example filters info labels to only those from targets with `job="prometheus"`:
This example filters info labels to only those from targets matching an expression:
```bash
curl 'localhost:9090/api/v1/info_labels?match[]={job="prometheus"}'
curl 'localhost:9090/api/v1/info_labels?expr=http_requests_total{job="prometheus"}'
```
```json
@ -604,6 +604,24 @@ curl 'localhost:9090/api/v1/info_labels?match[]={job="prometheus"}'
}
```
This example uses a complex PromQL expression to filter info labels based on
the result of a rate query:
```bash
curl 'localhost:9090/api/v1/info_labels?expr=rate(http_requests_total[5m])'
```
```json
{
"status": "success",
"data": {
"version": ["2.0", "2.1"],
"env": ["prod", "staging"],
"region": ["us-east"]
}
}
```
## Querying exemplars
This is **experimental** and might change in the future.

View file

@ -55,11 +55,11 @@ Test the new `/api/v1/info_labels` endpoint:
# Get all data labels from target_info
curl 'http://localhost:9090/api/v1/info_labels'
# Filter by base metrics matching job="api-gateway"
curl 'http://localhost:9090/api/v1/info_labels?match[]={job="api-gateway"}'
# Filter by expression results
curl 'http://localhost:9090/api/v1/info_labels?expr=http_requests_total{job="api-gateway"}'
# Use a different info metric
curl 'http://localhost:9090/api/v1/info_labels?metric=build_info'
curl 'http://localhost:9090/api/v1/info_labels?metric_match=build_info'
```
## Test Data

View file

@ -233,43 +233,60 @@ func appendTestMetrics(db *tsdb.DB) error {
}
// http_requests_total for api-gateway production
// Add multiple data points so rate() works
for _, status := range []string{"200", "404", "500"} {
for _, method := range []string{"GET", "POST"} {
_, err = app.Append(0, labels.FromStrings(
lbls := labels.FromStrings(
"__name__", "http_requests_total",
"job", "api-gateway",
"instance", "prod-gateway-1:8080",
"method", method,
"status", status,
), now, 100)
if err != nil {
return fmt.Errorf("append http_requests_total: %w", err)
)
// Add 6 data points, 1 minute apart, with increasing values
for i := range 6 {
ts := now - int64((5-i)*60*1000) // 5 min ago to now, 1 min intervals
val := float64(100 + i*10) // Increasing counter value
_, err = app.Append(0, lbls, ts, val)
if err != nil {
return fmt.Errorf("append http_requests_total: %w", err)
}
}
}
}
// http_requests_total for api-gateway staging
_, err = app.Append(0, labels.FromStrings(
lbls := labels.FromStrings(
"__name__", "http_requests_total",
"job", "api-gateway",
"instance", "staging-gateway-1:8080",
"method", "GET",
"status", "200",
), now, 50)
if err != nil {
return fmt.Errorf("append http_requests_total: %w", err)
)
for i := range 6 {
ts := now - int64((5-i)*60*1000)
val := float64(50 + i*5)
_, err = app.Append(0, lbls, ts, val)
if err != nil {
return fmt.Errorf("append http_requests_total: %w", err)
}
}
// db_queries_total for database
for _, op := range []string{"SELECT", "INSERT", "UPDATE"} {
_, err = app.Append(0, labels.FromStrings(
lbls := labels.FromStrings(
"__name__", "db_queries_total",
"job", "database",
"instance", "prod-db-1:5432",
"operation", op,
), now, 1000)
if err != nil {
return fmt.Errorf("append db_queries_total: %w", err)
)
for i := range 6 {
ts := now - int64((5-i)*60*1000)
val := float64(1000 + i*100)
_, err = app.Append(0, lbls, ts, val)
if err != nil {
return fmt.Errorf("append db_queries_total: %w", err)
}
}
}
@ -297,7 +314,8 @@ func printInstructions() {
fmt.Println()
fmt.Println("4. Test the info_labels API endpoint:")
fmt.Println(" curl 'http://localhost:9090/api/v1/info_labels'")
fmt.Println(" curl 'http://localhost:9090/api/v1/info_labels?match[]={job=\"api-gateway\"}'")
fmt.Println(" curl 'http://localhost:9090/api/v1/info_labels?expr=http_requests_total{job=\"api-gateway\"}'")
fmt.Println(" curl 'http://localhost:9090/api/v1/info_labels?expr=rate(http_requests_total[5m])'")
fmt.Println()
fmt.Println("Available metrics:")
fmt.Println(" - http_requests_total (api-gateway in prod & staging)")

View file

@ -76,8 +76,9 @@ func NewWithDefaults() *InfoLabelExtractor {
// - ctx: Context for cancellation
// - querier: Storage querier to use for fetching series
// - infoMetricMatcher: Matcher for the info metric __name__ (e.g., MatchEqual "target_info" or MatchRegexp ".*_info")
// - baseMetricMatchers: If provided, only info metrics matching the same identifying labels
// as the base metrics are considered. If nil, all info metrics are returned.
// - identifyingLabelValues: If provided, only info metrics matching these identifying label values
// are considered. The map keys are identifying label names (e.g., "job", "instance") and values
// are sets of label values. If nil or empty, all info metrics are returned.
// - hints: Select hints for the storage layer
//
// Returns a map where keys are label names and values are slices of unique values for that label.
@ -85,7 +86,7 @@ func (e *InfoLabelExtractor) ExtractDataLabels(
ctx context.Context,
querier storage.Querier,
infoMetricMatcher *labels.Matcher,
baseMetricMatchers [][]*labels.Matcher,
identifyingLabelValues map[string]map[string]struct{},
hints *storage.SelectHints,
) (map[string][]string, annotations.Annotations, error) {
var warnings annotations.Annotations
@ -93,22 +94,10 @@ func (e *InfoLabelExtractor) ExtractDataLabels(
// Build matchers for the info metric query
infoMatchers := []*labels.Matcher{infoMetricMatcher}
// If base metric matchers are provided, collect identifying label values from base metrics
// and filter info metrics by those values
if len(baseMetricMatchers) > 0 {
idLblValues, baseWarnings, err := e.collectIdentifyingLabelValues(ctx, querier, baseMetricMatchers, hints)
warnings.Merge(baseWarnings)
if err != nil {
return nil, warnings, err
}
// If no identifying label values found, return empty result
if len(idLblValues) == 0 {
return map[string][]string{}, warnings, nil
}
// If identifying label values are provided, filter info metrics by those values
if len(identifyingLabelValues) > 0 {
// Add regex matchers for identifying labels
for name, vals := range idLblValues {
for name, vals := range identifyingLabelValues {
infoMatchers = append(infoMatchers, labels.MustNewMatcher(labels.MatchRegexp, name, BuildRegexpAlternation(vals)))
}
}
@ -121,6 +110,11 @@ func (e *InfoLabelExtractor) ExtractDataLabels(
dataLabels := make(map[string]map[string]struct{})
for infoSet.Next() {
// Check for context cancellation periodically
if ctx.Err() != nil {
return nil, warnings, ctx.Err()
}
series := infoSet.At()
lbls := series.Labels()
@ -161,45 +155,6 @@ func (e *InfoLabelExtractor) ExtractDataLabels(
return result, warnings, nil
}
// collectIdentifyingLabelValues queries base metrics and extracts values for identifying labels.
func (e *InfoLabelExtractor) collectIdentifyingLabelValues(
ctx context.Context,
querier storage.Querier,
matcherSets [][]*labels.Matcher,
hints *storage.SelectHints,
) (map[string]map[string]struct{}, annotations.Annotations, error) {
idLblValues := make(map[string]map[string]struct{})
var warnings annotations.Annotations
for _, matchers := range matcherSets {
set := querier.Select(ctx, false, hints, matchers...)
warnings.Merge(set.Warnings())
for set.Next() {
series := set.At()
lbls := series.Labels()
// Extract identifying labels
for _, idLbl := range e.config.IdentifyingLabels {
val := lbls.Get(idLbl)
if val == "" {
continue
}
if idLblValues[idLbl] == nil {
idLblValues[idLbl] = make(map[string]struct{})
}
idLblValues[idLbl][val] = struct{}{}
}
}
if err := set.Err(); err != nil {
return nil, warnings, err
}
}
return idLblValues, warnings, nil
}
// IdentifyingLabels returns the identifying labels configured for this extractor.
func (e *InfoLabelExtractor) IdentifyingLabels() []string {
return e.config.IdentifyingLabels

View file

@ -40,10 +40,10 @@ func TestExtractDataLabels(t *testing.T) {
t.Cleanup(func() { testStorage.Close() })
tests := []struct {
name string
infoMetricMatcher *labels.Matcher
baseMetricMatchers [][]*labels.Matcher
expectedLabels map[string][]string
name string
infoMetricMatcher *labels.Matcher
identifyingLabelValues map[string]map[string]struct{}
expectedLabels map[string][]string
}{
{
name: "all target_info labels without filter",
@ -57,8 +57,8 @@ func TestExtractDataLabels(t *testing.T) {
{
name: "filter by job=prometheus",
infoMetricMatcher: labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "target_info"),
baseMetricMatchers: [][]*labels.Matcher{
{labels.MustNewMatcher(labels.MatchEqual, "job", "prometheus")},
identifyingLabelValues: map[string]map[string]struct{}{
"job": {"prometheus": {}},
},
expectedLabels: map[string][]string{
"version": {"2.0", "2.1"},
@ -68,8 +68,8 @@ func TestExtractDataLabels(t *testing.T) {
{
name: "filter by specific instance",
infoMetricMatcher: labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "target_info"),
baseMetricMatchers: [][]*labels.Matcher{
{labels.MustNewMatcher(labels.MatchEqual, "instance", "localhost:9090")},
identifyingLabelValues: map[string]map[string]struct{}{
"instance": {"localhost:9090": {}},
},
expectedLabels: map[string][]string{
"version": {"2.0"},
@ -77,11 +77,11 @@ func TestExtractDataLabels(t *testing.T) {
},
},
{
name: "filter with multiple matcher sets",
name: "filter with multiple identifying values",
infoMetricMatcher: labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "target_info"),
baseMetricMatchers: [][]*labels.Matcher{
{labels.MustNewMatcher(labels.MatchEqual, "job", "prometheus"), labels.MustNewMatcher(labels.MatchEqual, "instance", "localhost:9090")},
{labels.MustNewMatcher(labels.MatchEqual, "job", "node")},
identifyingLabelValues: map[string]map[string]struct{}{
"job": {"prometheus": {}, "node": {}},
"instance": {"localhost:9090": {}, "node1:9100": {}},
},
expectedLabels: map[string][]string{
"version": {"1.0", "2.0"},
@ -102,10 +102,10 @@ func TestExtractDataLabels(t *testing.T) {
expectedLabels: map[string][]string{},
},
{
name: "no matching base metrics",
name: "no matching identifying labels",
infoMetricMatcher: labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "target_info"),
baseMetricMatchers: [][]*labels.Matcher{
{labels.MustNewMatcher(labels.MatchEqual, "job", "nonexistent")},
identifyingLabelValues: map[string]map[string]struct{}{
"job": {"nonexistent": {}},
},
expectedLabels: map[string][]string{},
},
@ -136,7 +136,7 @@ func TestExtractDataLabels(t *testing.T) {
Func: "info_labels",
}
result, _, err := extractor.ExtractDataLabels(ctx, q, tc.infoMetricMatcher, tc.baseMetricMatchers, hints)
result, _, err := extractor.ExtractDataLabels(ctx, q, tc.infoMetricMatcher, tc.identifyingLabelValues, hints)
require.NoError(t, err)
require.Equal(t, tc.expectedLabels, result)
})

View file

@ -935,8 +935,9 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) {
// Query parameters:
// - metric_match: Matcher for the info metric name (default: "target_info").
// Supports =, =~, !=, !~ operators (e.g., "target_info", "~.*_info", "!=build_info").
// - match[]: Optional series selectors to filter info metrics by base metrics' identifying labels.
// If provided, only info metrics with matching job/instance values are returned.
// - expr: Optional PromQL expression. If provided, the expression is evaluated and
// identifying labels (job, instance) are extracted from the result to filter info metrics.
// Note that some use cases require for this to be optional, as they want to e.g. get all labels off of target_info.
// - start/end: Time range for the query (default: last 12 hours)
// - limit: Maximum number of values per label to return
func (api *API) infoLabels(r *http.Request) (result apiFuncResult) {
@ -973,13 +974,41 @@ func (api *API) infoLabels(r *http.Request) (result apiFuncResult) {
return invalidParamError(err, "end")
}
// Parse match[] for filtering by base metrics
var baseMetricMatchers [][]*labels.Matcher
if len(r.Form["match[]"]) > 0 {
baseMetricMatchers, err = parseMatchersParam(r.Form["match[]"])
// Parse expr parameter - if provided, evaluate as PromQL and extract identifying labels
var identifyingLabelValues map[string]map[string]struct{}
var exprWarnings annotations.Annotations
if exprParam := r.FormValue("expr"); exprParam != "" {
opts, err := extractQueryOpts(r)
if err != nil {
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
}
qry, err := api.QueryEngine.NewInstantQuery(ctx, api.Queryable, opts, exprParam, end)
if err != nil {
return invalidParamError(err, "expr")
}
defer qry.Close()
res := qry.Exec(ctx)
if res.Err != nil {
return apiFuncResult{nil, returnAPIError(res.Err), res.Warnings, nil}
}
exprWarnings = res.Warnings
// Only Vector and Matrix results have labels to extract
switch res.Value.Type() {
case parser.ValueTypeVector, parser.ValueTypeMatrix:
// OK - these have labels
default:
return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("expr must return series (vector or matrix), got %s", res.Value.Type())}, exprWarnings, nil}
}
// Extract identifying labels from the result
identifyingLabelValues = extractIdentifyingLabels(res.Value, extractor.IdentifyingLabels())
// If no identifying labels were found, return empty result early
if len(identifyingLabelValues) == 0 {
return apiFuncResult{map[string][]string{}, nil, exprWarnings, nil}
}
}
// Create querier
@ -1002,7 +1031,8 @@ func (api *API) infoLabels(r *http.Request) (result apiFuncResult) {
Func: "info_labels",
}
dataLabels, warnings, err := extractor.ExtractDataLabels(ctx, q, infoMetricMatcher, baseMetricMatchers, hints)
dataLabels, warnings, err := extractor.ExtractDataLabels(ctx, q, infoMetricMatcher, identifyingLabelValues, hints)
warnings.Merge(exprWarnings)
if err != nil {
return apiFuncResult{nil, returnAPIError(err), warnings, closer}
}
@ -1020,6 +1050,39 @@ func (api *API) infoLabels(r *http.Request) (result apiFuncResult) {
return apiFuncResult{dataLabels, nil, warnings, closer}
}
// extractIdentifyingLabels extracts identifying label values from a PromQL query result.
// It iterates through Vector or Matrix results and collects all unique values for the
// specified identifying labels (typically "job" and "instance").
func extractIdentifyingLabels(val parser.Value, identifyingLabels []string) map[string]map[string]struct{} {
result := make(map[string]map[string]struct{})
switch v := val.(type) {
case promql.Vector:
for _, sample := range v {
for _, idLbl := range identifyingLabels {
if val := sample.Metric.Get(idLbl); val != "" {
if result[idLbl] == nil {
result[idLbl] = make(map[string]struct{})
}
result[idLbl][val] = struct{}{}
}
}
}
case promql.Matrix:
for _, series := range v {
for _, idLbl := range identifyingLabels {
if val := series.Metric.Get(idLbl); val != "" {
if result[idLbl] == nil {
result[idLbl] = make(map[string]struct{})
}
result[idLbl][val] = struct{}{}
}
}
}
}
return result
}
var (
// MinTime is the default timestamp used for the start of optional time ranges.
// Exposed to let downstream projects reference it.

View file

@ -915,7 +915,8 @@ func TestInfoLabels(t *testing.T) {
`)
api := &API{
Queryable: storage,
Queryable: storage,
QueryEngine: testEngine(t),
}
request := func(method string, params map[string][]string) (*http.Request, error) {
@ -954,8 +955,8 @@ func TestInfoLabels(t *testing.T) {
api: api,
},
{
name: "filter by job=prometheus",
params: map[string][]string{"match[]": {`{job="prometheus"}`}, "start": {"0"}, "end": {"100000"}},
name: "filter by job=prometheus using expr",
params: map[string][]string{"expr": {`http_requests_total{job="prometheus"}`}, "start": {"0"}, "end": {"120"}},
expected: map[string][]string{
"version": {"2.0", "2.1"},
"env": {"prod", "staging"},
@ -963,8 +964,8 @@ func TestInfoLabels(t *testing.T) {
api: api,
},
{
name: "filter by specific instance",
params: map[string][]string{"match[]": {`{instance="localhost:9090"}`}, "start": {"0"}, "end": {"100000"}},
name: "filter by specific instance using expr",
params: map[string][]string{"expr": {`http_requests_total{instance="localhost:9090"}`}, "start": {"0"}, "end": {"120"}},
expected: map[string][]string{
"version": {"2.0"},
"env": {"prod"},
@ -972,8 +973,17 @@ func TestInfoLabels(t *testing.T) {
api: api,
},
{
name: "multiple matchers",
params: map[string][]string{"match[]": {`{job="prometheus", instance="localhost:9090"}`, `{job="node"}`}, "start": {"0"}, "end": {"100000"}},
name: "filter with aggregation expression",
params: map[string][]string{"expr": {`sum by (job, instance) (http_requests_total{job="prometheus", instance="localhost:9090"})`}, "start": {"0"}, "end": {"120"}},
expected: map[string][]string{
"version": {"2.0"},
"env": {"prod"},
},
api: api,
},
{
name: "filter with regex in expr",
params: map[string][]string{"expr": {`http_requests_total{job=~"prometheus|node", instance=~"localhost:9090|node1:9100"}`}, "start": {"0"}, "end": {"120"}},
expected: map[string][]string{
"version": {"1.0", "2.0"},
"env": {"prod"},
@ -1028,8 +1038,8 @@ func TestInfoLabels(t *testing.T) {
api: api,
},
{
name: "no matching base metrics",
params: map[string][]string{"match[]": {`{job="nonexistent"}`}, "start": {"0"}, "end": {"100000"}},
name: "no matching expr results",
params: map[string][]string{"expr": {`http_requests_total{job="nonexistent"}`}, "start": {"0"}, "end": {"120"}},
expected: map[string][]string{},
api: api,
},
@ -1044,13 +1054,26 @@ func TestInfoLabels(t *testing.T) {
api: api,
},
{
name: "exec error type",
params: map[string][]string{"match[]": {`{foo="boo"}`}},
name: "exec error from queryable",
params: map[string][]string{"start": {"0"}, "end": {"100000"}},
expectedErrorType: errorExec,
api: &API{
Queryable: errorTestQueryable{err: errors.New("generic")},
Queryable: errorTestQueryable{err: errors.New("generic")},
QueryEngine: testEngine(t),
},
},
{
name: "invalid expr parameter",
params: map[string][]string{"expr": {`invalid{`}, "start": {"0"}, "end": {"100000"}},
expectedErrorType: errorBadData,
api: api,
},
{
name: "scalar expr returns error",
params: map[string][]string{"expr": {`1+1`}, "start": {"0"}, "end": {"100000"}},
expectedErrorType: errorBadData,
api: api,
},
} {
t.Run(tc.name, func(t *testing.T) {
for _, method := range []string{http.MethodGet, http.MethodPost} {

View file

@ -41,10 +41,10 @@ export interface PrometheusClient {
flags(): Promise<Record<string, string>>;
// infoLabelPairs returns data labels from info metrics (like target_info) and their values.
// If metricName is provided, only returns labels from info metrics associated with that metric
// via identifying labels (job, instance).
// If expr is provided, the expression is evaluated and identifying labels (job, instance)
// are extracted from the result to filter which info metrics are returned.
// If metricMatch is provided, it specifies which info metrics to query (supports =, =~, !=, !~).
infoLabelPairs(metricName?: string, metricMatch?: string): Promise<Record<string, string[]>>;
infoLabelPairs(expr?: string, metricMatch?: string): Promise<Record<string, string[]>>;
// destroy is called to release all resources held by this client
destroy?(): void;
@ -209,7 +209,7 @@ export class HTTPPrometheusClient implements PrometheusClient {
});
}
infoLabelPairs(metricName?: string, metricMatch?: string): Promise<Record<string, string[]>> {
infoLabelPairs(expr?: string, metricMatch?: string): Promise<Record<string, string[]>> {
const params: URLSearchParams = new URLSearchParams();
if (this.lookbackInterval) {
const end = new Date();
@ -217,18 +217,26 @@ export class HTTPPrometheusClient implements PrometheusClient {
params.set('start', start.toISOString());
params.set('end', end.toISOString());
}
if (metricName) {
params.set('match[]', metricName);
if (expr) {
params.set('expr', expr);
}
if (metricMatch) {
params.set('metric_match', metricMatch);
}
return this.fetchAPI<Record<string, string[]>>(`${this.infoLabelsEndpoint()}?${params}`).catch((error) => {
if (this.errorHandler) {
this.errorHandler(error);
}
return {};
});
return this.fetchAPI<Record<string, string[]>>(`${this.infoLabelsEndpoint()}?${params}`)
.then((data) => {
// Validate response is an object (API could return null or array on error)
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
return {};
}
return data;
})
.catch((error) => {
if (this.errorHandler) {
this.errorHandler(error);
}
return {};
});
}
destroy(): void {
@ -509,16 +517,17 @@ export class CachedPrometheusClient implements PrometheusClient {
});
}
infoLabelPairs(metricName?: string, metricMatch?: string): Promise<Record<string, string[]>> {
infoLabelPairs(expr?: string, metricMatch?: string): Promise<Record<string, string[]>> {
// Info labels are expected to be relatively stable, so we cache them.
// The cache key includes the metric name and metric match.
const cacheKey = `infoLabels_${metricName || ''}_${metricMatch || ''}`;
// The cache key includes the expression and metric match.
// Use JSON.stringify to avoid collisions when parameters contain underscores.
const cacheKey = JSON.stringify(['infoLabels', expr || '', metricMatch || '']);
const cached = this.cache.getInfoLabelPairs(cacheKey);
if (cached && Object.keys(cached).length > 0) {
if (cached !== undefined) {
return Promise.resolve(cached);
}
return this.client.infoLabelPairs(metricName, metricMatch).then((labels) => {
return this.client.infoLabelPairs(expr, metricMatch).then((labels) => {
this.cache.setInfoLabelPairs(cacheKey, labels);
return labels;
});

View file

@ -125,29 +125,19 @@ export interface Context {
// extractMetricName extracts the metric name from an expression node.
// It traverses the tree to find an Identifier node and returns its text.
function extractMetricName(node: SyntaxNode, state: EditorState): string {
// Try to find an Identifier node in the subtree using recursive descent
function findIdentifier(n: SyntaxNode): string {
if (n.type.id === Identifier) {
return state.sliceDoc(n.from, n.to);
}
// Check children
for (let child = n.firstChild; child; child = child.nextSibling) {
const result = findIdentifier(child);
if (result) {
return result;
}
}
return '';
}
return findIdentifier(node);
function extractExpression(node: SyntaxNode, state: EditorState): string {
// Extract the full text of the expression node.
// This is used to pass the complete expression (e.g., "rate(http_requests_total[5m])")
// to the API for evaluating complex PromQL expressions.
return state.sliceDoc(node.from, node.to);
}
// InfoFunctionContext contains information about the info() function call context.
interface InfoFunctionContext {
// Whether we're inside the second argument of info()
isInSecondArg: boolean;
// The metric expression from the first argument (e.g., "http_requests_total")
// The full expression from the first argument (e.g., "rate(http_requests_total[5m])" or "http_requests_total")
// This is passed to the API's expr parameter for evaluation.
metricName: string;
// The __name__ matcher from the second argument, if present (e.g., "~.*_info" for =~, "!=target_info" for !=)
// Format matches the API's metric_match parameter
@ -155,7 +145,7 @@ interface InfoFunctionContext {
}
// getInfoFunctionContext checks if we're inside the second argument (data label matchers)
// of the info() function and extracts the metric name from the first argument.
// of the info() function and extracts the expression from the first argument.
function getInfoFunctionContext(node: SyntaxNode, state: EditorState): InfoFunctionContext {
const notInInfo: InfoFunctionContext = { isInSecondArg: false, metricName: '' };
@ -178,8 +168,8 @@ function getInfoFunctionContext(node: SyntaxNode, state: EditorState): InfoFunct
const firstArg = current.firstChild;
if (firstArg !== null && node.from >= firstArg.to) {
// Extract the metric name from the first argument
// The first arg is typically a VectorSelector with an Identifier child
const metricName = extractMetricName(firstArg, state);
// Extract the full expression from the first argument (e.g., "rate(http_requests_total[5m])")
const metricName = extractExpression(firstArg, state);
// Extract __name__ matcher from the second argument (LabelMatchers)
const infoMetricMatch = extractInfoMetricMatch(current, state);
return { isInSecondArg: true, metricName, infoMetricMatch };
@ -202,10 +192,16 @@ function extractInfoMetricMatch(functionCallBody: SyntaxNode, state: EditorState
// e.g., info(metric, {__name__=~"target_info"}) parses as:
// FunctionCallBody > VectorSelector > LabelMatchers
let labelMatchersNode: SyntaxNode | null = null;
let isSecondArg = false;
let argIndex = 0;
for (let child = functionCallBody.firstChild; child !== null; child = child.nextSibling) {
if (isSecondArg) {
// Second argument - look for LabelMatchers directly or inside VectorSelector
// Skip punctuation nodes (opening paren, comma, closing paren)
const nodeText = state.sliceDoc(child.from, child.to);
if (nodeText === '(' || nodeText === ')' || nodeText === ',') {
continue;
}
argIndex++;
if (argIndex >= 2) {
// Second argument or later - look for LabelMatchers directly or inside VectorSelector
if (child.type.id === LabelMatchers) {
labelMatchersNode = child;
break;
@ -215,10 +211,6 @@ function extractInfoMetricMatch(functionCallBody: SyntaxNode, state: EditorState
break;
}
}
// After first non-trivial child, we're in the second argument territory
if (child.type.id === VectorSelector || child.type.id === Identifier) {
isSecondArg = true;
}
}
if (!labelMatchersNode) {
return undefined;