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