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	apiPrefix = "/api/v1"
121
122	epAlerts          = apiPrefix + "/alerts"
123	epAlertManagers   = apiPrefix + "/alertmanagers"
124	epQuery           = apiPrefix + "/query"
125	epQueryRange      = apiPrefix + "/query_range"
126	epLabels          = apiPrefix + "/labels"
127	epLabelValues     = apiPrefix + "/label/:name/values"
128	epSeries          = apiPrefix + "/series"
129	epTargets         = apiPrefix + "/targets"
130	epTargetsMetadata = apiPrefix + "/targets/metadata"
131	epMetadata        = apiPrefix + "/metadata"
132	epRules           = apiPrefix + "/rules"
133	epSnapshot        = apiPrefix + "/admin/tsdb/snapshot"
134	epDeleteSeries    = apiPrefix + "/admin/tsdb/delete_series"
135	epCleanTombstones = apiPrefix + "/admin/tsdb/clean_tombstones"
136	epConfig          = apiPrefix + "/status/config"
137	epFlags           = apiPrefix + "/status/flags"
138	epBuildinfo       = apiPrefix + "/status/buildinfo"
139	epRuntimeinfo     = apiPrefix + "/status/runtimeinfo"
140	epTSDB            = apiPrefix + "/status/tsdb"
141)
142
143// AlertState models the state of an alert.
144type AlertState string
145
146// ErrorType models the different API error types.
147type ErrorType string
148
149// HealthStatus models the health status of a scrape target.
150type HealthStatus string
151
152// RuleType models the type of a rule.
153type RuleType string
154
155// RuleHealth models the health status of a rule.
156type RuleHealth string
157
158// MetricType models the type of a metric.
159type MetricType string
160
161const (
162	// Possible values for AlertState.
163	AlertStateFiring   AlertState = "firing"
164	AlertStateInactive AlertState = "inactive"
165	AlertStatePending  AlertState = "pending"
166
167	// Possible values for ErrorType.
168	ErrBadData     ErrorType = "bad_data"
169	ErrTimeout     ErrorType = "timeout"
170	ErrCanceled    ErrorType = "canceled"
171	ErrExec        ErrorType = "execution"
172	ErrBadResponse ErrorType = "bad_response"
173	ErrServer      ErrorType = "server_error"
174	ErrClient      ErrorType = "client_error"
175
176	// Possible values for HealthStatus.
177	HealthGood    HealthStatus = "up"
178	HealthUnknown HealthStatus = "unknown"
179	HealthBad     HealthStatus = "down"
180
181	// Possible values for RuleType.
182	RuleTypeRecording RuleType = "recording"
183	RuleTypeAlerting  RuleType = "alerting"
184
185	// Possible values for RuleHealth.
186	RuleHealthGood    = "ok"
187	RuleHealthUnknown = "unknown"
188	RuleHealthBad     = "err"
189
190	// Possible values for MetricType
191	MetricTypeCounter        MetricType = "counter"
192	MetricTypeGauge          MetricType = "gauge"
193	MetricTypeHistogram      MetricType = "histogram"
194	MetricTypeGaugeHistogram MetricType = "gaugehistogram"
195	MetricTypeSummary        MetricType = "summary"
196	MetricTypeInfo           MetricType = "info"
197	MetricTypeStateset       MetricType = "stateset"
198	MetricTypeUnknown        MetricType = "unknown"
199)
200
201// Error is an error returned by the API.
202type Error struct {
203	Type   ErrorType
204	Msg    string
205	Detail string
206}
207
208func (e *Error) Error() string {
209	return fmt.Sprintf("%s: %s", e.Type, e.Msg)
210}
211
212// Range represents a sliced time range.
213type Range struct {
214	// The boundaries of the time range.
215	Start, End time.Time
216	// The maximum time between two slices within the boundaries.
217	Step time.Duration
218}
219
220// API provides bindings for Prometheus's v1 API.
221type API interface {
222	// Alerts returns a list of all active alerts.
223	Alerts(ctx context.Context) (AlertsResult, error)
224	// AlertManagers returns an overview of the current state of the Prometheus alert manager discovery.
225	AlertManagers(ctx context.Context) (AlertManagersResult, error)
226	// CleanTombstones removes the deleted data from disk and cleans up the existing tombstones.
227	CleanTombstones(ctx context.Context) error
228	// Config returns the current Prometheus configuration.
229	Config(ctx context.Context) (ConfigResult, error)
230	// DeleteSeries deletes data for a selection of series in a time range.
231	DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error
232	// Flags returns the flag values that Prometheus was launched with.
233	Flags(ctx context.Context) (FlagsResult, error)
234	// LabelNames returns the unique label names present in the block in sorted order by given time range and matchers.
235	LabelNames(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]string, Warnings, error)
236	// LabelValues performs a query for the values of the given label, time range and matchers.
237	LabelValues(ctx context.Context, label string, matches []string, startTime time.Time, endTime time.Time) (model.LabelValues, Warnings, error)
238	// Query performs a query for the given time.
239	Query(ctx context.Context, query string, ts time.Time) (model.Value, Warnings, error)
240	// QueryRange performs a query for the given range.
241	QueryRange(ctx context.Context, query string, r Range) (model.Value, Warnings, error)
242	// Buildinfo returns various build information properties about the Prometheus server
243	Buildinfo(ctx context.Context) (BuildinfoResult, error)
244	// Runtimeinfo returns the various runtime information properties about the Prometheus server.
245	Runtimeinfo(ctx context.Context) (RuntimeinfoResult, error)
246	// Series finds series by label matchers.
247	Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, Warnings, error)
248	// Snapshot creates a snapshot of all current data into snapshots/<datetime>-<rand>
249	// under the TSDB's data directory and returns the directory as response.
250	Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error)
251	// Rules returns a list of alerting and recording rules that are currently loaded.
252	Rules(ctx context.Context) (RulesResult, error)
253	// Targets returns an overview of the current state of the Prometheus target discovery.
254	Targets(ctx context.Context) (TargetsResult, error)
255	// TargetsMetadata returns metadata about metrics currently scraped by the target.
256	TargetsMetadata(ctx context.Context, matchTarget string, metric string, limit string) ([]MetricMetadata, error)
257	// Metadata returns metadata about metrics currently scraped by the metric name.
258	Metadata(ctx context.Context, metric string, limit string) (map[string][]Metadata, error)
259	// TSDB returns the cardinality statistics.
260	TSDB(ctx context.Context) (TSDBResult, error)
261}
262
263// AlertsResult contains the result from querying the alerts endpoint.
264type AlertsResult struct {
265	Alerts []Alert `json:"alerts"`
266}
267
268// AlertManagersResult contains the result from querying the alertmanagers endpoint.
269type AlertManagersResult struct {
270	Active  []AlertManager `json:"activeAlertManagers"`
271	Dropped []AlertManager `json:"droppedAlertManagers"`
272}
273
274// AlertManager models a configured Alert Manager.
275type AlertManager struct {
276	URL string `json:"url"`
277}
278
279// ConfigResult contains the result from querying the config endpoint.
280type ConfigResult struct {
281	YAML string `json:"yaml"`
282}
283
284// FlagsResult contains the result from querying the flag endpoint.
285type FlagsResult map[string]string
286
287// BuildinfoResult contains the results from querying the buildinfo endpoint.
288type BuildinfoResult struct {
289	Version   string `json:"version"`
290	Revision  string `json:"revision"`
291	Branch    string `json:"branch"`
292	BuildUser string `json:"buildUser"`
293	BuildDate string `json:"buildDate"`
294	GoVersion string `json:"goVersion"`
295}
296
297// RuntimeinfoResult contains the result from querying the runtimeinfo endpoint.
298type RuntimeinfoResult struct {
299	StartTime           time.Time `json:"startTime"`
300	CWD                 string    `json:"CWD"`
301	ReloadConfigSuccess bool      `json:"reloadConfigSuccess"`
302	LastConfigTime      time.Time `json:"lastConfigTime"`
303	ChunkCount          int       `json:"chunkCount"`
304	TimeSeriesCount     int       `json:"timeSeriesCount"`
305	CorruptionCount     int       `json:"corruptionCount"`
306	GoroutineCount      int       `json:"goroutineCount"`
307	GOMAXPROCS          int       `json:"GOMAXPROCS"`
308	GOGC                string    `json:"GOGC"`
309	GODEBUG             string    `json:"GODEBUG"`
310	StorageRetention    string    `json:"storageRetention"`
311}
312
313// SnapshotResult contains the result from querying the snapshot endpoint.
314type SnapshotResult struct {
315	Name string `json:"name"`
316}
317
318// RulesResult contains the result from querying the rules endpoint.
319type RulesResult struct {
320	Groups []RuleGroup `json:"groups"`
321}
322
323// RuleGroup models a rule group that contains a set of recording and alerting rules.
324type RuleGroup struct {
325	Name     string  `json:"name"`
326	File     string  `json:"file"`
327	Interval float64 `json:"interval"`
328	Rules    Rules   `json:"rules"`
329}
330
331// Recording and alerting rules are stored in the same slice to preserve the order
332// that rules are returned in by the API.
333//
334// Rule types can be determined using a type switch:
335//   switch v := rule.(type) {
336//   case RecordingRule:
337//   	fmt.Print("got a recording rule")
338//   case AlertingRule:
339//   	fmt.Print("got a alerting rule")
340//   default:
341//   	fmt.Printf("unknown rule type %s", v)
342//   }
343type Rules []interface{}
344
345// AlertingRule models a alerting rule.
346type AlertingRule struct {
347	Name        string         `json:"name"`
348	Query       string         `json:"query"`
349	Duration    float64        `json:"duration"`
350	Labels      model.LabelSet `json:"labels"`
351	Annotations model.LabelSet `json:"annotations"`
352	Alerts      []*Alert       `json:"alerts"`
353	Health      RuleHealth     `json:"health"`
354	LastError   string         `json:"lastError,omitempty"`
355}
356
357// RecordingRule models a recording rule.
358type RecordingRule struct {
359	Name      string         `json:"name"`
360	Query     string         `json:"query"`
361	Labels    model.LabelSet `json:"labels,omitempty"`
362	Health    RuleHealth     `json:"health"`
363	LastError string         `json:"lastError,omitempty"`
364}
365
366// Alert models an active alert.
367type Alert struct {
368	ActiveAt    time.Time `json:"activeAt"`
369	Annotations model.LabelSet
370	Labels      model.LabelSet
371	State       AlertState
372	Value       string
373}
374
375// TargetsResult contains the result from querying the targets endpoint.
376type TargetsResult struct {
377	Active  []ActiveTarget  `json:"activeTargets"`
378	Dropped []DroppedTarget `json:"droppedTargets"`
379}
380
381// ActiveTarget models an active Prometheus scrape target.
382type ActiveTarget struct {
383	DiscoveredLabels map[string]string `json:"discoveredLabels"`
384	Labels           model.LabelSet    `json:"labels"`
385	ScrapeURL        string            `json:"scrapeUrl"`
386	LastError        string            `json:"lastError"`
387	LastScrape       time.Time         `json:"lastScrape"`
388	Health           HealthStatus      `json:"health"`
389}
390
391// DroppedTarget models a dropped Prometheus scrape target.
392type DroppedTarget struct {
393	DiscoveredLabels map[string]string `json:"discoveredLabels"`
394}
395
396// MetricMetadata models the metadata of a metric with its scrape target and name.
397type MetricMetadata struct {
398	Target map[string]string `json:"target"`
399	Metric string            `json:"metric,omitempty"`
400	Type   MetricType        `json:"type"`
401	Help   string            `json:"help"`
402	Unit   string            `json:"unit"`
403}
404
405// Metadata models the metadata of a metric.
406type Metadata struct {
407	Type MetricType `json:"type"`
408	Help string     `json:"help"`
409	Unit string     `json:"unit"`
410}
411
412// queryResult contains result data for a query.
413type queryResult struct {
414	Type   model.ValueType `json:"resultType"`
415	Result interface{}     `json:"result"`
416
417	// The decoded value.
418	v model.Value
419}
420
421// TSDBResult contains the result from querying the tsdb endpoint.
422type TSDBResult struct {
423	SeriesCountByMetricName     []Stat `json:"seriesCountByMetricName"`
424	LabelValueCountByLabelName  []Stat `json:"labelValueCountByLabelName"`
425	MemoryInBytesByLabelName    []Stat `json:"memoryInBytesByLabelName"`
426	SeriesCountByLabelValuePair []Stat `json:"seriesCountByLabelValuePair"`
427}
428
429// Stat models information about statistic value.
430type Stat struct {
431	Name  string `json:"name"`
432	Value uint64 `json:"value"`
433}
434
435func (rg *RuleGroup) UnmarshalJSON(b []byte) error {
436	v := struct {
437		Name     string            `json:"name"`
438		File     string            `json:"file"`
439		Interval float64           `json:"interval"`
440		Rules    []json.RawMessage `json:"rules"`
441	}{}
442
443	if err := json.Unmarshal(b, &v); err != nil {
444		return err
445	}
446
447	rg.Name = v.Name
448	rg.File = v.File
449	rg.Interval = v.Interval
450
451	for _, rule := range v.Rules {
452		alertingRule := AlertingRule{}
453		if err := json.Unmarshal(rule, &alertingRule); err == nil {
454			rg.Rules = append(rg.Rules, alertingRule)
455			continue
456		}
457		recordingRule := RecordingRule{}
458		if err := json.Unmarshal(rule, &recordingRule); err == nil {
459			rg.Rules = append(rg.Rules, recordingRule)
460			continue
461		}
462		return errors.New("failed to decode JSON into an alerting or recording rule")
463	}
464
465	return nil
466}
467
468func (r *AlertingRule) UnmarshalJSON(b []byte) error {
469	v := struct {
470		Type string `json:"type"`
471	}{}
472	if err := json.Unmarshal(b, &v); err != nil {
473		return err
474	}
475	if v.Type == "" {
476		return errors.New("type field not present in rule")
477	}
478	if v.Type != string(RuleTypeAlerting) {
479		return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeAlerting), v.Type)
480	}
481
482	rule := struct {
483		Name        string         `json:"name"`
484		Query       string         `json:"query"`
485		Duration    float64        `json:"duration"`
486		Labels      model.LabelSet `json:"labels"`
487		Annotations model.LabelSet `json:"annotations"`
488		Alerts      []*Alert       `json:"alerts"`
489		Health      RuleHealth     `json:"health"`
490		LastError   string         `json:"lastError,omitempty"`
491	}{}
492	if err := json.Unmarshal(b, &rule); err != nil {
493		return err
494	}
495	r.Health = rule.Health
496	r.Annotations = rule.Annotations
497	r.Name = rule.Name
498	r.Query = rule.Query
499	r.Alerts = rule.Alerts
500	r.Duration = rule.Duration
501	r.Labels = rule.Labels
502	r.LastError = rule.LastError
503
504	return nil
505}
506
507func (r *RecordingRule) UnmarshalJSON(b []byte) error {
508	v := struct {
509		Type string `json:"type"`
510	}{}
511	if err := json.Unmarshal(b, &v); err != nil {
512		return err
513	}
514	if v.Type == "" {
515		return errors.New("type field not present in rule")
516	}
517	if v.Type != string(RuleTypeRecording) {
518		return fmt.Errorf("expected rule of type %s but got %s", string(RuleTypeRecording), v.Type)
519	}
520
521	rule := struct {
522		Name      string         `json:"name"`
523		Query     string         `json:"query"`
524		Labels    model.LabelSet `json:"labels,omitempty"`
525		Health    RuleHealth     `json:"health"`
526		LastError string         `json:"lastError,omitempty"`
527	}{}
528	if err := json.Unmarshal(b, &rule); err != nil {
529		return err
530	}
531	r.Health = rule.Health
532	r.Labels = rule.Labels
533	r.Name = rule.Name
534	r.LastError = rule.LastError
535	r.Query = rule.Query
536
537	return nil
538}
539
540func (qr *queryResult) UnmarshalJSON(b []byte) error {
541	v := struct {
542		Type   model.ValueType `json:"resultType"`
543		Result json.RawMessage `json:"result"`
544	}{}
545
546	err := json.Unmarshal(b, &v)
547	if err != nil {
548		return err
549	}
550
551	switch v.Type {
552	case model.ValScalar:
553		var sv model.Scalar
554		err = json.Unmarshal(v.Result, &sv)
555		qr.v = &sv
556
557	case model.ValVector:
558		var vv model.Vector
559		err = json.Unmarshal(v.Result, &vv)
560		qr.v = vv
561
562	case model.ValMatrix:
563		var mv model.Matrix
564		err = json.Unmarshal(v.Result, &mv)
565		qr.v = mv
566
567	default:
568		err = fmt.Errorf("unexpected value type %q", v.Type)
569	}
570	return err
571}
572
573// NewAPI returns a new API for the client.
574//
575// It is safe to use the returned API from multiple goroutines.
576func NewAPI(c api.Client) API {
577	return &httpAPI{
578		client: &apiClientImpl{
579			client: c,
580		},
581	}
582}
583
584type httpAPI struct {
585	client apiClient
586}
587
588func (h *httpAPI) Alerts(ctx context.Context) (AlertsResult, error) {
589	u := h.client.URL(epAlerts, nil)
590
591	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
592	if err != nil {
593		return AlertsResult{}, err
594	}
595
596	_, body, _, err := h.client.Do(ctx, req)
597	if err != nil {
598		return AlertsResult{}, err
599	}
600
601	var res AlertsResult
602	return res, json.Unmarshal(body, &res)
603}
604
605func (h *httpAPI) AlertManagers(ctx context.Context) (AlertManagersResult, error) {
606	u := h.client.URL(epAlertManagers, nil)
607
608	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
609	if err != nil {
610		return AlertManagersResult{}, err
611	}
612
613	_, body, _, err := h.client.Do(ctx, req)
614	if err != nil {
615		return AlertManagersResult{}, err
616	}
617
618	var res AlertManagersResult
619	return res, json.Unmarshal(body, &res)
620}
621
622func (h *httpAPI) CleanTombstones(ctx context.Context) error {
623	u := h.client.URL(epCleanTombstones, nil)
624
625	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
626	if err != nil {
627		return err
628	}
629
630	_, _, _, err = h.client.Do(ctx, req)
631	return err
632}
633
634func (h *httpAPI) Config(ctx context.Context) (ConfigResult, error) {
635	u := h.client.URL(epConfig, nil)
636
637	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
638	if err != nil {
639		return ConfigResult{}, err
640	}
641
642	_, body, _, err := h.client.Do(ctx, req)
643	if err != nil {
644		return ConfigResult{}, err
645	}
646
647	var res ConfigResult
648	return res, json.Unmarshal(body, &res)
649}
650
651func (h *httpAPI) DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error {
652	u := h.client.URL(epDeleteSeries, nil)
653	q := u.Query()
654
655	for _, m := range matches {
656		q.Add("match[]", m)
657	}
658
659	q.Set("start", formatTime(startTime))
660	q.Set("end", formatTime(endTime))
661
662	u.RawQuery = q.Encode()
663
664	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
665	if err != nil {
666		return err
667	}
668
669	_, _, _, err = h.client.Do(ctx, req)
670	return err
671}
672
673func (h *httpAPI) Flags(ctx context.Context) (FlagsResult, error) {
674	u := h.client.URL(epFlags, nil)
675
676	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
677	if err != nil {
678		return FlagsResult{}, err
679	}
680
681	_, body, _, err := h.client.Do(ctx, req)
682	if err != nil {
683		return FlagsResult{}, err
684	}
685
686	var res FlagsResult
687	return res, json.Unmarshal(body, &res)
688}
689
690func (h *httpAPI) Buildinfo(ctx context.Context) (BuildinfoResult, error) {
691	u := h.client.URL(epBuildinfo, nil)
692
693	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
694	if err != nil {
695		return BuildinfoResult{}, err
696	}
697
698	_, body, _, err := h.client.Do(ctx, req)
699	if err != nil {
700		return BuildinfoResult{}, err
701	}
702
703	var res BuildinfoResult
704	return res, json.Unmarshal(body, &res)
705}
706
707func (h *httpAPI) Runtimeinfo(ctx context.Context) (RuntimeinfoResult, error) {
708	u := h.client.URL(epRuntimeinfo, nil)
709
710	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
711	if err != nil {
712		return RuntimeinfoResult{}, err
713	}
714
715	_, body, _, err := h.client.Do(ctx, req)
716	if err != nil {
717		return RuntimeinfoResult{}, err
718	}
719
720	var res RuntimeinfoResult
721	return res, json.Unmarshal(body, &res)
722}
723
724func (h *httpAPI) LabelNames(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]string, Warnings, error) {
725	u := h.client.URL(epLabels, nil)
726	q := u.Query()
727	q.Set("start", formatTime(startTime))
728	q.Set("end", formatTime(endTime))
729	for _, m := range matches {
730		q.Add("match[]", m)
731	}
732
733	u.RawQuery = q.Encode()
734
735	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
736	if err != nil {
737		return nil, nil, err
738	}
739	_, body, w, err := h.client.Do(ctx, req)
740	if err != nil {
741		return nil, w, err
742	}
743	var labelNames []string
744	return labelNames, w, json.Unmarshal(body, &labelNames)
745}
746
747func (h *httpAPI) LabelValues(ctx context.Context, label string, matches []string, startTime time.Time, endTime time.Time) (model.LabelValues, Warnings, error) {
748	u := h.client.URL(epLabelValues, map[string]string{"name": label})
749	q := u.Query()
750	q.Set("start", formatTime(startTime))
751	q.Set("end", formatTime(endTime))
752	for _, m := range matches {
753		q.Add("match[]", m)
754	}
755
756	u.RawQuery = q.Encode()
757
758	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
759	if err != nil {
760		return nil, nil, err
761	}
762	_, body, w, err := h.client.Do(ctx, req)
763	if err != nil {
764		return nil, w, err
765	}
766	var labelValues model.LabelValues
767	return labelValues, w, json.Unmarshal(body, &labelValues)
768}
769
770func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, Warnings, error) {
771	u := h.client.URL(epQuery, nil)
772	q := u.Query()
773
774	q.Set("query", query)
775	if !ts.IsZero() {
776		q.Set("time", formatTime(ts))
777	}
778
779	_, body, warnings, err := h.client.DoGetFallback(ctx, u, q)
780	if err != nil {
781		return nil, warnings, err
782	}
783
784	var qres queryResult
785	return model.Value(qres.v), warnings, json.Unmarshal(body, &qres)
786}
787
788func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, Warnings, error) {
789	u := h.client.URL(epQueryRange, nil)
790	q := u.Query()
791
792	q.Set("query", query)
793	q.Set("start", formatTime(r.Start))
794	q.Set("end", formatTime(r.End))
795	q.Set("step", strconv.FormatFloat(r.Step.Seconds(), 'f', -1, 64))
796
797	_, body, warnings, err := h.client.DoGetFallback(ctx, u, q)
798	if err != nil {
799		return nil, warnings, err
800	}
801
802	var qres queryResult
803
804	return model.Value(qres.v), warnings, json.Unmarshal(body, &qres)
805}
806
807func (h *httpAPI) Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, Warnings, error) {
808	u := h.client.URL(epSeries, nil)
809	q := u.Query()
810
811	for _, m := range matches {
812		q.Add("match[]", m)
813	}
814
815	q.Set("start", formatTime(startTime))
816	q.Set("end", formatTime(endTime))
817
818	u.RawQuery = q.Encode()
819
820	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
821	if err != nil {
822		return nil, nil, err
823	}
824
825	_, body, warnings, err := h.client.Do(ctx, req)
826	if err != nil {
827		return nil, warnings, err
828	}
829
830	var mset []model.LabelSet
831	return mset, warnings, json.Unmarshal(body, &mset)
832}
833
834func (h *httpAPI) Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error) {
835	u := h.client.URL(epSnapshot, nil)
836	q := u.Query()
837
838	q.Set("skip_head", strconv.FormatBool(skipHead))
839
840	u.RawQuery = q.Encode()
841
842	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
843	if err != nil {
844		return SnapshotResult{}, err
845	}
846
847	_, body, _, err := h.client.Do(ctx, req)
848	if err != nil {
849		return SnapshotResult{}, err
850	}
851
852	var res SnapshotResult
853	return res, json.Unmarshal(body, &res)
854}
855
856func (h *httpAPI) Rules(ctx context.Context) (RulesResult, error) {
857	u := h.client.URL(epRules, nil)
858
859	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
860	if err != nil {
861		return RulesResult{}, err
862	}
863
864	_, body, _, err := h.client.Do(ctx, req)
865	if err != nil {
866		return RulesResult{}, err
867	}
868
869	var res RulesResult
870	return res, json.Unmarshal(body, &res)
871}
872
873func (h *httpAPI) Targets(ctx context.Context) (TargetsResult, error) {
874	u := h.client.URL(epTargets, nil)
875
876	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
877	if err != nil {
878		return TargetsResult{}, err
879	}
880
881	_, body, _, err := h.client.Do(ctx, req)
882	if err != nil {
883		return TargetsResult{}, err
884	}
885
886	var res TargetsResult
887	return res, json.Unmarshal(body, &res)
888}
889
890func (h *httpAPI) TargetsMetadata(ctx context.Context, matchTarget string, metric string, limit string) ([]MetricMetadata, error) {
891	u := h.client.URL(epTargetsMetadata, nil)
892	q := u.Query()
893
894	q.Set("match_target", matchTarget)
895	q.Set("metric", metric)
896	q.Set("limit", limit)
897
898	u.RawQuery = q.Encode()
899
900	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
901	if err != nil {
902		return nil, err
903	}
904
905	_, body, _, err := h.client.Do(ctx, req)
906	if err != nil {
907		return nil, err
908	}
909
910	var res []MetricMetadata
911	return res, json.Unmarshal(body, &res)
912}
913
914func (h *httpAPI) Metadata(ctx context.Context, metric string, limit string) (map[string][]Metadata, error) {
915	u := h.client.URL(epMetadata, nil)
916	q := u.Query()
917
918	q.Set("metric", metric)
919	q.Set("limit", limit)
920
921	u.RawQuery = q.Encode()
922
923	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
924	if err != nil {
925		return nil, err
926	}
927
928	_, body, _, err := h.client.Do(ctx, req)
929	if err != nil {
930		return nil, err
931	}
932
933	var res map[string][]Metadata
934	return res, json.Unmarshal(body, &res)
935}
936
937func (h *httpAPI) TSDB(ctx context.Context) (TSDBResult, error) {
938	u := h.client.URL(epTSDB, nil)
939
940	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
941	if err != nil {
942		return TSDBResult{}, err
943	}
944
945	_, body, _, err := h.client.Do(ctx, req)
946	if err != nil {
947		return TSDBResult{}, err
948	}
949
950	var res TSDBResult
951	return res, json.Unmarshal(body, &res)
952
953}
954
955// Warnings is an array of non critical errors
956type Warnings []string
957
958// apiClient wraps a regular client and processes successful API responses.
959// Successful also includes responses that errored at the API level.
960type apiClient interface {
961	URL(ep string, args map[string]string) *url.URL
962	Do(context.Context, *http.Request) (*http.Response, []byte, Warnings, error)
963	DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, error)
964}
965
966type apiClientImpl struct {
967	client api.Client
968}
969
970type apiResponse struct {
971	Status    string          `json:"status"`
972	Data      json.RawMessage `json:"data"`
973	ErrorType ErrorType       `json:"errorType"`
974	Error     string          `json:"error"`
975	Warnings  []string        `json:"warnings,omitempty"`
976}
977
978func apiError(code int) bool {
979	// These are the codes that Prometheus sends when it returns an error.
980	return code == http.StatusUnprocessableEntity || code == http.StatusBadRequest
981}
982
983func errorTypeAndMsgFor(resp *http.Response) (ErrorType, string) {
984	switch resp.StatusCode / 100 {
985	case 4:
986		return ErrClient, fmt.Sprintf("client error: %d", resp.StatusCode)
987	case 5:
988		return ErrServer, fmt.Sprintf("server error: %d", resp.StatusCode)
989	}
990	return ErrBadResponse, fmt.Sprintf("bad response code %d", resp.StatusCode)
991}
992
993func (h *apiClientImpl) URL(ep string, args map[string]string) *url.URL {
994	return h.client.URL(ep, args)
995}
996
997func (h *apiClientImpl) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, Warnings, error) {
998	resp, body, err := h.client.Do(ctx, req)
999	if err != nil {
1000		return resp, body, nil, err
1001	}
1002
1003	code := resp.StatusCode
1004
1005	if code/100 != 2 && !apiError(code) {
1006		errorType, errorMsg := errorTypeAndMsgFor(resp)
1007		return resp, body, nil, &Error{
1008			Type:   errorType,
1009			Msg:    errorMsg,
1010			Detail: string(body),
1011		}
1012	}
1013
1014	var result apiResponse
1015
1016	if http.StatusNoContent != code {
1017		if jsonErr := json.Unmarshal(body, &result); jsonErr != nil {
1018			return resp, body, nil, &Error{
1019				Type: ErrBadResponse,
1020				Msg:  jsonErr.Error(),
1021			}
1022		}
1023	}
1024
1025	if apiError(code) && result.Status == "success" {
1026		err = &Error{
1027			Type: ErrBadResponse,
1028			Msg:  "inconsistent body for response code",
1029		}
1030	}
1031
1032	if result.Status == "error" {
1033		err = &Error{
1034			Type: result.ErrorType,
1035			Msg:  result.Error,
1036		}
1037	}
1038
1039	return resp, []byte(result.Data), result.Warnings, err
1040
1041}
1042
1043// DoGetFallback will attempt to do the request as-is, and on a 405 or 501 it
1044// will fallback to a GET request.
1045func (h *apiClientImpl) DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, error) {
1046	req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(args.Encode()))
1047	if err != nil {
1048		return nil, nil, nil, err
1049	}
1050	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
1051
1052	resp, body, warnings, err := h.Do(ctx, req)
1053	if resp != nil && (resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotImplemented) {
1054		u.RawQuery = args.Encode()
1055		req, err = http.NewRequest(http.MethodGet, u.String(), nil)
1056		if err != nil {
1057			return nil, nil, warnings, err
1058		}
1059
1060	} else {
1061		if err != nil {
1062			return resp, body, warnings, err
1063		}
1064		return resp, body, warnings, nil
1065	}
1066	return h.Do(ctx, req)
1067}
1068
1069func formatTime(t time.Time) string {
1070	return strconv.FormatFloat(float64(t.Unix())+float64(t.Nanosecond())/1e9, 'f', -1, 64)
1071}
1072