mirror of
https://github.com/prometheus/prometheus.git
synced 2026-02-03 20:39:32 -05:00
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:
parent
5bc2e36bde
commit
bf72404a5b
9 changed files with 236 additions and 158 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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} {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue