diff --git a/docs/querying/api.md b/docs/querying/api.md index 10c91b4310..111a4e205f 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -1568,28 +1568,28 @@ NOTE: This endpoint is available before the server has been marked ready and is The following endpoint returns Prometheus' own instrumentation metrics from its internal client registry as structured JSON. These are the same metrics that are exposed on the `/metrics` endpoint in Prometheus text exposition format, but returned as JSON for programmatic access by the web UI. +The response uses the standard [ProtoJSON](https://protobuf.dev/programming-guides/json/) representation of the `io.prometheus.client.MetricFamily` protocol buffer message. + ``` GET /api/v1/status/self_metrics ``` URL query parameters: -- `metric_name=`: Filter returned metric families by name prefix. For example, `metric_name=prometheus_tsdb` returns all metric families whose names start with `prometheus_tsdb`. Optional. When omitted, all metric families are returned. +- `metric_name_pattern=`: A regular expression filter for metric names (fully anchored, like PromQL label matchers). Only metric families whose names fully match the pattern are returned. For example, `metric_name_pattern=prometheus_tsdb_.*` returns all metric families whose names start with `prometheus_tsdb_`. Optional. When omitted, all metric families are returned. -Each returned metric family contains: +Each returned metric family is a ProtoJSON-encoded `MetricFamily` containing: - **name**: The metric name. - **help**: The metric help string. - **type**: The metric type (`COUNTER`, `GAUGE`, `SUMMARY`, `HISTOGRAM`, `UNTYPED`). - **unit**: The metric unit, if set (optional). -- **metrics**: A list of individual metric samples, each containing: - - **labels**: A map of label name/value pairs (omitted when empty). - - **value**: The metric value as a string. For summaries and histograms, this contains the sum and count. - - **quantiles**: For summary metrics, a list of quantile/value pairs. - - **buckets**: For histogram metrics, a list of upper bound / cumulative count pairs. +- **metric**: A list of individual metrics, each containing: + - **label**: A list of `{name, value}` label pairs. + - **gauge**, **counter**, **summary**, **histogram**, or **untyped**: The type-specific metric data. ```bash -curl 'http://localhost:9090/api/v1/status/self_metrics?metric_name=prometheus_build' +curl 'http://localhost:9090/api/v1/status/self_metrics?metric_name_pattern=prometheus_build_info' ``` ```json @@ -1600,18 +1600,20 @@ curl 'http://localhost:9090/api/v1/status/self_metrics?metric_name=prometheus_bu "name": "prometheus_build_info", "help": "A metric with a constant '1' value labeled by version, revision, branch, goversion from which prometheus was built, and the goos and goarch for the build.", "type": "GAUGE", - "metrics": [ + "metric": [ { - "labels": { - "branch": "main", - "goarch": "amd64", - "goos": "linux", - "goversion": "go1.26.1-X:nodwarf5", - "revision": "7b5a4090e38d9e1ad7697c7641234f4ed135a6c7", - "tags": "netgo,builtinassets", - "version": "3.11.0-rc.0" - }, - "value": "1" + "label": [ + { "name": "branch", "value": "main" }, + { "name": "goarch", "value": "amd64" }, + { "name": "goos", "value": "linux" }, + { "name": "goversion", "value": "go1.26.1-X:nodwarf5" }, + { "name": "revision", "value": "7b5a4090e38d9e1ad7697c7641234f4ed135a6c7" }, + { "name": "tags", "value": "netgo,builtinassets" }, + { "name": "version", "value": "3.11.0-rc.0" } + ], + "gauge": { + "value": 1 + } } ] } diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 5af406fafb..28ad09051e 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -41,6 +41,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" "github.com/prometheus/common/route" + "google.golang.org/protobuf/encoding/protojson" "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/model/labels" @@ -1866,101 +1867,33 @@ func TSDBStatsFromIndexStats(stats []index.Stat) []TSDBStat { return result } -// SelfMetricFamily represents a single metric family from Prometheus' own instrumentation registry. -type SelfMetricFamily struct { - Name string `json:"name"` - Help string `json:"help"` - Type string `json:"type"` - Unit string `json:"unit,omitempty"` - Metrics []SelfMetric `json:"metrics"` -} - -// SelfMetric represents a single metric sample within a metric family. -type SelfMetric struct { - Labels map[string]string `json:"labels,omitempty"` - Value string `json:"value,omitempty"` - Quantiles []SelfMetricQuantile `json:"quantiles,omitempty"` - Buckets []SelfMetricBucket `json:"buckets,omitempty"` -} - -// SelfMetricQuantile represents a single quantile within a summary metric. -type SelfMetricQuantile struct { - Quantile string `json:"quantile"` - Value string `json:"value"` -} - -// SelfMetricBucket represents a single bucket within a histogram metric. -type SelfMetricBucket struct { - UpperBound string `json:"upperBound"` - CumulativeCount string `json:"cumulativeCount"` -} - func (api *API) selfMetrics(r *http.Request) apiFuncResult { - metricNameFilter := r.FormValue("metric_name") + var nameFilter *regexp.Regexp + if pattern := r.FormValue("metric_name_pattern"); pattern != "" { + var err error + nameFilter, err = regexp.Compile("^(?:" + pattern + ")$") + if err != nil { + return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("invalid metric_name_pattern: %w", err)}, nil, nil} + } + } mfs, err := api.gatherer.Gather() if err != nil { return apiFuncResult{nil, &apiError{errorInternal, fmt.Errorf("error gathering self metrics: %w", err)}, nil, nil} } - result := make([]SelfMetricFamily, 0, len(mfs)) + marshaler := protojson.MarshalOptions{} + result := make([]json.RawMessage, 0, len(mfs)) for _, mf := range mfs { - name := mf.GetName() - if metricNameFilter != "" && !strings.HasPrefix(name, metricNameFilter) { + if nameFilter != nil && !nameFilter.MatchString(mf.GetName()) { continue } - family := SelfMetricFamily{ - Name: name, - Help: mf.GetHelp(), - Type: mf.GetType().String(), - Unit: mf.GetUnit(), - Metrics: make([]SelfMetric, 0, len(mf.GetMetric())), + b, err := marshaler.Marshal(mf) + if err != nil { + return apiFuncResult{nil, &apiError{errorInternal, fmt.Errorf("error marshaling metric family %q: %w", mf.GetName(), err)}, nil, nil} } - - for _, m := range mf.GetMetric() { - sm := SelfMetric{} - - if len(m.GetLabel()) > 0 { - sm.Labels = make(map[string]string, len(m.GetLabel())) - for _, l := range m.GetLabel() { - sm.Labels[l.GetName()] = l.GetValue() - } - } - - switch { - case m.Gauge != nil: - sm.Value = strconv.FormatFloat(m.GetGauge().GetValue(), 'g', -1, 64) - case m.Counter != nil: - sm.Value = strconv.FormatFloat(m.GetCounter().GetValue(), 'g', -1, 64) - case m.Untyped != nil: - sm.Value = strconv.FormatFloat(m.GetUntyped().GetValue(), 'g', -1, 64) - case m.Summary != nil: - s := m.GetSummary() - sm.Value = strconv.FormatFloat(s.GetSampleSum(), 'g', -1, 64) + " (sum), " + strconv.FormatUint(s.GetSampleCount(), 10) + " (count)" - sm.Quantiles = make([]SelfMetricQuantile, 0, len(s.GetQuantile())) - for _, q := range s.GetQuantile() { - sm.Quantiles = append(sm.Quantiles, SelfMetricQuantile{ - Quantile: strconv.FormatFloat(q.GetQuantile(), 'g', -1, 64), - Value: strconv.FormatFloat(q.GetValue(), 'g', -1, 64), - }) - } - case m.Histogram != nil: - h := m.GetHistogram() - sm.Value = strconv.FormatFloat(h.GetSampleSum(), 'g', -1, 64) + " (sum), " + strconv.FormatUint(h.GetSampleCount(), 10) + " (count)" - sm.Buckets = make([]SelfMetricBucket, 0, len(h.GetBucket())) - for _, b := range h.GetBucket() { - sm.Buckets = append(sm.Buckets, SelfMetricBucket{ - UpperBound: strconv.FormatFloat(b.GetUpperBound(), 'g', -1, 64), - CumulativeCount: strconv.FormatUint(b.GetCumulativeCount(), 10), - }) - } - } - - family.Metrics = append(family.Metrics, sm) - } - - result = append(result, family) + result = append(result, json.RawMessage(b)) } return apiFuncResult{result, nil, nil, nil} diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 1329284e4f..e4a3dc59d7 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -4558,48 +4558,62 @@ func TestSelfMetrics(t *testing.T) { require.NoError(t, err) res := api.selfMetrics(req) assertAPIError(t, res.err, errorNone) - families, ok := res.data.([]SelfMetricFamily) - require.True(t, ok, "expected []SelfMetricFamily") + families, ok := res.data.([]json.RawMessage) + require.True(t, ok, "expected []json.RawMessage") require.NotEmpty(t, families, "expected at least one metric family from default gatherer") }) - t.Run("filters metrics by prefix", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "?metric_name=go_", http.NoBody) + t.Run("filters metrics by regex", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "?metric_name_pattern=go_.*", http.NoBody) require.NoError(t, err) - req.Form = url.Values{"metric_name": {"go_"}} + req.Form = url.Values{"metric_name_pattern": {"go_.*"}} res := api.selfMetrics(req) assertAPIError(t, res.err, errorNone) - families, ok := res.data.([]SelfMetricFamily) - require.True(t, ok, "expected []SelfMetricFamily") - for _, f := range families { - require.True(t, strings.HasPrefix(f.Name, "go_"), "expected metric name to start with 'go_', got %q", f.Name) + families, ok := res.data.([]json.RawMessage) + require.True(t, ok, "expected []json.RawMessage") + require.NotEmpty(t, families) + for _, raw := range families { + var f map[string]any + require.NoError(t, json.Unmarshal(raw, &f)) + name, _ := f["name"].(string) + require.True(t, strings.HasPrefix(name, "go_"), "expected metric name to start with 'go_', got %q", name) } }) - t.Run("non-matching prefix returns empty", func(t *testing.T) { - req, err := http.NewRequest(http.MethodGet, "?metric_name=nonexistent_prefix_xyz_", http.NoBody) + t.Run("non-matching pattern returns empty", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "?metric_name_pattern=nonexistent_prefix_xyz_", http.NoBody) require.NoError(t, err) - req.Form = url.Values{"metric_name": {"nonexistent_prefix_xyz_"}} + req.Form = url.Values{"metric_name_pattern": {"nonexistent_prefix_xyz_"}} res := api.selfMetrics(req) assertAPIError(t, res.err, errorNone) - families, ok := res.data.([]SelfMetricFamily) - require.True(t, ok, "expected []SelfMetricFamily") + families, ok := res.data.([]json.RawMessage) + require.True(t, ok, "expected []json.RawMessage") require.Empty(t, families) }) - t.Run("metric families have expected structure", func(t *testing.T) { + t.Run("invalid regex returns error", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "?metric_name_pattern=[invalid", http.NoBody) + require.NoError(t, err) + req.Form = url.Values{"metric_name_pattern": {"[invalid"}} + res := api.selfMetrics(req) + assertAPIError(t, res.err, errorBadData) + }) + + t.Run("metric families have expected ProtoJSON structure", func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "", http.NoBody) require.NoError(t, err) res := api.selfMetrics(req) assertAPIError(t, res.err, errorNone) - families := res.data.([]SelfMetricFamily) - for _, f := range families { - require.NotEmpty(t, f.Name, "metric family name should not be empty") - require.NotEmpty(t, f.Type, "metric family type should not be empty") - for _, m := range f.Metrics { - // Every metric should have a value set. - require.NotEmpty(t, m.Value, "metric value should not be empty for %s", f.Name) - } + families := res.data.([]json.RawMessage) + for _, raw := range families { + var f map[string]any + require.NoError(t, json.Unmarshal(raw, &f)) + name, _ := f["name"].(string) + require.NotEmpty(t, name, "metric family name should not be empty") + typ, _ := f["type"].(string) + require.NotEmpty(t, typ, "metric family type should not be empty") + metrics, _ := f["metric"].([]any) + require.NotEmpty(t, metrics, "metric family %q should have at least one metric", name) } }) } diff --git a/web/api/v1/openapi_examples.go b/web/api/v1/openapi_examples.go index fab40cf4c4..25637adcde 100644 --- a/web/api/v1/openapi_examples.go +++ b/web/api/v1/openapi_examples.go @@ -891,7 +891,7 @@ func statusSelfMetricsResponseExamples() *orderedmap.Map[string, *base.Example] examples := orderedmap.New[string, *base.Example]() examples.Set("selfMetrics", &base.Example{ - Summary: "Prometheus self-instrumentation metrics", + Summary: "Prometheus self-instrumentation metrics in ProtoJSON format", Value: createYAMLNode(map[string]any{ "status": "success", "data": []map[string]any{ @@ -899,18 +899,20 @@ func statusSelfMetricsResponseExamples() *orderedmap.Map[string, *base.Example] "name": "prometheus_build_info", "help": "A metric with a constant '1' value labeled by version, revision, branch, goversion from which prometheus was built, and the goos and goarch for the build.", "type": "GAUGE", - "metrics": []map[string]any{ + "metric": []map[string]any{ { - "labels": map[string]string{ - "branch": "HEAD", - "goarch": "amd64", - "goos": "linux", - "goversion": "go1.23.0", - "revision": "abc1234", - "tags": "netgo,builtinassets,stringlabels", - "version": "3.0.0", + "label": []map[string]string{ + {"name": "branch", "value": "HEAD"}, + {"name": "goarch", "value": "amd64"}, + {"name": "goos", "value": "linux"}, + {"name": "goversion", "value": "go1.23.0"}, + {"name": "revision", "value": "abc1234"}, + {"name": "tags", "value": "netgo,builtinassets,stringlabels"}, + {"name": "version", "value": "3.0.0"}, + }, + "gauge": map[string]any{ + "value": 1, }, - "value": "1", }, }, }, @@ -918,9 +920,11 @@ func statusSelfMetricsResponseExamples() *orderedmap.Map[string, *base.Example] "name": "prometheus_tsdb_head_chunks", "help": "Total number of chunks in the head block.", "type": "GAUGE", - "metrics": []map[string]any{ + "metric": []map[string]any{ { - "value": "1024", + "gauge": map[string]any{ + "value": 1024, + }, }, }, }, diff --git a/web/api/v1/openapi_paths.go b/web/api/v1/openapi_paths.go index ad1a4e6882..6aa4310637 100644 --- a/web/api/v1/openapi_paths.go +++ b/web/api/v1/openapi_paths.go @@ -443,13 +443,13 @@ func (*OpenAPIBuilder) statusWALReplayPath() *v3.PathItem { func (*OpenAPIBuilder) statusSelfMetricsPath() *v3.PathItem { params := []*v3.Parameter{ - queryParamWithExample("metric_name", "Filter metrics by name prefix.", false, stringSchema(), []example{{"example", "prometheus_tsdb"}}), + queryParamWithExample("metric_name_pattern", "Regular expression filter for metric names (fully anchored, like PromQL label matchers). Only metric families whose names fully match the pattern are returned.", false, stringSchema(), []example{{"example", "prometheus_tsdb_.*"}}), } return &v3.PathItem{ Get: &v3.Operation{ OperationId: "get-status-self-metrics", Summary: "Get Prometheus self-instrumentation metrics", - Description: "Returns Prometheus' own instrumentation metrics from its internal client registry, as structured JSON. Supports optional prefix filtering via the metric_name parameter.", + Description: "Returns Prometheus' own instrumentation metrics from its internal client registry, as structured JSON. Supports optional regex filtering via the metric_name_pattern parameter.", Tags: []string{"status"}, Parameters: params, Responses: responsesWithErrorExamples("StatusSelfMetricsOutputBody", statusSelfMetricsResponseExamples(), errorResponseExamples(), "Self metrics retrieved successfully.", "Error retrieving self metrics."), diff --git a/web/api/v1/testdata/openapi_3.1_golden.yaml b/web/api/v1/testdata/openapi_3.1_golden.yaml index d912be1ae7..e1282e2658 100644 --- a/web/api/v1/testdata/openapi_3.1_golden.yaml +++ b/web/api/v1/testdata/openapi_3.1_golden.yaml @@ -2079,19 +2079,19 @@ paths: tags: - status summary: Get Prometheus self-instrumentation metrics - description: Returns Prometheus' own instrumentation metrics from its internal client registry, as structured JSON. Supports optional prefix filtering via the metric_name parameter. + description: Returns Prometheus' own instrumentation metrics from its internal client registry, as structured JSON. Supports optional regex filtering via the metric_name_pattern parameter. operationId: get-status-self-metrics parameters: - - name: metric_name + - name: metric_name_pattern in: query - description: Filter metrics by name prefix. + description: Regular expression filter for metric names (fully anchored, like PromQL label matchers). Only metric families whose names fully match the pattern are returned. required: false explode: false schema: type: string examples: example: - value: prometheus_tsdb + value: prometheus_tsdb_.* responses: "200": description: Self metrics retrieved successfully. @@ -2101,25 +2101,34 @@ paths: $ref: '#/components/schemas/StatusSelfMetricsOutputBody' examples: selfMetrics: - summary: Prometheus self-instrumentation metrics + summary: Prometheus self-instrumentation metrics in ProtoJSON format value: data: - help: A metric with a constant '1' value labeled by version, revision, branch, goversion from which prometheus was built, and the goos and goarch for the build. - metrics: - - labels: - branch: HEAD - goarch: amd64 - goos: linux - goversion: go1.23.0 - revision: abc1234 - tags: netgo,builtinassets,stringlabels - version: 3.0.0 - value: "1" + metric: + - gauge: + value: 1 + label: + - name: branch + value: HEAD + - name: goarch + value: amd64 + - name: goos + value: linux + - name: goversion + value: go1.23.0 + - name: revision + value: abc1234 + - name: tags + value: netgo,builtinassets,stringlabels + - name: version + value: 3.0.0 name: prometheus_build_info type: GAUGE - help: Total number of chunks in the head block. - metrics: - - value: "1024" + metric: + - gauge: + value: 1024 name: prometheus_tsdb_head_chunks type: GAUGE status: success diff --git a/web/api/v1/testdata/openapi_3.2_golden.yaml b/web/api/v1/testdata/openapi_3.2_golden.yaml index 2678baa20f..901e24280c 100644 --- a/web/api/v1/testdata/openapi_3.2_golden.yaml +++ b/web/api/v1/testdata/openapi_3.2_golden.yaml @@ -2079,19 +2079,19 @@ paths: tags: - status summary: Get Prometheus self-instrumentation metrics - description: Returns Prometheus' own instrumentation metrics from its internal client registry, as structured JSON. Supports optional prefix filtering via the metric_name parameter. + description: Returns Prometheus' own instrumentation metrics from its internal client registry, as structured JSON. Supports optional regex filtering via the metric_name_pattern parameter. operationId: get-status-self-metrics parameters: - - name: metric_name + - name: metric_name_pattern in: query - description: Filter metrics by name prefix. + description: Regular expression filter for metric names (fully anchored, like PromQL label matchers). Only metric families whose names fully match the pattern are returned. required: false explode: false schema: type: string examples: example: - value: prometheus_tsdb + value: prometheus_tsdb_.* responses: "200": description: Self metrics retrieved successfully. @@ -2101,25 +2101,34 @@ paths: $ref: '#/components/schemas/StatusSelfMetricsOutputBody' examples: selfMetrics: - summary: Prometheus self-instrumentation metrics + summary: Prometheus self-instrumentation metrics in ProtoJSON format value: data: - help: A metric with a constant '1' value labeled by version, revision, branch, goversion from which prometheus was built, and the goos and goarch for the build. - metrics: - - labels: - branch: HEAD - goarch: amd64 - goos: linux - goversion: go1.23.0 - revision: abc1234 - tags: netgo,builtinassets,stringlabels - version: 3.0.0 - value: "1" + metric: + - gauge: + value: 1 + label: + - name: branch + value: HEAD + - name: goarch + value: amd64 + - name: goos + value: linux + - name: goversion + value: go1.23.0 + - name: revision + value: abc1234 + - name: tags + value: netgo,builtinassets,stringlabels + - name: version + value: 3.0.0 name: prometheus_build_info type: GAUGE - help: Total number of chunks in the head block. - metrics: - - value: "1024" + metric: + - gauge: + value: 1024 name: prometheus_tsdb_head_chunks type: GAUGE status: success diff --git a/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts b/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts index 46864d7e09..a19da1da44 100644 --- a/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts +++ b/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts @@ -1,28 +1,73 @@ // Result type for /api/v1/status/self_metrics endpoint. +// The response uses the standard ProtoJSON format for io.prometheus.client.MetricFamily. +// See https://protobuf.dev/programming-guides/json/ -export interface SelfMetricQuantile { - quantile: string; +export interface ProtoLabelPair { + name: string; value: string; } -export interface SelfMetricBucket { - upperBound: string; +export interface ProtoGauge { + value: number; +} + +export interface ProtoCounter { + value: number; + exemplar?: ProtoExemplar; + createdTimestamp?: string; +} + +export interface ProtoQuantile { + quantile: number; + value: number; +} + +export interface ProtoSummary { + sampleCount: string; + sampleSum: number; + quantile?: ProtoQuantile[]; + createdTimestamp?: string; +} + +export interface ProtoBucket { cumulativeCount: string; + upperBound: number; + exemplar?: ProtoExemplar; } -export interface SelfMetric { - labels?: Record; - value?: string; - quantiles?: SelfMetricQuantile[]; - buckets?: SelfMetricBucket[]; +export interface ProtoHistogram { + sampleCount: string; + sampleSum: number; + bucket?: ProtoBucket[]; + createdTimestamp?: string; } -export interface SelfMetricFamily { +export interface ProtoExemplar { + label?: ProtoLabelPair[]; + value: number; + timestamp?: string; +} + +export interface ProtoUntyped { + value: number; +} + +export interface ProtoMetric { + label?: ProtoLabelPair[]; + gauge?: ProtoGauge; + counter?: ProtoCounter; + summary?: ProtoSummary; + histogram?: ProtoHistogram; + untyped?: ProtoUntyped; + timestampMs?: string; +} + +export interface ProtoMetricFamily { name: string; help: string; type: string; + metric: ProtoMetric[]; unit?: string; - metrics: SelfMetric[]; } -export type SelfMetricsResult = SelfMetricFamily[]; +export type SelfMetricsResult = ProtoMetricFamily[];