1// Copyright 2017 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14// Package v1 provides bindings to the Prometheus HTTP API v1:
15// http://prometheus.io/docs/querying/api/
16package v1
17
18import (
19	"context"
20	"errors"
21	"fmt"
22	"math"
23	"net/http"
24	"net/url"
25	"strconv"
26	"strings"
27	"time"
28	"unsafe"
29
30	json "github.com/json-iterator/go"
31
32	"github.com/prometheus/common/model"
33
34	"github.com/prometheus/client_golang/api"
35)
36
37func init() {
38	json.RegisterTypeEncoderFunc("model.SamplePair", marshalPointJSON, marshalPointJSONIsEmpty)
39	json.RegisterTypeDecoderFunc("model.SamplePair", unMarshalPointJSON)
40}
41
42func unMarshalPointJSON(ptr unsafe.Pointer, iter *json.Iterator) {
43	p := (*model.SamplePair)(ptr)
44	if !iter.ReadArray() {
45		iter.ReportError("unmarshal model.SamplePair", "SamplePair must be [timestamp, value]")
46		return
47	}
48	t := iter.ReadNumber()
49	if err := p.Timestamp.UnmarshalJSON([]byte(t)); err != nil {
50		iter.ReportError("unmarshal model.SamplePair", err.Error())
51		return
52	}
53	if !iter.ReadArray() {
54		iter.ReportError("unmarshal model.SamplePair", "SamplePair missing value")
55		return
56	}
57
58	f, err := strconv.ParseFloat(iter.ReadString(), 64)
59	if err != nil {
60		iter.ReportError("unmarshal model.SamplePair", err.Error())
61		return
62	}
63	p.Value = model.SampleValue(f)
64
65	if iter.ReadArray() {
66		iter.ReportError("unmarshal model.SamplePair", "SamplePair has too many values, must be [timestamp, value]")
67		return
68	}
69}
70
71func marshalPointJSON(ptr unsafe.Pointer, stream *json.Stream) {
72	p := *((*model.SamplePair)(ptr))
73	stream.WriteArrayStart()
74	// Write out the timestamp as a float divided by 1000.
75	// This is ~3x faster than converting to a float.
76	t := int64(p.Timestamp)
77	if t < 0 {
78		stream.WriteRaw(`-`)
79		t = -t
80	}
81	stream.WriteInt64(t / 1000)
82	fraction := t % 1000
83	if fraction != 0 {
84		stream.WriteRaw(`.`)
85		if fraction < 100 {
86			stream.WriteRaw(`0`)
87		}
88		if fraction < 10 {
89			stream.WriteRaw(`0`)
90		}
91		stream.WriteInt64(fraction)
92	}
93	stream.WriteMore()
94	stream.WriteRaw(`"`)
95
96	// Taken from https://github.com/json-iterator/go/blob/master/stream_float.go#L71 as a workaround
97	// to https://github.com/json-iterator/go/issues/365 (jsoniter, to follow json standard, doesn't allow inf/nan)
98	buf := stream.Buffer()
99	abs := math.Abs(float64(p.Value))
100	fmt := byte('f')
101	// Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right.
102	if abs != 0 {
103		if abs < 1e-6 || abs >= 1e21 {
104			fmt = 'e'
105		}
106	}
107	buf = strconv.AppendFloat(buf, float64(p.Value), fmt, -1, 64)
108	stream.SetBuffer(buf)
109
110	stream.WriteRaw(`"`)
111	stream.WriteArrayEnd()
112
113}
114
115func marshalPointJSONIsEmpty(ptr unsafe.Pointer) bool {
116	return false
117}
118
119const (
120	statusAPIError = 422
121
122	apiPrefix = "/api/v1"
123
124	epAlerts          = apiPrefix + "/alerts"
125	epAlertManagers   = apiPrefix + "/alertmanagers"
126	epQuery           = apiPrefix + "/query"
127	epQueryRange      = apiPrefix + "/query_range"
128	epLabels          = apiPrefix + "/labels"
129	epLabelValues     = apiPrefix + "/label/:name/values"
130	epSeries          = apiPrefix + "/series"
131	epTargets         = apiPrefix + "/targets"
132	epTargetsMetadata = apiPrefix + "/targets/metadata"
133	epRules           = apiPrefix + "/rules"
134	epSnapshot        = apiPrefix + "/admin/tsdb/snapshot"
135	epDeleteSeries    = apiPrefix + "/admin/tsdb/delete_series"
136	epCleanTombstones = apiPrefix + "/admin/tsdb/clean_tombstones"
137	epConfig          = apiPrefix + "/status/config"
138	epFlags           = apiPrefix + "/status/flags"
139)
140
141// AlertState models the state of an alert.
142type AlertState string
143
144// ErrorType models the different API error types.
145type ErrorType string
146
147// HealthStatus models the health status of a scrape target.
148type HealthStatus string
149
150// RuleType models the type of a rule.
151type RuleType string
152
153// RuleHealth models the health status of a rule.
154type RuleHealth string
155
156// MetricType models the type of a metric.
157type MetricType string
158
159const (
160	// Possible values for AlertState.
161	AlertStateFiring   AlertState = "firing"
162	AlertStateInactive AlertState = "inactive"
163	AlertStatePending  AlertState = "pending"
164
165	// Possible values for ErrorType.
166	ErrBadData     ErrorType = "bad_data"
167	ErrTimeout     ErrorType = "timeout"
168	ErrCanceled    ErrorType = "canceled"
169	ErrExec        ErrorType = "execution"
170	ErrBadResponse ErrorType = "bad_response"
171	ErrServer      ErrorType = "server_error"
172	ErrClient      ErrorType = "client_error"
173
174	// Possible values for HealthStatus.
175	HealthGood    HealthStatus = "up"
176	HealthUnknown HealthStatus = "unknown"
177	HealthBad     HealthStatus = "down"
178
179	// Possible values for RuleType.
180	RuleTypeRecording RuleType = "recording"
181	RuleTypeAlerting  RuleType = "alerting"
182
183	// Possible values for RuleHealth.
184	RuleHealthGood    = "ok"
185	RuleHealthUnknown = "unknown"
186	RuleHealthBad     = "err"
187
188	// Possible values for MetricType
189	MetricTypeCounter        MetricType = "counter"
190	MetricTypeGauge          MetricType = "gauge"
191	MetricTypeHistogram      MetricType = "histogram"
192	MetricTypeGaugeHistogram MetricType = "gaugehistogram"
193	MetricTypeSummary        MetricType = "summary"
194	MetricTypeInfo           MetricType = "info"
195	MetricTypeStateset       MetricType = "stateset"
196	MetricTypeUnknown        MetricType = "unknown"
197)
198
199// Error is an error returned by the API.
200type Error struct {
201	Type   ErrorType
202	Msg    string
203	Detail string
204}
205
206func (e *Error) Error() string {
207	return fmt.Sprintf("%s: %s", e.Type, e.Msg)
208}
209
210// Range represents a sliced time range.
211type Range struct {
212	// The boundaries of the time range.
213	Start, End time.Time
214	// The maximum time between two slices within the boundaries.
215	Step time.Duration
216}
217
218// API provides bindings for Prometheus's v1 API.
219type API interface {
220	// Alerts returns a list of all active alerts.
221	Alerts(ctx context.Context) (AlertsResult, error)
222	// AlertManagers returns an overview of the current state of the Prometheus alert manager discovery.
223	AlertManagers(ctx context.Context) (AlertManagersResult, error)
224	// CleanTombstones removes the deleted data from disk and cleans up the existing tombstones.
225	CleanTombstones(ctx context.Context) error
226	// Config returns the current Prometheus configuration.
227	Config(ctx context.Context) (ConfigResult, error)
228	// DeleteSeries deletes data for a selection of series in a time range.
229	DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error
230	// Flags returns the flag values that Prometheus was launched with.
231	Flags(ctx context.Context) (FlagsResult, error)
232	// LabelNames returns all the unique label names present in the block in sorted order.
233	LabelNames(ctx context.Context) ([]string, Warnings, error)
234	// LabelValues performs a query for the values of the given label.
235	LabelValues(ctx context.Context, label string) (model.LabelValues, Warnings, error)
236	// Query performs a query for the given time.
237	Query(ctx context.Context, query string, ts time.Time) (model.Value, Warnings, error)
238	// QueryRange performs a query for the given range.
239	QueryRange(ctx context.Context, query string, r Range) (model.Value, Warnings, error)
240	// Series finds series by label matchers.
241	Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, Warnings, error)
242	// Snapshot creates a snapshot of all current data into snapshots/<datetime>-<rand>
243	// under the TSDB's data directory and returns the directory as response.
244	Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error)
245	// Rules returns a list of alerting and recording rules that are currently loaded.
246	Rules(ctx context.Context) (RulesResult, error)
247	// Targets returns an overview of the current state of the Prometheus target discovery.
248	Targets(ctx context.Context) (TargetsResult, error)
249	// TargetsMetadata returns metadata about metrics currently scraped by the target.
250	TargetsMetadata(ctx context.Context, matchTarget string, metric string, limit string) ([]MetricMetadata, error)
251}
252
253// AlertsResult contains the result from querying the alerts endpoint.
254type AlertsResult struct {
255	Alerts []Alert `json:"alerts"`
256}
257
258// AlertManagersResult contains the result from querying the alertmanagers endpoint.
259type AlertManagersResult struct {
260	Active  []AlertManager `json:"activeAlertManagers"`
261	Dropped []AlertManager `json:"droppedAlertManagers"`
262}
263
264// AlertManager models a configured Alert Manager.
265type AlertManager struct {
266	URL string `json:"url"`
267}
268
269// ConfigResult contains the result from querying the config endpoint.
270type ConfigResult struct {
271	YAML string `json:"yaml"`
272}
273
274// FlagsResult contains the result from querying the flag endpoint.
275type FlagsResult map[string]string
276
277// SnapshotResult contains the result from querying the snapshot endpoint.
278type SnapshotResult struct {
279	Name string `json:"name"`
280}
281
282// RulesResult contains the result from querying the rules endpoint.
283type RulesResult struct {
284	Groups []RuleGroup `json:"groups"`
285}
286
287// RuleGroup models a rule group that contains a set of recording and alerting rules.
288type RuleGroup struct {
289	Name     string  `json:"name"`
290	File     string  `json:"file"`
291	Interval float64 `json:"interval"`
292	Rules    Rules   `json:"rules"`
293}
294
295// Recording and alerting rules are stored in the same slice to preserve the order
296// that rules are returned in by the API.
297//
298// Rule types can be determined using a type switch:
299//   switch v := rule.(type) {
300//   case RecordingRule:
301//   	fmt.Print("got a recording rule")
302//   case AlertingRule:
303//   	fmt.Print("got a alerting rule")
304//   default:
305//   	fmt.Printf("unknown rule type %s", v)
306//   }
307type Rules []interface{}
308
309// AlertingRule models a alerting rule.
310type AlertingRule struct {
311	Name        string         `json:"name"`
312	Query       string         `json:"query"`
313	Duration    float64        `json:"duration"`
314	Labels      model.LabelSet `json:"labels"`
315	Annotations model.LabelSet `json:"annotations"`
316	Alerts      []*Alert       `json:"alerts"`
317	Health      RuleHealth     `json:"health"`
318	LastError   string         `json:"lastError,omitempty"`
319}
320
321// RecordingRule models a recording rule.
322type RecordingRule struct {
323	Name      string         `json:"name"`
324	Query     string         `json:"query"`
325	Labels    model.LabelSet `json:"labels,omitempty"`
326	Health    RuleHealth     `json:"health"`
327	LastError string         `json:"lastError,omitempty"`
328}
329
330// Alert models an active alert.
331type Alert struct {
332	ActiveAt    time.Time `json:"activeAt"`
333	Annotations model.LabelSet
334	Labels      model.LabelSet
335	State       AlertState
336	Value       string
337}
338
339// TargetsResult contains the result from querying the targets endpoint.
340type TargetsResult struct {
341	Active  []ActiveTarget  `json:"activeTargets"`
342	Dropped []DroppedTarget `json:"droppedTargets"`
343}
344
345// ActiveTarget models an active Prometheus scrape target.
346type ActiveTarget struct {
347	DiscoveredLabels map[string]string `json:"discoveredLabels"`
348	Labels           model.LabelSet    `json:"labels"`
349	ScrapeURL        string            `json:"scrapeUrl"`
350	LastError        string            `json:"lastError"`
351	LastScrape       time.Time         `json:"lastScrape"`
352	Health           HealthStatus      `json:"health"`
353}
354
355// DroppedTarget models a dropped Prometheus scrape target.
356type DroppedTarget struct {
357	DiscoveredLabels map[string]string `json:"discoveredLabels"`
358}
359
360// MetricMetadata models the metadata of a metric.
361type MetricMetadata struct {
362	Target map[string]string `json:"target"`
363	Metric string            `json:"metric,omitempty"`
364	Type   MetricType        `json:"type"`
365	Help   string            `json:"help"`
366	Unit   string            `json:"unit"`
367}
368
369// queryResult contains result data for a query.
370type queryResult struct {
371	Type   model.ValueType `json:"resultType"`
372	Result interface{}     `json:"result"`
373
374	// The decoded value.
375	v model.Value
376}
377
378func (rg *RuleGroup) UnmarshalJSON(b []byte) error {
379	v := struct {
380		Name     string            `json:"name"`
381		File     string            `json:"file"`
382		Interval float64           `json:"interval"`
383		Rules    []json.RawMessage `json:"rules"`
384	}{}
385
386	if err := json.Unmarshal(b, &v); err != nil {
387		return err
388	}
389
390	rg.Name = v.Name
391	rg.File = v.File
392	rg.Interval = v.Interval
393
394	for _, rule := range v.Rules {
395		alertingRule := AlertingRule{}
396		if err := json.Unmarshal(rule, &alertingRule); err == nil {
397			rg.Rules = append(rg.Rules, alertingRule)
398			continue
399		}
400		recordingRule := RecordingRule{}
401		if err := json.Unmarshal(rule, &recordingRule); err == nil {
402			rg.Rules = append(rg.Rules, recordingRule)
403			continue
404		}
405		return errors.New("failed to decode JSON into an alerting or recording rule")
406	}
407
408	return nil
409}
410
411func (r *AlertingRule) UnmarshalJSON(b []byte) error {
412	v := struct {
413		Type string `json:"type"`
414	}{}
415	if err := json.Unmarshal(b, &v); err != nil {
416		return err
417	}
418	if v.Type == "" {
419		return errors.New("type field not present in rule")
420	}
421	if v.Type != string(RuleTypeAlerting) {
422		return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeAlerting), v.Type)
423	}
424
425	rule := struct {
426		Name        string         `json:"name"`
427		Query       string         `json:"query"`
428		Duration    float64        `json:"duration"`
429		Labels      model.LabelSet `json:"labels"`
430		Annotations model.LabelSet `json:"annotations"`
431		Alerts      []*Alert       `json:"alerts"`
432		Health      RuleHealth     `json:"health"`
433		LastError   string         `json:"lastError,omitempty"`
434	}{}
435	if err := json.Unmarshal(b, &rule); err != nil {
436		return err
437	}
438	r.Health = rule.Health
439	r.Annotations = rule.Annotations
440	r.Name = rule.Name
441	r.Query = rule.Query
442	r.Alerts = rule.Alerts
443	r.Duration = rule.Duration
444	r.Labels = rule.Labels
445	r.LastError = rule.LastError
446
447	return nil
448}
449
450func (r *RecordingRule) UnmarshalJSON(b []byte) error {
451	v := struct {
452		Type string `json:"type"`
453	}{}
454	if err := json.Unmarshal(b, &v); err != nil {
455		return err
456	}
457	if v.Type == "" {
458		return errors.New("type field not present in rule")
459	}
460	if v.Type != string(RuleTypeRecording) {
461		return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeRecording), v.Type)
462	}
463
464	rule := struct {
465		Name      string         `json:"name"`
466		Query     string         `json:"query"`
467		Labels    model.LabelSet `json:"labels,omitempty"`
468		Health    RuleHealth     `json:"health"`
469		LastError string         `json:"lastError,omitempty"`
470	}{}
471	if err := json.Unmarshal(b, &rule); err != nil {
472		return err
473	}
474	r.Health = rule.Health
475	r.Labels = rule.Labels
476	r.Name = rule.Name
477	r.LastError = rule.LastError
478	r.Query = rule.Query
479
480	return nil
481}
482
483func (qr *queryResult) UnmarshalJSON(b []byte) error {
484	v := struct {
485		Type   model.ValueType `json:"resultType"`
486		Result json.RawMessage `json:"result"`
487	}{}
488
489	err := json.Unmarshal(b, &v)
490	if err != nil {
491		return err
492	}
493
494	switch v.Type {
495	case model.ValScalar:
496		var sv model.Scalar
497		err = json.Unmarshal(v.Result, &sv)
498		qr.v = &sv
499
500	case model.ValVector:
501		var vv model.Vector
502		err = json.Unmarshal(v.Result, &vv)
503		qr.v = vv
504
505	case model.ValMatrix:
506		var mv model.Matrix
507		err = json.Unmarshal(v.Result, &mv)
508		qr.v = mv
509
510	default:
511		err = fmt.Errorf("unexpected value type %q", v.Type)
512	}
513	return err
514}
515
516// NewAPI returns a new API for the client.
517//
518// It is safe to use the returned API from multiple goroutines.
519func NewAPI(c api.Client) API {
520	return &httpAPI{
521		client: &apiClientImpl{
522			client: c,
523		},
524	}
525}
526
527type httpAPI struct {
528	client apiClient
529}
530
531func (h *httpAPI) Alerts(ctx context.Context) (AlertsResult, error) {
532	u := h.client.URL(epAlerts, nil)
533
534	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
535	if err != nil {
536		return AlertsResult{}, err
537	}
538
539	_, body, _, err := h.client.Do(ctx, req)
540	if err != nil {
541		return AlertsResult{}, err
542	}
543
544	var res AlertsResult
545	return res, json.Unmarshal(body, &res)
546}
547
548func (h *httpAPI) AlertManagers(ctx context.Context) (AlertManagersResult, error) {
549	u := h.client.URL(epAlertManagers, nil)
550
551	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
552	if err != nil {
553		return AlertManagersResult{}, err
554	}
555
556	_, body, _, err := h.client.Do(ctx, req)
557	if err != nil {
558		return AlertManagersResult{}, err
559	}
560
561	var res AlertManagersResult
562	return res, json.Unmarshal(body, &res)
563}
564
565func (h *httpAPI) CleanTombstones(ctx context.Context) error {
566	u := h.client.URL(epCleanTombstones, nil)
567
568	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
569	if err != nil {
570		return err
571	}
572
573	_, _, _, err = h.client.Do(ctx, req)
574	return err
575}
576
577func (h *httpAPI) Config(ctx context.Context) (ConfigResult, error) {
578	u := h.client.URL(epConfig, nil)
579
580	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
581	if err != nil {
582		return ConfigResult{}, err
583	}
584
585	_, body, _, err := h.client.Do(ctx, req)
586	if err != nil {
587		return ConfigResult{}, err
588	}
589
590	var res ConfigResult
591	return res, json.Unmarshal(body, &res)
592}
593
594func (h *httpAPI) DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error {
595	u := h.client.URL(epDeleteSeries, nil)
596	q := u.Query()
597
598	for _, m := range matches {
599		q.Add("match[]", m)
600	}
601
602	q.Set("start", formatTime(startTime))
603	q.Set("end", formatTime(endTime))
604
605	u.RawQuery = q.Encode()
606
607	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
608	if err != nil {
609		return err
610	}
611
612	_, _, _, err = h.client.Do(ctx, req)
613	return err
614}
615
616func (h *httpAPI) Flags(ctx context.Context) (FlagsResult, error) {
617	u := h.client.URL(epFlags, nil)
618
619	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
620	if err != nil {
621		return FlagsResult{}, err
622	}
623
624	_, body, _, err := h.client.Do(ctx, req)
625	if err != nil {
626		return FlagsResult{}, err
627	}
628
629	var res FlagsResult
630	return res, json.Unmarshal(body, &res)
631}
632
633func (h *httpAPI) LabelNames(ctx context.Context) ([]string, Warnings, error) {
634	u := h.client.URL(epLabels, nil)
635	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
636	if err != nil {
637		return nil, nil, err
638	}
639	_, body, w, err := h.client.Do(ctx, req)
640	if err != nil {
641		return nil, w, err
642	}
643	var labelNames []string
644	return labelNames, w, json.Unmarshal(body, &labelNames)
645}
646
647func (h *httpAPI) LabelValues(ctx context.Context, label string) (model.LabelValues, Warnings, error) {
648	u := h.client.URL(epLabelValues, map[string]string{"name": label})
649	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
650	if err != nil {
651		return nil, nil, err
652	}
653	_, body, w, err := h.client.Do(ctx, req)
654	if err != nil {
655		return nil, w, err
656	}
657	var labelValues model.LabelValues
658	return labelValues, w, json.Unmarshal(body, &labelValues)
659}
660
661func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, Warnings, error) {
662	u := h.client.URL(epQuery, nil)
663	q := u.Query()
664
665	q.Set("query", query)
666	if !ts.IsZero() {
667		q.Set("time", formatTime(ts))
668	}
669
670	_, body, warnings, err := h.client.DoGetFallback(ctx, u, q)
671	if err != nil {
672		return nil, warnings, err
673	}
674
675	var qres queryResult
676	return model.Value(qres.v), warnings, json.Unmarshal(body, &qres)
677}
678
679func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, Warnings, error) {
680	u := h.client.URL(epQueryRange, nil)
681	q := u.Query()
682
683	q.Set("query", query)
684	q.Set("start", formatTime(r.Start))
685	q.Set("end", formatTime(r.End))
686	q.Set("step", strconv.FormatFloat(r.Step.Seconds(), 'f', -1, 64))
687
688	_, body, warnings, err := h.client.DoGetFallback(ctx, u, q)
689	if err != nil {
690		return nil, warnings, err
691	}
692
693	var qres queryResult
694
695	return model.Value(qres.v), warnings, json.Unmarshal(body, &qres)
696}
697
698func (h *httpAPI) Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, Warnings, error) {
699	u := h.client.URL(epSeries, nil)
700	q := u.Query()
701
702	for _, m := range matches {
703		q.Add("match[]", m)
704	}
705
706	q.Set("start", formatTime(startTime))
707	q.Set("end", formatTime(endTime))
708
709	u.RawQuery = q.Encode()
710
711	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
712	if err != nil {
713		return nil, nil, err
714	}
715
716	_, body, warnings, err := h.client.Do(ctx, req)
717	if err != nil {
718		return nil, warnings, err
719	}
720
721	var mset []model.LabelSet
722	return mset, warnings, json.Unmarshal(body, &mset)
723}
724
725func (h *httpAPI) Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error) {
726	u := h.client.URL(epSnapshot, nil)
727	q := u.Query()
728
729	q.Set("skip_head", strconv.FormatBool(skipHead))
730
731	u.RawQuery = q.Encode()
732
733	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
734	if err != nil {
735		return SnapshotResult{}, err
736	}
737
738	_, body, _, err := h.client.Do(ctx, req)
739	if err != nil {
740		return SnapshotResult{}, err
741	}
742
743	var res SnapshotResult
744	return res, json.Unmarshal(body, &res)
745}
746
747func (h *httpAPI) Rules(ctx context.Context) (RulesResult, error) {
748	u := h.client.URL(epRules, nil)
749
750	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
751	if err != nil {
752		return RulesResult{}, err
753	}
754
755	_, body, _, err := h.client.Do(ctx, req)
756	if err != nil {
757		return RulesResult{}, err
758	}
759
760	var res RulesResult
761	return res, json.Unmarshal(body, &res)
762}
763
764func (h *httpAPI) Targets(ctx context.Context) (TargetsResult, error) {
765	u := h.client.URL(epTargets, nil)
766
767	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
768	if err != nil {
769		return TargetsResult{}, err
770	}
771
772	_, body, _, err := h.client.Do(ctx, req)
773	if err != nil {
774		return TargetsResult{}, err
775	}
776
777	var res TargetsResult
778	return res, json.Unmarshal(body, &res)
779}
780
781func (h *httpAPI) TargetsMetadata(ctx context.Context, matchTarget string, metric string, limit string) ([]MetricMetadata, error) {
782	u := h.client.URL(epTargetsMetadata, nil)
783	q := u.Query()
784
785	q.Set("match_target", matchTarget)
786	q.Set("metric", metric)
787	q.Set("limit", limit)
788
789	u.RawQuery = q.Encode()
790
791	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
792	if err != nil {
793		return nil, err
794	}
795
796	_, body, _, err := h.client.Do(ctx, req)
797	if err != nil {
798		return nil, err
799	}
800
801	var res []MetricMetadata
802	return res, json.Unmarshal(body, &res)
803}
804
805// Warnings is an array of non critical errors
806type Warnings []string
807
808// apiClient wraps a regular client and processes successful API responses.
809// Successful also includes responses that errored at the API level.
810type apiClient interface {
811	URL(ep string, args map[string]string) *url.URL
812	Do(context.Context, *http.Request) (*http.Response, []byte, Warnings, error)
813	DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, error)
814}
815
816type apiClientImpl struct {
817	client api.Client
818}
819
820type apiResponse struct {
821	Status    string          `json:"status"`
822	Data      json.RawMessage `json:"data"`
823	ErrorType ErrorType       `json:"errorType"`
824	Error     string          `json:"error"`
825	Warnings  []string        `json:"warnings,omitempty"`
826}
827
828func apiError(code int) bool {
829	// These are the codes that Prometheus sends when it returns an error.
830	return code == statusAPIError || code == http.StatusBadRequest
831}
832
833func errorTypeAndMsgFor(resp *http.Response) (ErrorType, string) {
834	switch resp.StatusCode / 100 {
835	case 4:
836		return ErrClient, fmt.Sprintf("client error: %d", resp.StatusCode)
837	case 5:
838		return ErrServer, fmt.Sprintf("server error: %d", resp.StatusCode)
839	}
840	return ErrBadResponse, fmt.Sprintf("bad response code %d", resp.StatusCode)
841}
842
843func (h *apiClientImpl) URL(ep string, args map[string]string) *url.URL {
844	return h.client.URL(ep, args)
845}
846
847func (h *apiClientImpl) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, Warnings, error) {
848	resp, body, err := h.client.Do(ctx, req)
849	if err != nil {
850		return resp, body, nil, err
851	}
852
853	code := resp.StatusCode
854
855	if code/100 != 2 && !apiError(code) {
856		errorType, errorMsg := errorTypeAndMsgFor(resp)
857		return resp, body, nil, &Error{
858			Type:   errorType,
859			Msg:    errorMsg,
860			Detail: string(body),
861		}
862	}
863
864	var result apiResponse
865
866	if http.StatusNoContent != code {
867		if jsonErr := json.Unmarshal(body, &result); jsonErr != nil {
868			return resp, body, nil, &Error{
869				Type: ErrBadResponse,
870				Msg:  jsonErr.Error(),
871			}
872		}
873	}
874
875	if apiError(code) != (result.Status == "error") {
876		err = &Error{
877			Type: ErrBadResponse,
878			Msg:  "inconsistent body for response code",
879		}
880	}
881
882	if apiError(code) && result.Status == "error" {
883		err = &Error{
884			Type: result.ErrorType,
885			Msg:  result.Error,
886		}
887	}
888
889	return resp, []byte(result.Data), result.Warnings, err
890
891}
892
893// DoGetFallback will attempt to do the request as-is, and on a 405 it will fallback to a GET request.
894func (h *apiClientImpl) DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, error) {
895	req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(args.Encode()))
896	if err != nil {
897		return nil, nil, nil, err
898	}
899	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
900
901	resp, body, warnings, err := h.Do(ctx, req)
902	if resp != nil && resp.StatusCode == http.StatusMethodNotAllowed {
903		u.RawQuery = args.Encode()
904		req, err = http.NewRequest(http.MethodGet, u.String(), nil)
905		if err != nil {
906			return nil, nil, warnings, err
907		}
908
909	} else {
910		if err != nil {
911			return resp, body, warnings, err
912		}
913		return resp, body, warnings, nil
914	}
915	return h.Do(ctx, req)
916}
917
918func formatTime(t time.Time) string {
919	return strconv.FormatFloat(float64(t.Unix())+float64(t.Nanosecond())/1e9, 'f', -1, 64)
920}
921