From 25dac1058a5687a747c14f8f0c4801ebd1bb231d Mon Sep 17 00:00:00 2001 From: felpau05 Date: Sun, 26 Jul 2026 19:48:13 -0400 Subject: [PATCH] api/prometheus/v1: merge envelope and payload JSON decode Removes the redundant second JSON parse across all 20 methods affected by #977. Decodes the envelope and typed payload in a single json.Unmarshal pass instead of an intermediate json.RawMessage that each method re-parsed separately. Changes apiResponse.Data from json.RawMessage to interface{}. Updates Do and DoGetFallback to take a new data interface{} destination. A typed pointer decodes directly via Go's existing behavior of following a non-nil pointer stored in an interface field. Passing nil falls back to the current json.RawMessage behavior unchanged (used by CleanTombstones and DeleteSeries). FormatQuery remains unconverted as it uses a raw string(body) cast rather than json.Unmarshal on the body. Fixes #977. Signed-off-by: felpau05 --- api/prometheus/v1/api.go | 177 ++++++++------------------ api/prometheus/v1/api_test.go | 53 ++++++-- api/prometheus/v1/query_bench_test.go | 75 +++++++++++ 3 files changed, 172 insertions(+), 133 deletions(-) create mode 100644 api/prometheus/v1/query_bench_test.go diff --git a/api/prometheus/v1/api.go b/api/prometheus/v1/api.go index 62f2ed7dc..4ba923852 100644 --- a/api/prometheus/v1/api.go +++ b/api/prometheus/v1/api.go @@ -926,13 +926,8 @@ func (h *httpAPI) Alerts(ctx context.Context) (AlertsResult, error) { return AlertsResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return AlertsResult{}, err - } - var res AlertsResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -944,13 +939,8 @@ func (h *httpAPI) AlertManagers(ctx context.Context) (AlertManagersResult, error return AlertManagersResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return AlertManagersResult{}, err - } - var res AlertManagersResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -962,7 +952,7 @@ func (h *httpAPI) CleanTombstones(ctx context.Context) error { return err } - _, _, _, _, err = h.client.Do(ctx, req) + _, _, _, _, err = h.client.Do(ctx, req, nil) return err } @@ -974,13 +964,8 @@ func (h *httpAPI) Config(ctx context.Context) (ConfigResult, error) { return ConfigResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return ConfigResult{}, err - } - var res ConfigResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1006,7 +991,7 @@ func (h *httpAPI) DeleteSeries(ctx context.Context, matches []string, startTime, return err } - _, _, _, _, err = h.client.Do(ctx, req) + _, _, _, _, err = h.client.Do(ctx, req, nil) return err } @@ -1018,13 +1003,8 @@ func (h *httpAPI) Flags(ctx context.Context) (FlagsResult, error) { return FlagsResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return FlagsResult{}, err - } - var res FlagsResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1036,13 +1016,8 @@ func (h *httpAPI) Buildinfo(ctx context.Context) (BuildinfoResult, error) { return BuildinfoResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return BuildinfoResult{}, err - } - var res BuildinfoResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1054,13 +1029,8 @@ func (h *httpAPI) Runtimeinfo(ctx context.Context) (RuntimeinfoResult, error) { return RuntimeinfoResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return RuntimeinfoResult{}, err - } - var res RuntimeinfoResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1078,13 +1048,12 @@ func (h *httpAPI) LabelNames(ctx context.Context, matches []string, startTime, e q.Add("match[]", m) } - _, body, w, i, err := h.client.DoGetFallback(ctx, u, q) + var labelNames model.LabelNames + _, _, w, i, err := h.client.DoGetFallback(ctx, u, q, &labelNames) if err != nil { return nil, w, i, err } - var labelNames model.LabelNames - err = json.Unmarshal(body, &labelNames) - return labelNames, w, i, err + return labelNames, w, i, nil } func (h *httpAPI) LabelValues(ctx context.Context, label string, matches []string, startTime, endTime time.Time, opts ...Option) (model.LabelValues, Warnings, Infos, error) { @@ -1107,13 +1076,12 @@ func (h *httpAPI) LabelValues(ctx context.Context, label string, matches []strin if err != nil { return nil, nil, nil, err } - _, body, w, i, err := h.client.Do(ctx, req) + var labelValues model.LabelValues + _, _, w, i, err := h.client.Do(ctx, req, &labelValues) if err != nil { return nil, w, i, err } - var labelValues model.LabelValues - err = json.Unmarshal(body, &labelValues) - return labelValues, w, i, err + return labelValues, w, i, nil } // StatsValue is a type for `stats` query parameter. @@ -1201,13 +1169,13 @@ func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time, opts .. q.Set("time", formatTime(ts)) } - _, body, warnings, infos, err := h.client.DoGetFallback(ctx, u, q) + var qres queryResult + _, _, warnings, infos, err := h.client.DoGetFallback(ctx, u, q, &qres) if err != nil { return nil, warnings, infos, err } - var qres queryResult - return qres.v, warnings, infos, json.Unmarshal(body, &qres) + return qres.v, warnings, infos, nil } func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range, opts ...Option) (model.Value, Warnings, Infos, error) { @@ -1219,13 +1187,13 @@ func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range, opts .. q.Set("end", formatTime(r.End)) q.Set("step", strconv.FormatFloat(r.Step.Seconds(), 'f', -1, 64)) - _, body, warnings, infos, err := h.client.DoGetFallback(ctx, u, q) + var qres queryResult + _, _, warnings, infos, err := h.client.DoGetFallback(ctx, u, q, &qres) if err != nil { return nil, warnings, infos, err } - var qres queryResult - return qres.v, warnings, infos, json.Unmarshal(body, &qres) + return qres.v, warnings, infos, nil } func (h *httpAPI) Series(ctx context.Context, matches []string, startTime, endTime time.Time, opts ...Option) ([]model.LabelSet, Warnings, Infos, error) { @@ -1243,13 +1211,13 @@ func (h *httpAPI) Series(ctx context.Context, matches []string, startTime, endTi q.Set("end", formatTime(endTime)) } - _, body, warnings, infos, err := h.client.DoGetFallback(ctx, u, q) + var mset []model.LabelSet + _, _, warnings, infos, err := h.client.DoGetFallback(ctx, u, q, &mset) if err != nil { return nil, warnings, infos, err } - var mset []model.LabelSet - return mset, warnings, infos, json.Unmarshal(body, &mset) + return mset, warnings, infos, nil } func (h *httpAPI) Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error) { @@ -1265,13 +1233,8 @@ func (h *httpAPI) Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, return SnapshotResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return SnapshotResult{}, err - } - var res SnapshotResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1290,13 +1253,8 @@ func (h *httpAPI) Rules(ctx context.Context, matches []string) (RulesResult, err return RulesResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return RulesResult{}, err - } - var res RulesResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1308,13 +1266,8 @@ func (h *httpAPI) Targets(ctx context.Context) (TargetsResult, error) { return TargetsResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return TargetsResult{}, err - } - var res TargetsResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1333,13 +1286,8 @@ func (h *httpAPI) TargetsMetadata(ctx context.Context, matchTarget, metric, limi return nil, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return nil, err - } - var res []MetricMetadata - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1357,13 +1305,8 @@ func (h *httpAPI) Metadata(ctx context.Context, metric, limit string) (map[strin return nil, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return nil, err - } - var res map[string][]Metadata - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1377,13 +1320,8 @@ func (h *httpAPI) TSDB(ctx context.Context, opts ...Option) (TSDBResult, error) return TSDBResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return TSDBResult{}, err - } - var res TSDBResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1395,13 +1333,8 @@ func (h *httpAPI) TSDBBlocks(ctx context.Context) (TSDBBlocksResult, error) { return TSDBBlocksResult{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return TSDBBlocksResult{}, err - } - var res TSDBBlocksResult - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1413,13 +1346,8 @@ func (h *httpAPI) WalReplay(ctx context.Context) (WalReplayStatus, error) { return WalReplayStatus{}, err } - _, body, _, _, err := h.client.Do(ctx, req) - if err != nil { - return WalReplayStatus{}, err - } - var res WalReplayStatus - err = json.Unmarshal(body, &res) + _, _, _, _, err = h.client.Do(ctx, req, &res) return res, err } @@ -1435,13 +1363,8 @@ func (h *httpAPI) QueryExemplars(ctx context.Context, query string, startTime, e q.Set("end", formatTime(endTime)) } - _, body, _, _, err := h.client.DoGetFallback(ctx, u, q) - if err != nil { - return nil, err - } - var res []ExemplarQueryResult - err = json.Unmarshal(body, &res) + _, _, _, _, err := h.client.DoGetFallback(ctx, u, q, &res) return res, err } @@ -1450,7 +1373,7 @@ func (h *httpAPI) FormatQuery(ctx context.Context, query string) (string, error) q := u.Query() q.Set("query", query) - _, body, _, _, err := h.client.DoGetFallback(ctx, u, q) + _, body, _, _, err := h.client.DoGetFallback(ctx, u, q, nil) if err != nil { return "", err } @@ -1468,8 +1391,8 @@ type Infos []string // Successful also includes responses that errored at the API level. type apiClient interface { URL(ep string, args map[string]string) *url.URL - Do(context.Context, *http.Request) (*http.Response, []byte, Warnings, Infos, error) - DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, Infos, error) + Do(ctx context.Context, req *http.Request, data interface{}) (*http.Response, []byte, Warnings, Infos, error) + DoGetFallback(ctx context.Context, u *url.URL, args url.Values, data interface{}) (*http.Response, []byte, Warnings, Infos, error) } type apiClientImpl struct { @@ -1477,12 +1400,12 @@ type apiClientImpl struct { } type apiResponse struct { - Status string `json:"status"` - Data json.RawMessage `json:"data"` - ErrorType ErrorType `json:"errorType"` - Error string `json:"error"` - Warnings []string `json:"warnings,omitempty"` - Infos []string `json:"infos,omitempty"` + Status string `json:"status"` + Data interface{} `json:"data"` + ErrorType ErrorType `json:"errorType"` + Error string `json:"error"` + Warnings []string `json:"warnings,omitempty"` + Infos []string `json:"infos,omitempty"` } func apiError(code int) bool { @@ -1504,7 +1427,7 @@ func (h *apiClientImpl) URL(ep string, args map[string]string) *url.URL { return h.client.URL(ep, args) } -func (h *apiClientImpl) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, Warnings, Infos, error) { +func (h *apiClientImpl) Do(ctx context.Context, req *http.Request, data interface{}) (*http.Response, []byte, Warnings, Infos, error) { resp, body, err := h.client.Do(ctx, req) if err != nil { return resp, body, nil, nil, err @@ -1521,7 +1444,13 @@ func (h *apiClientImpl) Do(ctx context.Context, req *http.Request) (*http.Respon } } - var result apiResponse + // When the caller does not provide a typed destination, decode the data + // field into a json.RawMessage so it can be returned as bytes. + var rawData json.RawMessage + if data == nil { + data = &rawData + } + result := apiResponse{Data: data} if http.StatusNoContent != code { if jsonErr := json.Unmarshal(body, &result); jsonErr != nil { @@ -1546,12 +1475,12 @@ func (h *apiClientImpl) Do(ctx context.Context, req *http.Request) (*http.Respon } } - return resp, []byte(result.Data), result.Warnings, result.Infos, err + return resp, []byte(rawData), result.Warnings, result.Infos, err } // DoGetFallback will attempt to do the request as-is, and on a 403, 405, or // 501 it will fallback to a GET request. -func (h *apiClientImpl) DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, Infos, error) { +func (h *apiClientImpl) DoGetFallback(ctx context.Context, u *url.URL, args url.Values, data interface{}) (*http.Response, []byte, Warnings, Infos, error) { encodedArgs := args.Encode() req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(encodedArgs)) if err != nil { @@ -1567,14 +1496,14 @@ func (h *apiClientImpl) DoGetFallback(ctx context.Context, u *url.URL, args url. // the header is not sent on the wire. req.Header["Idempotency-Key"] = nil - resp, body, warnings, infos, err := h.Do(ctx, req) + resp, body, warnings, infos, err := h.Do(ctx, req, data) if resp != nil && (resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented) { u.RawQuery = encodedArgs req, err = http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return nil, nil, warnings, infos, err } - return h.Do(ctx, req) + return h.Do(ctx, req, data) } return resp, body, warnings, infos, err } diff --git a/api/prometheus/v1/api_test.go b/api/prometheus/v1/api_test.go index cae4eb67c..59ab91d08 100644 --- a/api/prometheus/v1/api_test.go +++ b/api/prometheus/v1/api_test.go @@ -63,7 +63,7 @@ func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL { return u } -func (c *apiTestClient) Do(_ context.Context, req *http.Request) (*http.Response, []byte, Warnings, Infos, error) { +func (c *apiTestClient) Do(_ context.Context, req *http.Request, data interface{}) (*http.Response, []byte, Warnings, Infos, error) { test := c.curTest if req.URL.Path != test.reqPath { @@ -87,15 +87,23 @@ func (c *apiTestClient) Do(_ context.Context, req *http.Request) (*http.Response resp.StatusCode = http.StatusOK } + // Emulate apiClientImpl.Do by decoding the (already unwrapped) data body + // into the caller-provided destination on success. + if data != nil && test.inErr == nil { + if err := json.Unmarshal(b, data); err != nil { + c.Fatal(err) + } + } + return resp, b, test.inWarnings, test.inInfos, test.inErr } -func (c *apiTestClient) DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, Infos, error) { +func (c *apiTestClient) DoGetFallback(ctx context.Context, u *url.URL, args url.Values, data interface{}) (*http.Response, []byte, Warnings, Infos, error) { req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(args.Encode())) if err != nil { return nil, nil, nil, nil, err } - return c.Do(ctx, req) + return c.Do(ctx, req, data) } func TestAPIs(t *testing.T) { @@ -273,6 +281,33 @@ func TestAPIs(t *testing.T) { Timestamp: model.TimeFromUnix(testTime.Unix()), }, }, + { + do: doQuery("http_requests_total", testTime), + inRes: &queryResult{ + Type: model.ValMatrix, + Result: model.Matrix{ + &model.SampleStream{ + Metric: model.Metric{"__name__": "http_requests_total", "job": "prometheus"}, + Values: []model.SamplePair{ + {Timestamp: model.TimeFromUnix(testTime.Add(-1 * time.Minute).Unix()), Value: 1}, + {Timestamp: model.TimeFromUnix(testTime.Unix()), Value: 2}, + }, + }, + }, + }, + + reqMethod: "POST", + reqPath: "/api/v1/query", + res: model.Matrix{ + &model.SampleStream{ + Metric: model.Metric{"__name__": "http_requests_total", "job": "prometheus"}, + Values: []model.SamplePair{ + {Timestamp: model.TimeFromUnix(testTime.Add(-1 * time.Minute).Unix()), Value: 1}, + {Timestamp: model.TimeFromUnix(testTime.Unix()), Value: 2}, + }, + }, + }, + }, { do: doQuery("2", testTime), inErr: errors.New("some error"), @@ -1606,7 +1641,7 @@ func TestAPIClientDo(t *testing.T) { t.Run(strconv.Itoa(i), func(t *testing.T) { tc.ch <- test - _, body, warnings, infos, err := client.Do(context.Background(), tc.req) + _, body, warnings, infos, err := client.Do(context.Background(), tc.req, nil) if test.expectedWarnings != nil { if !reflect.DeepEqual(test.expectedWarnings, warnings) { @@ -1964,7 +1999,7 @@ func TestDoGetFallback(t *testing.T) { }) apiResp := &apiResponse{ - Data: testResp, + Data: json.RawMessage(testResp), } body, _ := json.Marshal(apiResp) @@ -2005,7 +2040,7 @@ func TestDoGetFallback(t *testing.T) { } // Do a post, and ensure that the post succeeds. - _, b, _, _, err := api.DoGetFallback(context.TODO(), u, v) + _, b, _, _, err := api.DoGetFallback(context.TODO(), u, v, nil) if err != nil { t.Fatalf("Error doing local request: %v", err) } @@ -2022,7 +2057,7 @@ func TestDoGetFallback(t *testing.T) { // Do a fallback to a get on 403. u.Path = "/blockPost403" - _, b, _, _, err = api.DoGetFallback(context.TODO(), u, v) + _, b, _, _, err = api.DoGetFallback(context.TODO(), u, v, nil) if err != nil { t.Fatalf("Error doing local request: %v", err) } @@ -2038,7 +2073,7 @@ func TestDoGetFallback(t *testing.T) { // Do a fallback to a get on 405. u.Path = "/blockPost405" - _, b, _, _, err = api.DoGetFallback(context.TODO(), u, v) + _, b, _, _, err = api.DoGetFallback(context.TODO(), u, v, nil) if err != nil { t.Fatalf("Error doing local request: %v", err) } @@ -2054,7 +2089,7 @@ func TestDoGetFallback(t *testing.T) { // Do a fallback to a get on 501. u.Path = "/blockPost501" - _, b, _, _, err = api.DoGetFallback(context.TODO(), u, v) + _, b, _, _, err = api.DoGetFallback(context.TODO(), u, v, nil) if err != nil { t.Fatalf("Error doing local request: %v", err) } diff --git a/api/prometheus/v1/query_bench_test.go b/api/prometheus/v1/query_bench_test.go new file mode 100644 index 000000000..d441eb844 --- /dev/null +++ b/api/prometheus/v1/query_bench_test.go @@ -0,0 +1,75 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/prometheus/client_golang/api" +) + +// buildLargeMatrixResponse builds a full Prometheus API envelope +// ({"status":"success","data":{...}}) holding a range-query matrix result +// with numSeries series of numSamples samples each. This mirrors the real +// wire shape decoded by queryResult.UnmarshalJSON. +func buildLargeMatrixResponse(numSeries, numSamples int) string { + var sb strings.Builder + sb.WriteString(`{"status":"success","data":{"resultType":"matrix","result":[`) + for s := 0; s < numSeries; s++ { + if s > 0 { + sb.WriteString(",") + } + fmt.Fprintf(&sb, `{"metric":{"__name__":"http_requests_total","instance":"10.0.0.%d:9100"},"values":[`, s%255) + for v := 0; v < numSamples; v++ { + if v > 0 { + sb.WriteString(",") + } + fmt.Fprintf(&sb, `[%d.000,"%d"]`, 1700000000+v, v*3+s) + } + sb.WriteString(`]}`) + } + sb.WriteString(`]}}`) + return sb.String() +} + +func BenchmarkQueryRange(b *testing.B) { + body := buildLargeMatrixResponse(100, 350) // ~35k samples; realistic large range-query payload. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(body)) + })) + defer srv.Close() + + client, err := api.NewClient(api.Config{Address: srv.URL}) + if err != nil { + b.Fatal(err) + } + v1api := NewAPI(client) + ctx := context.Background() + r := Range{Start: time.Now().Add(-time.Hour), End: time.Now(), Step: time.Minute} + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, _, _, err := v1api.QueryRange(ctx, "up", r); err != nil { + b.Fatal(err) + } + } +}