From 318c913fe26d5e4b5187117f21a5704f0b3607c2 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Tue, 31 Mar 2026 12:23:30 +0200 Subject: [PATCH 1/4] Add API endpoint for getting Prometheus' metrics about itself This adds a /api/v1/status/self_metrics endpoint that allows the frontend to fetch metrics about the server itself, making it easier to construct frontend pages that show the current server state. This is needed because fetching metrics from its own /metrics endpoint would be both hard to parse and also require CORS permissions on that endpoint (for cases where the frontend dashboard is not the same origin, at least). Signed-off-by: Julius Volz --- docs/querying/api.md | 57 ++++++++++ web/api/v1/api.go | 101 ++++++++++++++++++ web/api/v1/api_test.go | 54 ++++++++++ web/api/v1/openapi.go | 1 + web/api/v1/openapi_examples.go | 45 ++++++++ web/api/v1/openapi_paths.go | 16 +++ web/api/v1/testdata/openapi_3.1_golden.yaml | 62 +++++++++++ web/api/v1/testdata/openapi_3.2_golden.yaml | 62 +++++++++++ .../src/api/responseTypes/selfMetrics.ts | 28 +++++ 9 files changed, 426 insertions(+) create mode 100644 web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts diff --git a/docs/querying/api.md b/docs/querying/api.md index a45e3c292b..10c91b4310 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -1562,6 +1562,63 @@ NOTE: This endpoint is available before the server has been marked ready and is *New in v2.28* +### Self Metrics + +**NOTE**: This endpoint is **experimental** and might change in the future. + +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. + +``` +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. + +Each returned metric family contains: + +- **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. + +```bash +curl 'http://localhost:9090/api/v1/status/self_metrics?metric_name=prometheus_build' +``` + +```json +{ + "status": "success", + "data": [ + { + "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": [ + { + "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" + } + ] + } + ] +} +``` + ## TSDB Admin APIs These are APIs that expose database functionalities for the advanced user. These APIs are not enabled unless the `--web.enable-admin-api` is set. diff --git a/web/api/v1/api.go b/web/api/v1/api.go index c236f402d3..5af406fafb 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -458,6 +458,7 @@ func (api *API) Register(r *route.Router) { r.Get("/status/flags", wrap(api.serveFlags)) r.Get("/status/tsdb", wrapAgent(api.serveTSDBStatus)) r.Get("/status/tsdb/blocks", wrapAgent(api.serveTSDBBlocks)) + r.Get("/status/self_metrics", wrap(api.selfMetrics)) r.Get("/features", wrap(api.features)) r.Get("/status/walreplay", api.serveWALReplayStatus) r.Get("/notifications", api.notifications) @@ -1865,6 +1866,106 @@ 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") + + 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)) + for _, mf := range mfs { + name := mf.GetName() + if metricNameFilter != "" && !strings.HasPrefix(name, metricNameFilter) { + continue + } + + family := SelfMetricFamily{ + Name: name, + Help: mf.GetHelp(), + Type: mf.GetType().String(), + Unit: mf.GetUnit(), + Metrics: make([]SelfMetric, 0, len(mf.GetMetric())), + } + + 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) + } + + return apiFuncResult{result, nil, nil, nil} +} + func (api *API) serveTSDBBlocks(*http.Request) apiFuncResult { blockMetas, err := api.db.BlockMetas() if err != nil { diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 9b5b0af37f..1329284e4f 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -4550,6 +4550,60 @@ func TestTSDBStatus(t *testing.T) { } } +func TestSelfMetrics(t *testing.T) { + api := &API{gatherer: prometheus.DefaultGatherer} + + t.Run("returns all metrics without filter", 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, ok := res.data.([]SelfMetricFamily) + require.True(t, ok, "expected []SelfMetricFamily") + 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) + require.NoError(t, err) + req.Form = url.Values{"metric_name": {"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) + } + }) + + t.Run("non-matching prefix returns empty", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "?metric_name=nonexistent_prefix_xyz_", http.NoBody) + require.NoError(t, err) + req.Form = url.Values{"metric_name": {"nonexistent_prefix_xyz_"}} + res := api.selfMetrics(req) + assertAPIError(t, res.err, errorNone) + families, ok := res.data.([]SelfMetricFamily) + require.True(t, ok, "expected []SelfMetricFamily") + require.Empty(t, families) + }) + + t.Run("metric families have expected 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) + } + } + }) +} + func TestReturnAPIError(t *testing.T) { cases := []struct { err error diff --git a/web/api/v1/openapi.go b/web/api/v1/openapi.go index 59fa8969ef..839af8108e 100644 --- a/web/api/v1/openapi.go +++ b/web/api/v1/openapi.go @@ -298,6 +298,7 @@ func (b *OpenAPIBuilder) getAllPathDefinitions() *orderedmap.Map[string, *v3.Pat paths.Set("/status/tsdb", b.statusTSDBPath()) paths.Set("/status/tsdb/blocks", b.statusTSDBBlocksPath()) paths.Set("/status/walreplay", b.statusWALReplayPath()) + paths.Set("/status/self_metrics", b.statusSelfMetricsPath()) // Admin endpoints. paths.Set("/admin/tsdb/delete_series", b.adminDeleteSeriesPath()) diff --git a/web/api/v1/openapi_examples.go b/web/api/v1/openapi_examples.go index a1db5d5f3b..fab40cf4c4 100644 --- a/web/api/v1/openapi_examples.go +++ b/web/api/v1/openapi_examples.go @@ -886,6 +886,51 @@ func statusWALReplayResponseExamples() *orderedmap.Map[string, *base.Example] { return examples } +// statusSelfMetricsResponseExamples returns examples for /status/self_metrics response. +func statusSelfMetricsResponseExamples() *orderedmap.Map[string, *base.Example] { + examples := orderedmap.New[string, *base.Example]() + + examples.Set("selfMetrics", &base.Example{ + Summary: "Prometheus self-instrumentation metrics", + Value: createYAMLNode(map[string]any{ + "status": "success", + "data": []map[string]any{ + { + "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{ + { + "labels": map[string]string{ + "branch": "HEAD", + "goarch": "amd64", + "goos": "linux", + "goversion": "go1.23.0", + "revision": "abc1234", + "tags": "netgo,builtinassets,stringlabels", + "version": "3.0.0", + }, + "value": "1", + }, + }, + }, + { + "name": "prometheus_tsdb_head_chunks", + "help": "Total number of chunks in the head block.", + "type": "GAUGE", + "metrics": []map[string]any{ + { + "value": "1024", + }, + }, + }, + }, + }), + }) + + return examples +} + // deleteSeriesResponseExamples returns examples for /admin/tsdb/delete_series response. func deleteSeriesResponseExamples() *orderedmap.Map[string, *base.Example] { examples := orderedmap.New[string, *base.Example]() diff --git a/web/api/v1/openapi_paths.go b/web/api/v1/openapi_paths.go index b2622f4ff0..ad1a4e6882 100644 --- a/web/api/v1/openapi_paths.go +++ b/web/api/v1/openapi_paths.go @@ -441,6 +441,22 @@ 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"}}), + } + 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.", + Tags: []string{"status"}, + Parameters: params, + Responses: responsesWithErrorExamples("StatusSelfMetricsOutputBody", statusSelfMetricsResponseExamples(), errorResponseExamples(), "Self metrics retrieved successfully.", "Error retrieving self metrics."), + }, + } +} + func (*OpenAPIBuilder) adminDeleteSeriesPath() *v3.PathItem { params := []*v3.Parameter{ queryParamWithExample("match[]", "Series selectors to identify series to delete.", true, base.CreateSchemaProxy(&base.Schema{ diff --git a/web/api/v1/testdata/openapi_3.1_golden.yaml b/web/api/v1/testdata/openapi_3.1_golden.yaml index 47f3d06621..d912be1ae7 100644 --- a/web/api/v1/testdata/openapi_3.1_golden.yaml +++ b/web/api/v1/testdata/openapi_3.1_golden.yaml @@ -2074,6 +2074,68 @@ paths: error: TSDB not ready errorType: internal status: error + /status/self_metrics: + get: + 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. + operationId: get-status-self-metrics + parameters: + - name: metric_name + in: query + description: Filter metrics by name prefix. + required: false + explode: false + schema: + type: string + examples: + example: + value: prometheus_tsdb + responses: + "200": + description: Self metrics retrieved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/StatusSelfMetricsOutputBody' + examples: + selfMetrics: + summary: Prometheus self-instrumentation metrics + 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" + name: prometheus_build_info + type: GAUGE + - help: Total number of chunks in the head block. + metrics: + - value: "1024" + name: prometheus_tsdb_head_chunks + type: GAUGE + status: success + default: + description: Error retrieving self metrics. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + tsdbNotReady: + summary: TSDB not ready + value: + error: TSDB not ready + errorType: internal + status: error /admin/tsdb/delete_series: put: tags: diff --git a/web/api/v1/testdata/openapi_3.2_golden.yaml b/web/api/v1/testdata/openapi_3.2_golden.yaml index 7766f86f39..2678baa20f 100644 --- a/web/api/v1/testdata/openapi_3.2_golden.yaml +++ b/web/api/v1/testdata/openapi_3.2_golden.yaml @@ -2074,6 +2074,68 @@ paths: error: TSDB not ready errorType: internal status: error + /status/self_metrics: + get: + 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. + operationId: get-status-self-metrics + parameters: + - name: metric_name + in: query + description: Filter metrics by name prefix. + required: false + explode: false + schema: + type: string + examples: + example: + value: prometheus_tsdb + responses: + "200": + description: Self metrics retrieved successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/StatusSelfMetricsOutputBody' + examples: + selfMetrics: + summary: Prometheus self-instrumentation metrics + 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" + name: prometheus_build_info + type: GAUGE + - help: Total number of chunks in the head block. + metrics: + - value: "1024" + name: prometheus_tsdb_head_chunks + type: GAUGE + status: success + default: + description: Error retrieving self metrics. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + tsdbNotReady: + summary: TSDB not ready + value: + error: TSDB not ready + errorType: internal + status: error /admin/tsdb/delete_series: put: tags: diff --git a/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts b/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts new file mode 100644 index 0000000000..46864d7e09 --- /dev/null +++ b/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts @@ -0,0 +1,28 @@ +// Result type for /api/v1/status/self_metrics endpoint. + +export interface SelfMetricQuantile { + quantile: string; + value: string; +} + +export interface SelfMetricBucket { + upperBound: string; + cumulativeCount: string; +} + +export interface SelfMetric { + labels?: Record; + value?: string; + quantiles?: SelfMetricQuantile[]; + buckets?: SelfMetricBucket[]; +} + +export interface SelfMetricFamily { + name: string; + help: string; + type: string; + unit?: string; + metrics: SelfMetric[]; +} + +export type SelfMetricsResult = SelfMetricFamily[]; From 034c29411aa6370775dea8640fd1cab1a0f596c8 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Fri, 10 Apr 2026 14:59:46 +0200 Subject: [PATCH 2/4] Use ProtoJSON, allow regex-based filtering of metric names Signed-off-by: Julius Volz --- docs/querying/api.md | 40 ++++---- web/api/v1/api.go | 99 +++---------------- web/api/v1/api_test.go | 60 ++++++----- web/api/v1/openapi_examples.go | 30 +++--- web/api/v1/openapi_paths.go | 4 +- web/api/v1/testdata/openapi_3.1_golden.yaml | 43 ++++---- web/api/v1/testdata/openapi_3.2_golden.yaml | 43 ++++---- .../src/api/responseTypes/selfMetrics.ts | 69 ++++++++++--- 8 files changed, 202 insertions(+), 186 deletions(-) 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[]; From d456d314d2d422013afaf4819b19f010a337ca16 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Fri, 10 Apr 2026 16:01:23 +0200 Subject: [PATCH 3/4] Add some missing metrics protobuf field TypeScript definitions Signed-off-by: Julius Volz --- .../src/api/responseTypes/selfMetrics.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts b/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts index a19da1da44..c47374ae3b 100644 --- a/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts +++ b/web/ui/mantine-ui/src/api/responseTypes/selfMetrics.ts @@ -31,15 +31,33 @@ export interface ProtoSummary { export interface ProtoBucket { cumulativeCount: string; + cumulativeCountFloat?: number; upperBound: number; exemplar?: ProtoExemplar; } +export interface ProtoBucketSpan { + offset: number; + length: number; +} + export interface ProtoHistogram { sampleCount: string; + sampleCountFloat?: number; sampleSum: number; bucket?: ProtoBucket[]; createdTimestamp?: string; + schema?: number; + zeroThreshold?: number; + zeroCount?: string; + zeroCountFloat?: number; + negativeSpan?: ProtoBucketSpan[]; + negativeDelta?: string[]; + negativeCount?: number[]; + positiveSpan?: ProtoBucketSpan[]; + positiveDelta?: string[]; + positiveCount?: number[]; + exemplars?: ProtoExemplar[]; } export interface ProtoExemplar { From d4935fbf6e64ec350bb42d8b8191a44a9e3580d1 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Fri, 10 Apr 2026 19:13:39 +0200 Subject: [PATCH 4/4] Fix OpenAPI defs Signed-off-by: Julius Volz --- web/api/v1/openapi_schemas.go | 1 + web/api/v1/testdata/openapi_3.1_golden.yaml | 29 +++++++++++++++++++++ web/api/v1/testdata/openapi_3.2_golden.yaml | 29 +++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/web/api/v1/openapi_schemas.go b/web/api/v1/openapi_schemas.go index ac298ac0bd..d768f4208d 100644 --- a/web/api/v1/openapi_schemas.go +++ b/web/api/v1/openapi_schemas.go @@ -112,6 +112,7 @@ func (b *OpenAPIBuilder) buildComponents() *v3.Components { schemas.Set("StatusTSDBBlocksOutputBody", b.refResponseBodySchema("StatusTSDBBlocksData", "Response body for status TSDB blocks endpoint.")) schemas.Set("StatusWALReplayData", b.statusWALReplayDataSchema()) schemas.Set("StatusWALReplayOutputBody", b.refResponseBodySchema("StatusWALReplayData", "Response body for status WAL replay endpoint.")) + schemas.Set("StatusSelfMetricsOutputBody", b.simpleResponseBodySchema()) // Admin schemas. schemas.Set("DeleteSeriesOutputBody", b.statusOnlyResponseBodySchema()) diff --git a/web/api/v1/testdata/openapi_3.1_golden.yaml b/web/api/v1/testdata/openapi_3.1_golden.yaml index e1282e2658..8d1278ff91 100644 --- a/web/api/v1/testdata/openapi_3.1_golden.yaml +++ b/web/api/v1/testdata/openapi_3.1_golden.yaml @@ -4273,6 +4273,35 @@ components: - data additionalProperties: false description: Response body for status WAL replay endpoint. + StatusSelfMetricsOutputBody: + type: object + properties: + status: + type: string + enum: + - success + - error + description: Response status. + example: success + data: + description: Response data (structure varies by endpoint). + example: + result: ok + warnings: + type: array + items: + type: string + description: Only set if there were warnings while executing the request. There will still be data in the data field. + infos: + type: array + items: + type: string + description: Only set if there were info-level annotations while executing the request. + required: + - status + - data + additionalProperties: false + description: Generic response body. DeleteSeriesOutputBody: type: object properties: diff --git a/web/api/v1/testdata/openapi_3.2_golden.yaml b/web/api/v1/testdata/openapi_3.2_golden.yaml index 901e24280c..a3f3d88f52 100644 --- a/web/api/v1/testdata/openapi_3.2_golden.yaml +++ b/web/api/v1/testdata/openapi_3.2_golden.yaml @@ -4311,6 +4311,35 @@ components: - data additionalProperties: false description: Response body for status WAL replay endpoint. + StatusSelfMetricsOutputBody: + type: object + properties: + status: + type: string + enum: + - success + - error + description: Response status. + example: success + data: + description: Response data (structure varies by endpoint). + example: + result: ok + warnings: + type: array + items: + type: string + description: Only set if there were warnings while executing the request. There will still be data in the data field. + infos: + type: array + items: + type: string + description: Only set if there were info-level annotations while executing the request. + required: + - status + - data + additionalProperties: false + description: Generic response body. DeleteSeriesOutputBody: type: object properties: