Use ProtoJSON, allow regex-based filtering of metric names

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2026-04-10 14:59:46 +02:00
parent 318c913fe2
commit 034c29411a
8 changed files with 202 additions and 186 deletions

View File

@ -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=<string>`: 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=<string>`: 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
}
}
]
}

View File

@ -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}

View File

@ -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)
}
})
}

View File

@ -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,
},
},
},
},

View File

@ -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."),

View File

@ -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

View File

@ -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

View File

@ -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<string, string>;
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[];