// Copyright 2016 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package v1 import ( "encoding/json" "errors" "fmt" "math" "net/http" "strconv" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" "github.com/prometheus/common/route" "golang.org/x/net/context" "github.com/prometheus/prometheus/pkg/labels" "github.com/prometheus/prometheus/pkg/timestamp" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/retrieval" "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/util/httputil" ) type status string const ( statusSuccess status = "success" statusError = "error" ) type errorType string const ( errorNone errorType = "" errorTimeout = "timeout" errorCanceled = "canceled" errorExec = "execution" errorBadData = "bad_data" ) var corsHeaders = map[string]string{ "Access-Control-Allow-Headers": "Accept, Authorization, Content-Type, Origin", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Origin": "*", "Access-Control-Expose-Headers": "Date", } type apiError struct { typ errorType err error } func (e *apiError) Error() string { return fmt.Sprintf("%s: %s", e.typ, e.err) } type targetRetriever interface { Targets() []*retrieval.Target } type alertmanagerRetriever interface { Alertmanagers() []string } type response struct { Status status `json:"status"` Data interface{} `json:"data,omitempty"` ErrorType errorType `json:"errorType,omitempty"` Error string `json:"error,omitempty"` } // Enables cross-site script calls. func setCORS(w http.ResponseWriter) { for h, v := range corsHeaders { w.Header().Set(h, v) } } type apiFunc func(r *http.Request) (interface{}, *apiError) // API can register a set of endpoints in a router and handle // them using the provided storage and query engine. type API struct { Storage storage.Storage QueryEngine *promql.Engine targetRetriever targetRetriever alertmanagerRetriever alertmanagerRetriever context func(r *http.Request) context.Context now func() time.Time } // NewAPI returns an initialized API type. func NewAPI(qe *promql.Engine, st storage.Storage, tr targetRetriever, ar alertmanagerRetriever) *API { return &API{ QueryEngine: qe, Storage: st, targetRetriever: tr, alertmanagerRetriever: ar, context: route.Context, now: time.Now, } } // Register the API's endpoints in the given router. func (api *API) Register(r *route.Router) { instr := func(name string, f apiFunc) http.HandlerFunc { hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { setCORS(w) if data, err := f(r); err != nil { respondError(w, err, data) } else if data != nil { respond(w, data) } else { w.WriteHeader(http.StatusNoContent) } }) return prometheus.InstrumentHandler(name, httputil.CompressionHandler{ Handler: hf, }) } r.Options("/*path", instr("options", api.options)) r.Get("/query", instr("query", api.query)) r.Get("/query_range", instr("query_range", api.queryRange)) r.Get("/label/:name/values", instr("label_values", api.labelValues)) r.Get("/series", instr("series", api.series)) r.Del("/series", instr("drop_series", api.dropSeries)) r.Get("/targets", instr("targets", api.targets)) r.Get("/alertmanagers", instr("alertmanagers", api.alertmanagers)) } type queryData struct { ResultType promql.ValueType `json:"resultType"` Result promql.Value `json:"result"` } func (api *API) options(r *http.Request) (interface{}, *apiError) { return nil, nil } func (api *API) query(r *http.Request) (interface{}, *apiError) { var ts time.Time if t := r.FormValue("time"); t != "" { var err error ts, err = parseTime(t) if err != nil { return nil, &apiError{errorBadData, err} } } else { ts = api.now() } ctx := api.context(r) if to := r.FormValue("timeout"); to != "" { var cancel context.CancelFunc timeout, err := parseDuration(to) if err != nil { return nil, &apiError{errorBadData, err} } ctx, cancel = context.WithTimeout(ctx, timeout) defer cancel() } qry, err := api.QueryEngine.NewInstantQuery(r.FormValue("query"), ts) if err != nil { return nil, &apiError{errorBadData, err} } res := qry.Exec(ctx) if res.Err != nil { switch res.Err.(type) { case promql.ErrQueryCanceled: return nil, &apiError{errorCanceled, res.Err} case promql.ErrQueryTimeout: return nil, &apiError{errorTimeout, res.Err} } return nil, &apiError{errorExec, res.Err} } return &queryData{ ResultType: res.Value.Type(), Result: res.Value, }, nil } func (api *API) queryRange(r *http.Request) (interface{}, *apiError) { start, err := parseTime(r.FormValue("start")) if err != nil { return nil, &apiError{errorBadData, err} } end, err := parseTime(r.FormValue("end")) if err != nil { return nil, &apiError{errorBadData, err} } if end.Before(start) { err := errors.New("end timestamp must not be before start time") return nil, &apiError{errorBadData, err} } step, err := parseDuration(r.FormValue("step")) if err != nil { return nil, &apiError{errorBadData, err} } if step <= 0 { err := errors.New("zero or negative query resolution step widths are not accepted. Try a positive integer") return nil, &apiError{errorBadData, err} } // For safety, limit the number of returned points per timeseries. // This is sufficient for 60s resolution for a week or 1h resolution for a year. if end.Sub(start)/step > 11000 { err := errors.New("exceeded maximum resolution of 11,000 points per timeseries. Try decreasing the query resolution (?step=XX)") return nil, &apiError{errorBadData, err} } ctx := api.context(r) if to := r.FormValue("timeout"); to != "" { var cancel context.CancelFunc timeout, err := parseDuration(to) if err != nil { return nil, &apiError{errorBadData, err} } ctx, cancel = context.WithTimeout(ctx, timeout) defer cancel() } qry, err := api.QueryEngine.NewRangeQuery(r.FormValue("query"), start, end, step) if err != nil { return nil, &apiError{errorBadData, err} } res := qry.Exec(ctx) if res.Err != nil { switch res.Err.(type) { case promql.ErrQueryCanceled: return nil, &apiError{errorCanceled, res.Err} case promql.ErrQueryTimeout: return nil, &apiError{errorTimeout, res.Err} } return nil, &apiError{errorExec, res.Err} } return &queryData{ ResultType: res.Value.Type(), Result: res.Value, }, nil } func (api *API) labelValues(r *http.Request) (interface{}, *apiError) { name := route.Param(api.context(r), "name") if !model.LabelNameRE.MatchString(name) { return nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)} } q, err := api.Storage.Querier(math.MinInt64, math.MaxInt64) if err != nil { return nil, &apiError{errorExec, err} } defer q.Close() // TODO(fabxc): add back request context. vals, err := q.LabelValues(name) if err != nil { return nil, &apiError{errorExec, err} } return vals, nil } var ( minTime = time.Unix(math.MinInt64/1000+62135596801, 0) maxTime = time.Unix(math.MaxInt64/1000-62135596801, 999999999) ) func (api *API) series(r *http.Request) (interface{}, *apiError) { r.ParseForm() if len(r.Form["match[]"]) == 0 { return nil, &apiError{errorBadData, fmt.Errorf("no match[] parameter provided")} } var start time.Time if t := r.FormValue("start"); t != "" { var err error start, err = parseTime(t) if err != nil { return nil, &apiError{errorBadData, err} } } else { start = minTime } var end time.Time if t := r.FormValue("end"); t != "" { var err error end, err = parseTime(t) if err != nil { return nil, &apiError{errorBadData, err} } } else { end = maxTime } var matcherSets [][]*labels.Matcher for _, s := range r.Form["match[]"] { matchers, err := promql.ParseMetricSelector(s) if err != nil { return nil, &apiError{errorBadData, err} } matcherSets = append(matcherSets, matchers) } q, err := api.Storage.Querier(timestamp.FromTime(start), timestamp.FromTime(end)) if err != nil { return nil, &apiError{errorExec, err} } defer q.Close() var set storage.SeriesSet for _, mset := range matcherSets { if set == nil { set = q.Select(mset...) } else { set = storage.DeduplicateSeriesSet(set, q.Select(mset...)) } } metrics := []labels.Labels{} for set.Next() { metrics = append(metrics, set.At().Labels()) } if set.Err() != nil { return nil, &apiError{errorExec, set.Err()} } return metrics, nil } func (api *API) dropSeries(r *http.Request) (interface{}, *apiError) { r.ParseForm() if len(r.Form["match[]"]) == 0 { return nil, &apiError{errorBadData, fmt.Errorf("no match[] parameter provided")} } // TODO(fabxc): temporarily disabled panic("disabled") // numDeleted := 0 // for _, s := range r.Form["match[]"] { // matchers, err := promql.ParseMetricSelector(s) // if err != nil { // return nil, &apiError{errorBadData, err} // } // n, err := api.Storage.DropMetricsForLabelMatchers(context.TODO(), matchers...) // if err != nil { // return nil, &apiError{errorExec, err} // } // numDeleted += n // } // res := struct { // NumDeleted int `json:"numDeleted"` // }{ // NumDeleted: numDeleted, // } // return res, nil } // Target has the information for one target. type Target struct { // Labels before any processing. DiscoveredLabels map[string]string `json:"discoveredLabels"` // Any labels that are added to this target and its metrics. Labels map[string]string `json:"labels"` ScrapeURL string `json:"scrapeUrl"` LastError string `json:"lastError"` LastScrape time.Time `json:"lastScrape"` Health retrieval.TargetHealth `json:"health"` } // TargetDiscovery has all the active targets. type TargetDiscovery struct { ActiveTargets []*Target `json:"activeTargets"` } func (api *API) targets(r *http.Request) (interface{}, *apiError) { targets := api.targetRetriever.Targets() res := &TargetDiscovery{ActiveTargets: make([]*Target, len(targets))} for i, t := range targets { lastErrStr := "" lastErr := t.LastError() if lastErr != nil { lastErrStr = lastErr.Error() } res.ActiveTargets[i] = &Target{ DiscoveredLabels: t.DiscoveredLabels().Map(), Labels: t.Labels().Map(), ScrapeURL: t.URL().String(), LastError: lastErrStr, LastScrape: t.LastScrape(), Health: t.Health(), } } return res, nil } // AlertmanagerDiscovery has all the active Alertmanagers. type AlertmanagerDiscovery struct { ActiveAlertmanagers []*AlertmanagerTarget `json:"activeAlertmanagers"` } // AlertmanagerTarget has info on one AM. type AlertmanagerTarget struct { URL string `json:"url"` } func (api *API) alertmanagers(r *http.Request) (interface{}, *apiError) { urls := api.alertmanagerRetriever.Alertmanagers() ams := &AlertmanagerDiscovery{ActiveAlertmanagers: make([]*AlertmanagerTarget, len(urls))} for i := range urls { ams.ActiveAlertmanagers[i] = &AlertmanagerTarget{URL: urls[i]} } return ams, nil } func respond(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) b, err := json.Marshal(&response{ Status: statusSuccess, Data: data, }) if err != nil { return } w.Write(b) } func respondError(w http.ResponseWriter, apiErr *apiError, data interface{}) { w.Header().Set("Content-Type", "application/json") var code int switch apiErr.typ { case errorBadData: code = http.StatusBadRequest case errorExec: code = 422 case errorCanceled, errorTimeout: code = http.StatusServiceUnavailable default: code = http.StatusInternalServerError } w.WriteHeader(code) b, err := json.Marshal(&response{ Status: statusError, ErrorType: apiErr.typ, Error: apiErr.err.Error(), Data: data, }) if err != nil { return } w.Write(b) } func parseTime(s string) (time.Time, error) { if t, err := strconv.ParseFloat(s, 64); err == nil { s, ns := math.Modf(t) return time.Unix(int64(s), int64(ns*float64(time.Second))), nil } if t, err := time.Parse(time.RFC3339Nano, s); err == nil { return t, nil } return time.Time{}, fmt.Errorf("cannot parse %q to a valid timestamp", s) } func parseDuration(s string) (time.Duration, error) { if d, err := strconv.ParseFloat(s, 64); err == nil { ts := d * float64(time.Second) if ts > float64(math.MaxInt64) || ts < float64(math.MinInt64) { return 0, fmt.Errorf("cannot parse %q to a valid duration. It overflows int64", s) } return time.Duration(ts), nil } if d, err := model.ParseDuration(s); err == nil { return time.Duration(d), nil } return 0, fmt.Errorf("cannot parse %q to a valid duration", s) }