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// +build go1.7
15
16// Package v1 provides bindings to the Prometheus HTTP API v1:
17// http://prometheus.io/docs/querying/api/
18package v1
19
20import (
21	"context"
22	"encoding/json"
23	"fmt"
24	"net/http"
25	"strconv"
26	"time"
27
28	"github.com/prometheus/client_golang/api"
29	"github.com/prometheus/common/model"
30)
31
32const (
33	statusAPIError = 422
34
35	apiPrefix = "/api/v1"
36
37	epAlertManagers   = apiPrefix + "/alertmanagers"
38	epQuery           = apiPrefix + "/query"
39	epQueryRange      = apiPrefix + "/query_range"
40	epLabelValues     = apiPrefix + "/label/:name/values"
41	epSeries          = apiPrefix + "/series"
42	epTargets         = apiPrefix + "/targets"
43	epSnapshot        = apiPrefix + "/admin/tsdb/snapshot"
44	epDeleteSeries    = apiPrefix + "/admin/tsdb/delete_series"
45	epCleanTombstones = apiPrefix + "/admin/tsdb/clean_tombstones"
46	epConfig          = apiPrefix + "/status/config"
47	epFlags           = apiPrefix + "/status/flags"
48)
49
50// ErrorType models the different API error types.
51type ErrorType string
52
53// HealthStatus models the health status of a scrape target.
54type HealthStatus string
55
56const (
57	// Possible values for ErrorType.
58	ErrBadData     ErrorType = "bad_data"
59	ErrTimeout     ErrorType = "timeout"
60	ErrCanceled    ErrorType = "canceled"
61	ErrExec        ErrorType = "execution"
62	ErrBadResponse ErrorType = "bad_response"
63	ErrServer      ErrorType = "server_error"
64	ErrClient      ErrorType = "client_error"
65
66	// Possible values for HealthStatus.
67	HealthGood    HealthStatus = "up"
68	HealthUnknown HealthStatus = "unknown"
69	HealthBad     HealthStatus = "down"
70)
71
72// Error is an error returned by the API.
73type Error struct {
74	Type   ErrorType
75	Msg    string
76	Detail string
77}
78
79func (e *Error) Error() string {
80	return fmt.Sprintf("%s: %s", e.Type, e.Msg)
81}
82
83// Range represents a sliced time range.
84type Range struct {
85	// The boundaries of the time range.
86	Start, End time.Time
87	// The maximum time between two slices within the boundaries.
88	Step time.Duration
89}
90
91// API provides bindings for Prometheus's v1 API.
92type API interface {
93	// AlertManagers returns an overview of the current state of the Prometheus alert manager discovery.
94	AlertManagers(ctx context.Context) (AlertManagersResult, error)
95	// CleanTombstones removes the deleted data from disk and cleans up the existing tombstones.
96	CleanTombstones(ctx context.Context) error
97	// Config returns the current Prometheus configuration.
98	Config(ctx context.Context) (ConfigResult, error)
99	// DeleteSeries deletes data for a selection of series in a time range.
100	DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error
101	// Flags returns the flag values that Prometheus was launched with.
102	Flags(ctx context.Context) (FlagsResult, error)
103	// LabelValues performs a query for the values of the given label.
104	LabelValues(ctx context.Context, label string) (model.LabelValues, error)
105	// Query performs a query for the given time.
106	Query(ctx context.Context, query string, ts time.Time) (model.Value, error)
107	// QueryRange performs a query for the given range.
108	QueryRange(ctx context.Context, query string, r Range) (model.Value, error)
109	// Series finds series by label matchers.
110	Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, error)
111	// Snapshot creates a snapshot of all current data into snapshots/<datetime>-<rand>
112	// under the TSDB's data directory and returns the directory as response.
113	Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error)
114	// Targets returns an overview of the current state of the Prometheus target discovery.
115	Targets(ctx context.Context) (TargetsResult, error)
116}
117
118// AlertManagersResult contains the result from querying the alertmanagers endpoint.
119type AlertManagersResult struct {
120	Active  []AlertManager `json:"activeAlertManagers"`
121	Dropped []AlertManager `json:"droppedAlertManagers"`
122}
123
124// AlertManager models a configured Alert Manager.
125type AlertManager struct {
126	URL string `json:"url"`
127}
128
129// ConfigResult contains the result from querying the config endpoint.
130type ConfigResult struct {
131	YAML string `json:"yaml"`
132}
133
134// FlagsResult contains the result from querying the flag endpoint.
135type FlagsResult map[string]string
136
137// SnapshotResult contains the result from querying the snapshot endpoint.
138type SnapshotResult struct {
139	Name string `json:"name"`
140}
141
142// TargetsResult contains the result from querying the targets endpoint.
143type TargetsResult struct {
144	Active  []ActiveTarget  `json:"activeTargets"`
145	Dropped []DroppedTarget `json:"droppedTargets"`
146}
147
148// ActiveTarget models an active Prometheus scrape target.
149type ActiveTarget struct {
150	DiscoveredLabels model.LabelSet `json:"discoveredLabels"`
151	Labels           model.LabelSet `json:"labels"`
152	ScrapeURL        string         `json:"scrapeUrl"`
153	LastError        string         `json:"lastError"`
154	LastScrape       time.Time      `json:"lastScrape"`
155	Health           HealthStatus   `json:"health"`
156}
157
158// DroppedTarget models a dropped Prometheus scrape target.
159type DroppedTarget struct {
160	DiscoveredLabels model.LabelSet `json:"discoveredLabels"`
161}
162
163// queryResult contains result data for a query.
164type queryResult struct {
165	Type   model.ValueType `json:"resultType"`
166	Result interface{}     `json:"result"`
167
168	// The decoded value.
169	v model.Value
170}
171
172func (qr *queryResult) UnmarshalJSON(b []byte) error {
173	v := struct {
174		Type   model.ValueType `json:"resultType"`
175		Result json.RawMessage `json:"result"`
176	}{}
177
178	err := json.Unmarshal(b, &v)
179	if err != nil {
180		return err
181	}
182
183	switch v.Type {
184	case model.ValScalar:
185		var sv model.Scalar
186		err = json.Unmarshal(v.Result, &sv)
187		qr.v = &sv
188
189	case model.ValVector:
190		var vv model.Vector
191		err = json.Unmarshal(v.Result, &vv)
192		qr.v = vv
193
194	case model.ValMatrix:
195		var mv model.Matrix
196		err = json.Unmarshal(v.Result, &mv)
197		qr.v = mv
198
199	default:
200		err = fmt.Errorf("unexpected value type %q", v.Type)
201	}
202	return err
203}
204
205// NewAPI returns a new API for the client.
206//
207// It is safe to use the returned API from multiple goroutines.
208func NewAPI(c api.Client) API {
209	return &httpAPI{client: apiClient{c}}
210}
211
212type httpAPI struct {
213	client api.Client
214}
215
216func (h *httpAPI) AlertManagers(ctx context.Context) (AlertManagersResult, error) {
217	u := h.client.URL(epAlertManagers, nil)
218
219	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
220	if err != nil {
221		return AlertManagersResult{}, err
222	}
223
224	_, body, err := h.client.Do(ctx, req)
225	if err != nil {
226		return AlertManagersResult{}, err
227	}
228
229	var res AlertManagersResult
230	err = json.Unmarshal(body, &res)
231	return res, err
232}
233
234func (h *httpAPI) CleanTombstones(ctx context.Context) error {
235	u := h.client.URL(epCleanTombstones, nil)
236
237	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
238	if err != nil {
239		return err
240	}
241
242	_, _, err = h.client.Do(ctx, req)
243	return err
244}
245
246func (h *httpAPI) Config(ctx context.Context) (ConfigResult, error) {
247	u := h.client.URL(epConfig, nil)
248
249	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
250	if err != nil {
251		return ConfigResult{}, err
252	}
253
254	_, body, err := h.client.Do(ctx, req)
255	if err != nil {
256		return ConfigResult{}, err
257	}
258
259	var res ConfigResult
260	err = json.Unmarshal(body, &res)
261	return res, err
262}
263
264func (h *httpAPI) DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error {
265	u := h.client.URL(epDeleteSeries, nil)
266	q := u.Query()
267
268	for _, m := range matches {
269		q.Add("match[]", m)
270	}
271
272	q.Set("start", startTime.Format(time.RFC3339Nano))
273	q.Set("end", endTime.Format(time.RFC3339Nano))
274
275	u.RawQuery = q.Encode()
276
277	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
278	if err != nil {
279		return err
280	}
281
282	_, _, err = h.client.Do(ctx, req)
283	return err
284}
285
286func (h *httpAPI) Flags(ctx context.Context) (FlagsResult, error) {
287	u := h.client.URL(epFlags, nil)
288
289	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
290	if err != nil {
291		return FlagsResult{}, err
292	}
293
294	_, body, err := h.client.Do(ctx, req)
295	if err != nil {
296		return FlagsResult{}, err
297	}
298
299	var res FlagsResult
300	err = json.Unmarshal(body, &res)
301	return res, err
302}
303
304func (h *httpAPI) LabelValues(ctx context.Context, label string) (model.LabelValues, error) {
305	u := h.client.URL(epLabelValues, map[string]string{"name": label})
306	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
307	if err != nil {
308		return nil, err
309	}
310	_, body, err := h.client.Do(ctx, req)
311	if err != nil {
312		return nil, err
313	}
314	var labelValues model.LabelValues
315	err = json.Unmarshal(body, &labelValues)
316	return labelValues, err
317}
318
319func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) {
320	u := h.client.URL(epQuery, nil)
321	q := u.Query()
322
323	q.Set("query", query)
324	if !ts.IsZero() {
325		q.Set("time", ts.Format(time.RFC3339Nano))
326	}
327
328	u.RawQuery = q.Encode()
329
330	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
331	if err != nil {
332		return nil, err
333	}
334
335	_, body, err := h.client.Do(ctx, req)
336	if err != nil {
337		return nil, err
338	}
339
340	var qres queryResult
341	err = json.Unmarshal(body, &qres)
342
343	return model.Value(qres.v), err
344}
345
346func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) {
347	u := h.client.URL(epQueryRange, nil)
348	q := u.Query()
349
350	var (
351		start = r.Start.Format(time.RFC3339Nano)
352		end   = r.End.Format(time.RFC3339Nano)
353		step  = strconv.FormatFloat(r.Step.Seconds(), 'f', 3, 64)
354	)
355
356	q.Set("query", query)
357	q.Set("start", start)
358	q.Set("end", end)
359	q.Set("step", step)
360
361	u.RawQuery = q.Encode()
362
363	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
364	if err != nil {
365		return nil, err
366	}
367
368	_, body, err := h.client.Do(ctx, req)
369	if err != nil {
370		return nil, err
371	}
372
373	var qres queryResult
374	err = json.Unmarshal(body, &qres)
375
376	return model.Value(qres.v), err
377}
378
379func (h *httpAPI) Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, error) {
380	u := h.client.URL(epSeries, nil)
381	q := u.Query()
382
383	for _, m := range matches {
384		q.Add("match[]", m)
385	}
386
387	q.Set("start", startTime.Format(time.RFC3339Nano))
388	q.Set("end", endTime.Format(time.RFC3339Nano))
389
390	u.RawQuery = q.Encode()
391
392	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
393	if err != nil {
394		return nil, err
395	}
396
397	_, body, err := h.client.Do(ctx, req)
398	if err != nil {
399		return nil, err
400	}
401
402	var mset []model.LabelSet
403	err = json.Unmarshal(body, &mset)
404	return mset, err
405}
406
407func (h *httpAPI) Snapshot(ctx context.Context, skipHead bool) (SnapshotResult, error) {
408	u := h.client.URL(epSnapshot, nil)
409	q := u.Query()
410
411	q.Set("skip_head", strconv.FormatBool(skipHead))
412
413	u.RawQuery = q.Encode()
414
415	req, err := http.NewRequest(http.MethodPost, u.String(), nil)
416	if err != nil {
417		return SnapshotResult{}, err
418	}
419
420	_, body, err := h.client.Do(ctx, req)
421	if err != nil {
422		return SnapshotResult{}, err
423	}
424
425	var res SnapshotResult
426	err = json.Unmarshal(body, &res)
427	return res, err
428}
429
430func (h *httpAPI) Targets(ctx context.Context) (TargetsResult, error) {
431	u := h.client.URL(epTargets, nil)
432
433	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
434	if err != nil {
435		return TargetsResult{}, err
436	}
437
438	_, body, err := h.client.Do(ctx, req)
439	if err != nil {
440		return TargetsResult{}, err
441	}
442
443	var res TargetsResult
444	err = json.Unmarshal(body, &res)
445	return res, err
446}
447
448// apiClient wraps a regular client and processes successful API responses.
449// Successful also includes responses that errored at the API level.
450type apiClient struct {
451	api.Client
452}
453
454type apiResponse struct {
455	Status    string          `json:"status"`
456	Data      json.RawMessage `json:"data"`
457	ErrorType ErrorType       `json:"errorType"`
458	Error     string          `json:"error"`
459}
460
461func apiError(code int) bool {
462	// These are the codes that Prometheus sends when it returns an error.
463	return code == statusAPIError || code == http.StatusBadRequest
464}
465
466func errorTypeAndMsgFor(resp *http.Response) (ErrorType, string) {
467	switch resp.StatusCode / 100 {
468	case 4:
469		return ErrClient, fmt.Sprintf("client error: %d", resp.StatusCode)
470	case 5:
471		return ErrServer, fmt.Sprintf("server error: %d", resp.StatusCode)
472	}
473	return ErrBadResponse, fmt.Sprintf("bad response code %d", resp.StatusCode)
474}
475
476func (c apiClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
477	resp, body, err := c.Client.Do(ctx, req)
478	if err != nil {
479		return resp, body, err
480	}
481
482	code := resp.StatusCode
483
484	if code/100 != 2 && !apiError(code) {
485		errorType, errorMsg := errorTypeAndMsgFor(resp)
486		return resp, body, &Error{
487			Type:   errorType,
488			Msg:    errorMsg,
489			Detail: string(body),
490		}
491	}
492
493	var result apiResponse
494
495	if http.StatusNoContent != code {
496		if err = json.Unmarshal(body, &result); err != nil {
497			return resp, body, &Error{
498				Type: ErrBadResponse,
499				Msg:  err.Error(),
500			}
501		}
502	}
503
504	if apiError(code) != (result.Status == "error") {
505		err = &Error{
506			Type: ErrBadResponse,
507			Msg:  "inconsistent body for response code",
508		}
509	}
510
511	if apiError(code) && result.Status == "error" {
512		err = &Error{
513			Type: result.ErrorType,
514			Msg:  result.Error,
515		}
516	}
517
518	return resp, []byte(result.Data), err
519}
520