mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-30 07:59:40 -08:00
d2802c6fac
* Add paginated feature to list rules api Signed-off-by: Yijie Qin <qinyijie@amazon.com> * Refactor to simplify code: * Reduce number of variables * Reduce type convesion Signed-off-by: Raphael Silva <rapphil@gmail.com> * Simplify paginated implementation * Remove maxAlerts parameter. * Reuse existing API responses by using omitempty in some fields Signed-off-by: Raphael Silva <rapphil@gmail.com> * Simplify pagination implementation * Eliminate the need to sort the rule groups. Signed-off-by: Raphael Silva <rapphil@gmail.com> * Fix linting error Signed-off-by: Raphael Silva <rapphil@gmail.com> * Add more unit tests Signed-off-by: Raphael Silva <rapphil@gmail.com> * Update pagination parameters to be consistent with existing parameters Signed-off-by: Raphael Silva <rapphil@gmail.com> * Rename max_rule_groups to max_groups Signed-off-by: Raphael Silva <rapphil@gmail.com> * Refactor to simplify code Signed-off-by: Raphael Silva <rapphil@gmail.com> * Refactor to simplify the calculation of next token Signed-off-by: Raphael Silva <rapphil@gmail.com> * Handle corner case in pagination request Signed-off-by: Raphael Silva <rapphil@gmail.com> * Handle corner cases for pagination of list rules Signed-off-by: Raphael Silva <rapphil@gmail.com> * Update documentation for list rules parameters Signed-off-by: Raphael Silva <rapphil@gmail.com> * Refactor comments Signed-off-by: Raphael Silva <rapphil@gmail.com> * Simplify pagination implementation * Eliminate need for extra structs to store pagination parameters Signed-off-by: Raphael Silva <rapphil@gmail.com> * Update docs/querying/api.md Co-authored-by: Julius Volz <julius.volz@gmail.com> Signed-off-by: Raphael Philipe Mendes da Silva <rapphil@gmail.com> * Update web/api/v1/api.go Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com> Signed-off-by: Raphael Philipe Mendes da Silva <rapphil@gmail.com> * Update comment describing the need for next token Signed-off-by: Raphael Silva <rapphil@gmail.com> --------- Signed-off-by: Yijie Qin <qinyijie@amazon.com> Signed-off-by: Raphael Silva <rapphil@gmail.com> Signed-off-by: Raphael Philipe Mendes da Silva <rapphil@gmail.com> Co-authored-by: Raphael Silva <rapphil@gmail.com> Co-authored-by: Julius Volz <julius.volz@gmail.com> Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
2097 lines
62 KiB
Go
2097 lines
62 KiB
Go
// 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 (
|
|
"context"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"math"
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grafana/regexp"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/munnerz/goautoneg"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/common/model"
|
|
"github.com/prometheus/common/route"
|
|
|
|
"github.com/prometheus/prometheus/config"
|
|
"github.com/prometheus/prometheus/model/labels"
|
|
"github.com/prometheus/prometheus/model/metadata"
|
|
"github.com/prometheus/prometheus/model/timestamp"
|
|
"github.com/prometheus/prometheus/promql"
|
|
"github.com/prometheus/prometheus/promql/parser"
|
|
"github.com/prometheus/prometheus/rules"
|
|
"github.com/prometheus/prometheus/scrape"
|
|
"github.com/prometheus/prometheus/storage"
|
|
"github.com/prometheus/prometheus/storage/remote"
|
|
"github.com/prometheus/prometheus/tsdb"
|
|
"github.com/prometheus/prometheus/tsdb/index"
|
|
"github.com/prometheus/prometheus/util/annotations"
|
|
"github.com/prometheus/prometheus/util/httputil"
|
|
"github.com/prometheus/prometheus/util/notifications"
|
|
"github.com/prometheus/prometheus/util/stats"
|
|
)
|
|
|
|
type status string
|
|
|
|
const (
|
|
statusSuccess status = "success"
|
|
statusError status = "error"
|
|
|
|
// Non-standard status code (originally introduced by nginx) for the case when a client closes
|
|
// the connection while the server is still processing the request.
|
|
statusClientClosedConnection = 499
|
|
)
|
|
|
|
type errorType string
|
|
|
|
const (
|
|
errorNone errorType = ""
|
|
errorTimeout errorType = "timeout"
|
|
errorCanceled errorType = "canceled"
|
|
errorExec errorType = "execution"
|
|
errorBadData errorType = "bad_data"
|
|
errorInternal errorType = "internal"
|
|
errorUnavailable errorType = "unavailable"
|
|
errorNotFound errorType = "not_found"
|
|
errorNotAcceptable errorType = "not_acceptable"
|
|
)
|
|
|
|
var LocalhostRepresentations = []string{"127.0.0.1", "localhost", "::1"}
|
|
|
|
type apiError struct {
|
|
typ errorType
|
|
err error
|
|
}
|
|
|
|
func (e *apiError) Error() string {
|
|
return fmt.Sprintf("%s: %s", e.typ, e.err)
|
|
}
|
|
|
|
// ScrapePoolsRetriever provide the list of all scrape pools.
|
|
type ScrapePoolsRetriever interface {
|
|
ScrapePools() []string
|
|
}
|
|
|
|
// TargetRetriever provides the list of active/dropped targets to scrape or not.
|
|
type TargetRetriever interface {
|
|
TargetsActive() map[string][]*scrape.Target
|
|
TargetsDropped() map[string][]*scrape.Target
|
|
TargetsDroppedCounts() map[string]int
|
|
}
|
|
|
|
// AlertmanagerRetriever provides a list of all/dropped AlertManager URLs.
|
|
type AlertmanagerRetriever interface {
|
|
Alertmanagers() []*url.URL
|
|
DroppedAlertmanagers() []*url.URL
|
|
}
|
|
|
|
// RulesRetriever provides a list of active rules and alerts.
|
|
type RulesRetriever interface {
|
|
RuleGroups() []*rules.Group
|
|
AlertingRules() []*rules.AlertingRule
|
|
}
|
|
|
|
// StatsRenderer converts engine statistics into a format suitable for the API.
|
|
type StatsRenderer func(context.Context, *stats.Statistics, string) stats.QueryStats
|
|
|
|
// DefaultStatsRenderer is the default stats renderer for the API.
|
|
func DefaultStatsRenderer(_ context.Context, s *stats.Statistics, param string) stats.QueryStats {
|
|
if param != "" {
|
|
return stats.NewQueryStats(s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PrometheusVersion contains build information about Prometheus.
|
|
type PrometheusVersion struct {
|
|
Version string `json:"version"`
|
|
Revision string `json:"revision"`
|
|
Branch string `json:"branch"`
|
|
BuildUser string `json:"buildUser"`
|
|
BuildDate string `json:"buildDate"`
|
|
GoVersion string `json:"goVersion"`
|
|
}
|
|
|
|
// RuntimeInfo contains runtime information about Prometheus.
|
|
type RuntimeInfo struct {
|
|
StartTime time.Time `json:"startTime"`
|
|
CWD string `json:"CWD"`
|
|
ReloadConfigSuccess bool `json:"reloadConfigSuccess"`
|
|
LastConfigTime time.Time `json:"lastConfigTime"`
|
|
CorruptionCount int64 `json:"corruptionCount"`
|
|
GoroutineCount int `json:"goroutineCount"`
|
|
GOMAXPROCS int `json:"GOMAXPROCS"`
|
|
GOMEMLIMIT int64 `json:"GOMEMLIMIT"`
|
|
GOGC string `json:"GOGC"`
|
|
GODEBUG string `json:"GODEBUG"`
|
|
StorageRetention string `json:"storageRetention"`
|
|
}
|
|
|
|
// Response contains a response to a HTTP API request.
|
|
type Response struct {
|
|
Status status `json:"status"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
ErrorType errorType `json:"errorType,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Warnings []string `json:"warnings,omitempty"`
|
|
Infos []string `json:"infos,omitempty"`
|
|
}
|
|
|
|
type apiFuncResult struct {
|
|
data interface{}
|
|
err *apiError
|
|
warnings annotations.Annotations
|
|
finalizer func()
|
|
}
|
|
|
|
type apiFunc func(r *http.Request) apiFuncResult
|
|
|
|
// TSDBAdminStats defines the tsdb interfaces used by the v1 API for admin operations as well as statistics.
|
|
type TSDBAdminStats interface {
|
|
CleanTombstones() error
|
|
Delete(ctx context.Context, mint, maxt int64, ms ...*labels.Matcher) error
|
|
Snapshot(dir string, withHead bool) error
|
|
Stats(statsByLabelName string, limit int) (*tsdb.Stats, error)
|
|
WALReplayStatus() (tsdb.WALReplayStatus, error)
|
|
}
|
|
|
|
type QueryOpts interface {
|
|
EnablePerStepStats() bool
|
|
LookbackDelta() time.Duration
|
|
}
|
|
|
|
// API can register a set of endpoints in a router and handle
|
|
// them using the provided storage and query engine.
|
|
type API struct {
|
|
Queryable storage.SampleAndChunkQueryable
|
|
QueryEngine promql.QueryEngine
|
|
ExemplarQueryable storage.ExemplarQueryable
|
|
|
|
scrapePoolsRetriever func(context.Context) ScrapePoolsRetriever
|
|
targetRetriever func(context.Context) TargetRetriever
|
|
alertmanagerRetriever func(context.Context) AlertmanagerRetriever
|
|
rulesRetriever func(context.Context) RulesRetriever
|
|
now func() time.Time
|
|
config func() config.Config
|
|
flagsMap map[string]string
|
|
ready func(http.HandlerFunc) http.HandlerFunc
|
|
globalURLOptions GlobalURLOptions
|
|
|
|
db TSDBAdminStats
|
|
dbDir string
|
|
enableAdmin bool
|
|
logger *slog.Logger
|
|
CORSOrigin *regexp.Regexp
|
|
buildInfo *PrometheusVersion
|
|
runtimeInfo func() (RuntimeInfo, error)
|
|
gatherer prometheus.Gatherer
|
|
isAgent bool
|
|
statsRenderer StatsRenderer
|
|
notificationsGetter func() []notifications.Notification
|
|
notificationsSub func() (<-chan notifications.Notification, func(), bool)
|
|
|
|
remoteWriteHandler http.Handler
|
|
remoteReadHandler http.Handler
|
|
otlpWriteHandler http.Handler
|
|
|
|
codecs []Codec
|
|
}
|
|
|
|
// NewAPI returns an initialized API type.
|
|
func NewAPI(
|
|
qe promql.QueryEngine,
|
|
q storage.SampleAndChunkQueryable,
|
|
ap storage.Appendable,
|
|
eq storage.ExemplarQueryable,
|
|
spsr func(context.Context) ScrapePoolsRetriever,
|
|
tr func(context.Context) TargetRetriever,
|
|
ar func(context.Context) AlertmanagerRetriever,
|
|
configFunc func() config.Config,
|
|
flagsMap map[string]string,
|
|
globalURLOptions GlobalURLOptions,
|
|
readyFunc func(http.HandlerFunc) http.HandlerFunc,
|
|
db TSDBAdminStats,
|
|
dbDir string,
|
|
enableAdmin bool,
|
|
logger *slog.Logger,
|
|
rr func(context.Context) RulesRetriever,
|
|
remoteReadSampleLimit int,
|
|
remoteReadConcurrencyLimit int,
|
|
remoteReadMaxBytesInFrame int,
|
|
isAgent bool,
|
|
corsOrigin *regexp.Regexp,
|
|
runtimeInfo func() (RuntimeInfo, error),
|
|
buildInfo *PrometheusVersion,
|
|
notificationsGetter func() []notifications.Notification,
|
|
notificationsSub func() (<-chan notifications.Notification, func(), bool),
|
|
gatherer prometheus.Gatherer,
|
|
registerer prometheus.Registerer,
|
|
statsRenderer StatsRenderer,
|
|
rwEnabled bool,
|
|
acceptRemoteWriteProtoMsgs []config.RemoteWriteProtoMsg,
|
|
otlpEnabled bool,
|
|
) *API {
|
|
a := &API{
|
|
QueryEngine: qe,
|
|
Queryable: q,
|
|
ExemplarQueryable: eq,
|
|
|
|
scrapePoolsRetriever: spsr,
|
|
targetRetriever: tr,
|
|
alertmanagerRetriever: ar,
|
|
|
|
now: time.Now,
|
|
config: configFunc,
|
|
flagsMap: flagsMap,
|
|
ready: readyFunc,
|
|
globalURLOptions: globalURLOptions,
|
|
db: db,
|
|
dbDir: dbDir,
|
|
enableAdmin: enableAdmin,
|
|
rulesRetriever: rr,
|
|
logger: logger,
|
|
CORSOrigin: corsOrigin,
|
|
runtimeInfo: runtimeInfo,
|
|
buildInfo: buildInfo,
|
|
gatherer: gatherer,
|
|
isAgent: isAgent,
|
|
statsRenderer: DefaultStatsRenderer,
|
|
notificationsGetter: notificationsGetter,
|
|
notificationsSub: notificationsSub,
|
|
|
|
remoteReadHandler: remote.NewReadHandler(logger, registerer, q, configFunc, remoteReadSampleLimit, remoteReadConcurrencyLimit, remoteReadMaxBytesInFrame),
|
|
}
|
|
|
|
a.InstallCodec(JSONCodec{})
|
|
|
|
if statsRenderer != nil {
|
|
a.statsRenderer = statsRenderer
|
|
}
|
|
|
|
if ap == nil && (rwEnabled || otlpEnabled) {
|
|
panic("remote write or otlp write enabled, but no appender passed in.")
|
|
}
|
|
|
|
if rwEnabled {
|
|
a.remoteWriteHandler = remote.NewWriteHandler(logger, registerer, ap, acceptRemoteWriteProtoMsgs)
|
|
}
|
|
if otlpEnabled {
|
|
a.otlpWriteHandler = remote.NewOTLPWriteHandler(logger, ap, configFunc)
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
// InstallCodec adds codec to this API's available codecs.
|
|
// Codecs installed first take precedence over codecs installed later when evaluating wildcards in Accept headers.
|
|
// The first installed codec is used as a fallback when the Accept header cannot be satisfied or if there is no Accept header.
|
|
func (api *API) InstallCodec(codec Codec) {
|
|
api.codecs = append(api.codecs, codec)
|
|
}
|
|
|
|
// ClearCodecs removes all available codecs from this API, including the default codec installed by NewAPI.
|
|
func (api *API) ClearCodecs() {
|
|
api.codecs = nil
|
|
}
|
|
|
|
func setUnavailStatusOnTSDBNotReady(r apiFuncResult) apiFuncResult {
|
|
if r.err != nil && errors.Is(r.err.err, tsdb.ErrNotReady) {
|
|
r.err.typ = errorUnavailable
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Register the API's endpoints in the given router.
|
|
func (api *API) Register(r *route.Router) {
|
|
wrap := func(f apiFunc) http.HandlerFunc {
|
|
hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
httputil.SetCORS(w, api.CORSOrigin, r)
|
|
result := setUnavailStatusOnTSDBNotReady(f(r))
|
|
if result.finalizer != nil {
|
|
defer result.finalizer()
|
|
}
|
|
if result.err != nil {
|
|
api.respondError(w, result.err, result.data)
|
|
return
|
|
}
|
|
|
|
if result.data != nil {
|
|
api.respond(w, r, result.data, result.warnings, r.FormValue("query"))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
return api.ready(httputil.CompressionHandler{
|
|
Handler: hf,
|
|
}.ServeHTTP)
|
|
}
|
|
|
|
wrapAgent := func(f apiFunc) http.HandlerFunc {
|
|
return wrap(func(r *http.Request) apiFuncResult {
|
|
if api.isAgent {
|
|
return apiFuncResult{nil, &apiError{errorExec, errors.New("unavailable with Prometheus Agent")}, nil, nil}
|
|
}
|
|
return f(r)
|
|
})
|
|
}
|
|
|
|
r.Options("/*path", wrap(api.options))
|
|
|
|
r.Get("/query", wrapAgent(api.query))
|
|
r.Post("/query", wrapAgent(api.query))
|
|
r.Get("/query_range", wrapAgent(api.queryRange))
|
|
r.Post("/query_range", wrapAgent(api.queryRange))
|
|
r.Get("/query_exemplars", wrapAgent(api.queryExemplars))
|
|
r.Post("/query_exemplars", wrapAgent(api.queryExemplars))
|
|
|
|
r.Get("/format_query", wrapAgent(api.formatQuery))
|
|
r.Post("/format_query", wrapAgent(api.formatQuery))
|
|
|
|
r.Get("/parse_query", wrapAgent(api.parseQuery))
|
|
r.Post("/parse_query", wrapAgent(api.parseQuery))
|
|
|
|
r.Get("/labels", wrapAgent(api.labelNames))
|
|
r.Post("/labels", wrapAgent(api.labelNames))
|
|
r.Get("/label/:name/values", wrapAgent(api.labelValues))
|
|
|
|
r.Get("/series", wrapAgent(api.series))
|
|
r.Post("/series", wrapAgent(api.series))
|
|
r.Del("/series", wrapAgent(api.dropSeries))
|
|
|
|
r.Get("/scrape_pools", wrap(api.scrapePools))
|
|
r.Get("/targets", wrap(api.targets))
|
|
r.Get("/targets/metadata", wrap(api.targetMetadata))
|
|
r.Get("/alertmanagers", wrapAgent(api.alertmanagers))
|
|
|
|
r.Get("/metadata", wrap(api.metricMetadata))
|
|
|
|
r.Get("/status/config", wrap(api.serveConfig))
|
|
r.Get("/status/runtimeinfo", wrap(api.serveRuntimeInfo))
|
|
r.Get("/status/buildinfo", wrap(api.serveBuildInfo))
|
|
r.Get("/status/flags", wrap(api.serveFlags))
|
|
r.Get("/status/tsdb", wrapAgent(api.serveTSDBStatus))
|
|
r.Get("/status/walreplay", api.serveWALReplayStatus)
|
|
r.Get("/notifications", api.notifications)
|
|
r.Get("/notifications/live", api.notificationsSSE)
|
|
r.Post("/read", api.ready(api.remoteRead))
|
|
r.Post("/write", api.ready(api.remoteWrite))
|
|
r.Post("/otlp/v1/metrics", api.ready(api.otlpWrite))
|
|
|
|
r.Get("/alerts", wrapAgent(api.alerts))
|
|
r.Get("/rules", wrapAgent(api.rules))
|
|
|
|
// Admin APIs
|
|
r.Post("/admin/tsdb/delete_series", wrapAgent(api.deleteSeries))
|
|
r.Post("/admin/tsdb/clean_tombstones", wrapAgent(api.cleanTombstones))
|
|
r.Post("/admin/tsdb/snapshot", wrapAgent(api.snapshot))
|
|
|
|
r.Put("/admin/tsdb/delete_series", wrapAgent(api.deleteSeries))
|
|
r.Put("/admin/tsdb/clean_tombstones", wrapAgent(api.cleanTombstones))
|
|
r.Put("/admin/tsdb/snapshot", wrapAgent(api.snapshot))
|
|
}
|
|
|
|
type QueryData struct {
|
|
ResultType parser.ValueType `json:"resultType"`
|
|
Result parser.Value `json:"result"`
|
|
Stats stats.QueryStats `json:"stats,omitempty"`
|
|
}
|
|
|
|
func invalidParamError(err error, parameter string) apiFuncResult {
|
|
return apiFuncResult{nil, &apiError{
|
|
errorBadData, fmt.Errorf("invalid parameter %q: %w", parameter, err),
|
|
}, nil, nil}
|
|
}
|
|
|
|
func (api *API) options(*http.Request) apiFuncResult {
|
|
return apiFuncResult{nil, nil, nil, nil}
|
|
}
|
|
|
|
func (api *API) query(r *http.Request) (result apiFuncResult) {
|
|
ts, err := parseTimeParam(r, "time", api.now())
|
|
if err != nil {
|
|
return invalidParamError(err, "time")
|
|
}
|
|
ctx := r.Context()
|
|
if to := r.FormValue("timeout"); to != "" {
|
|
var cancel context.CancelFunc
|
|
timeout, err := parseDuration(to)
|
|
if err != nil {
|
|
return invalidParamError(err, "timeout")
|
|
}
|
|
|
|
ctx, cancel = context.WithDeadline(ctx, api.now().Add(timeout))
|
|
defer cancel()
|
|
}
|
|
|
|
opts, err := extractQueryOpts(r)
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
|
|
}
|
|
qry, err := api.QueryEngine.NewInstantQuery(ctx, api.Queryable, opts, r.FormValue("query"), ts)
|
|
if err != nil {
|
|
return invalidParamError(err, "query")
|
|
}
|
|
|
|
// From now on, we must only return with a finalizer in the result (to
|
|
// be called by the caller) or call qry.Close ourselves (which is
|
|
// required in the case of a panic).
|
|
defer func() {
|
|
if result.finalizer == nil {
|
|
qry.Close()
|
|
}
|
|
}()
|
|
|
|
ctx = httputil.ContextFromRequest(ctx, r)
|
|
|
|
res := qry.Exec(ctx)
|
|
if res.Err != nil {
|
|
return apiFuncResult{nil, returnAPIError(res.Err), res.Warnings, qry.Close}
|
|
}
|
|
|
|
// Optional stats field in response if parameter "stats" is not empty.
|
|
sr := api.statsRenderer
|
|
if sr == nil {
|
|
sr = DefaultStatsRenderer
|
|
}
|
|
qs := sr(ctx, qry.Stats(), r.FormValue("stats"))
|
|
|
|
return apiFuncResult{&QueryData{
|
|
ResultType: res.Value.Type(),
|
|
Result: res.Value,
|
|
Stats: qs,
|
|
}, nil, res.Warnings, qry.Close}
|
|
}
|
|
|
|
func (api *API) formatQuery(r *http.Request) (result apiFuncResult) {
|
|
expr, err := parser.ParseExpr(r.FormValue("query"))
|
|
if err != nil {
|
|
return invalidParamError(err, "query")
|
|
}
|
|
|
|
return apiFuncResult{expr.Pretty(0), nil, nil, nil}
|
|
}
|
|
|
|
func (api *API) parseQuery(r *http.Request) apiFuncResult {
|
|
expr, err := parser.ParseExpr(r.FormValue("query"))
|
|
if err != nil {
|
|
return invalidParamError(err, "query")
|
|
}
|
|
|
|
return apiFuncResult{data: translateAST(expr), err: nil, warnings: nil, finalizer: nil}
|
|
}
|
|
|
|
func extractQueryOpts(r *http.Request) (promql.QueryOpts, error) {
|
|
var duration time.Duration
|
|
|
|
if strDuration := r.FormValue("lookback_delta"); strDuration != "" {
|
|
parsedDuration, err := parseDuration(strDuration)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing lookback delta duration: %w", err)
|
|
}
|
|
duration = parsedDuration
|
|
}
|
|
|
|
return promql.NewPrometheusQueryOpts(r.FormValue("stats") == "all", duration), nil
|
|
}
|
|
|
|
func (api *API) queryRange(r *http.Request) (result apiFuncResult) {
|
|
start, err := parseTime(r.FormValue("start"))
|
|
if err != nil {
|
|
return invalidParamError(err, "start")
|
|
}
|
|
end, err := parseTime(r.FormValue("end"))
|
|
if err != nil {
|
|
return invalidParamError(err, "end")
|
|
}
|
|
if end.Before(start) {
|
|
return invalidParamError(errors.New("end timestamp must not be before start time"), "end")
|
|
}
|
|
|
|
step, err := parseDuration(r.FormValue("step"))
|
|
if err != nil {
|
|
return invalidParamError(err, "step")
|
|
}
|
|
|
|
if step <= 0 {
|
|
return invalidParamError(errors.New("zero or negative query resolution step widths are not accepted. Try a positive integer"), "step")
|
|
}
|
|
|
|
// 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 apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
|
|
}
|
|
|
|
ctx := r.Context()
|
|
if to := r.FormValue("timeout"); to != "" {
|
|
var cancel context.CancelFunc
|
|
timeout, err := parseDuration(to)
|
|
if err != nil {
|
|
return invalidParamError(err, "timeout")
|
|
}
|
|
|
|
ctx, cancel = context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
}
|
|
|
|
opts, err := extractQueryOpts(r)
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
|
|
}
|
|
qry, err := api.QueryEngine.NewRangeQuery(ctx, api.Queryable, opts, r.FormValue("query"), start, end, step)
|
|
if err != nil {
|
|
return invalidParamError(err, "query")
|
|
}
|
|
// From now on, we must only return with a finalizer in the result (to
|
|
// be called by the caller) or call qry.Close ourselves (which is
|
|
// required in the case of a panic).
|
|
defer func() {
|
|
if result.finalizer == nil {
|
|
qry.Close()
|
|
}
|
|
}()
|
|
|
|
ctx = httputil.ContextFromRequest(ctx, r)
|
|
|
|
res := qry.Exec(ctx)
|
|
if res.Err != nil {
|
|
return apiFuncResult{nil, returnAPIError(res.Err), res.Warnings, qry.Close}
|
|
}
|
|
|
|
// Optional stats field in response if parameter "stats" is not empty.
|
|
sr := api.statsRenderer
|
|
if sr == nil {
|
|
sr = DefaultStatsRenderer
|
|
}
|
|
qs := sr(ctx, qry.Stats(), r.FormValue("stats"))
|
|
|
|
return apiFuncResult{&QueryData{
|
|
ResultType: res.Value.Type(),
|
|
Result: res.Value,
|
|
Stats: qs,
|
|
}, nil, res.Warnings, qry.Close}
|
|
}
|
|
|
|
func (api *API) queryExemplars(r *http.Request) apiFuncResult {
|
|
start, err := parseTimeParam(r, "start", MinTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "start")
|
|
}
|
|
end, err := parseTimeParam(r, "end", MaxTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "end")
|
|
}
|
|
if end.Before(start) {
|
|
err := errors.New("end timestamp must not be before start timestamp")
|
|
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
|
|
}
|
|
|
|
expr, err := parser.ParseExpr(r.FormValue("query"))
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
|
|
}
|
|
|
|
selectors := parser.ExtractSelectors(expr)
|
|
if len(selectors) < 1 {
|
|
return apiFuncResult{nil, nil, nil, nil}
|
|
}
|
|
|
|
ctx := r.Context()
|
|
eq, err := api.ExemplarQueryable.ExemplarQuerier(ctx)
|
|
if err != nil {
|
|
return apiFuncResult{nil, returnAPIError(err), nil, nil}
|
|
}
|
|
|
|
res, err := eq.Select(timestamp.FromTime(start), timestamp.FromTime(end), selectors...)
|
|
if err != nil {
|
|
return apiFuncResult{nil, returnAPIError(err), nil, nil}
|
|
}
|
|
|
|
return apiFuncResult{res, nil, nil, nil}
|
|
}
|
|
|
|
func returnAPIError(err error) *apiError {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
var eqc promql.ErrQueryCanceled
|
|
var eqt promql.ErrQueryTimeout
|
|
var es promql.ErrStorage
|
|
switch {
|
|
case errors.As(err, &eqc):
|
|
return &apiError{errorCanceled, err}
|
|
case errors.As(err, &eqt):
|
|
return &apiError{errorTimeout, err}
|
|
case errors.As(err, &es):
|
|
return &apiError{errorInternal, err}
|
|
}
|
|
|
|
if errors.Is(err, context.Canceled) {
|
|
return &apiError{errorCanceled, err}
|
|
}
|
|
|
|
return &apiError{errorExec, err}
|
|
}
|
|
|
|
func (api *API) labelNames(r *http.Request) apiFuncResult {
|
|
limit, err := parseLimitParam(r.FormValue("limit"))
|
|
if err != nil {
|
|
return invalidParamError(err, "limit")
|
|
}
|
|
|
|
start, err := parseTimeParam(r, "start", MinTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "start")
|
|
}
|
|
end, err := parseTimeParam(r, "end", MaxTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "end")
|
|
}
|
|
|
|
matcherSets, err := parseMatchersParam(r.Form["match[]"])
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
|
|
}
|
|
|
|
hints := &storage.LabelHints{
|
|
Limit: toHintLimit(limit),
|
|
}
|
|
|
|
q, err := api.Queryable.Querier(timestamp.FromTime(start), timestamp.FromTime(end))
|
|
if err != nil {
|
|
return apiFuncResult{nil, returnAPIError(err), nil, nil}
|
|
}
|
|
defer q.Close()
|
|
|
|
var (
|
|
names []string
|
|
warnings annotations.Annotations
|
|
)
|
|
if len(matcherSets) > 1 {
|
|
labelNamesSet := make(map[string]struct{})
|
|
|
|
for _, matchers := range matcherSets {
|
|
vals, callWarnings, err := q.LabelNames(r.Context(), hints, matchers...)
|
|
if err != nil {
|
|
return apiFuncResult{nil, returnAPIError(err), warnings, nil}
|
|
}
|
|
|
|
warnings.Merge(callWarnings)
|
|
for _, val := range vals {
|
|
labelNamesSet[val] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Convert the map to an array.
|
|
names = make([]string, 0, len(labelNamesSet))
|
|
for key := range labelNamesSet {
|
|
names = append(names, key)
|
|
}
|
|
slices.Sort(names)
|
|
} else {
|
|
var matchers []*labels.Matcher
|
|
if len(matcherSets) == 1 {
|
|
matchers = matcherSets[0]
|
|
}
|
|
names, warnings, err = q.LabelNames(r.Context(), hints, matchers...)
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorExec, err}, warnings, nil}
|
|
}
|
|
}
|
|
|
|
if names == nil {
|
|
names = []string{}
|
|
}
|
|
|
|
if limit > 0 && len(names) > limit {
|
|
names = names[:limit]
|
|
warnings = warnings.Add(errors.New("results truncated due to limit"))
|
|
}
|
|
return apiFuncResult{names, nil, warnings, nil}
|
|
}
|
|
|
|
func (api *API) labelValues(r *http.Request) (result apiFuncResult) {
|
|
ctx := r.Context()
|
|
name := route.Param(ctx, "name")
|
|
|
|
if !model.LabelNameRE.MatchString(name) {
|
|
return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)}, nil, nil}
|
|
}
|
|
|
|
limit, err := parseLimitParam(r.FormValue("limit"))
|
|
if err != nil {
|
|
return invalidParamError(err, "limit")
|
|
}
|
|
|
|
start, err := parseTimeParam(r, "start", MinTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "start")
|
|
}
|
|
end, err := parseTimeParam(r, "end", MaxTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "end")
|
|
}
|
|
|
|
matcherSets, err := parseMatchersParam(r.Form["match[]"])
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
|
|
}
|
|
|
|
hints := &storage.LabelHints{
|
|
Limit: toHintLimit(limit),
|
|
}
|
|
|
|
q, err := api.Queryable.Querier(timestamp.FromTime(start), timestamp.FromTime(end))
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorExec, err}, nil, nil}
|
|
}
|
|
// From now on, we must only return with a finalizer in the result (to
|
|
// be called by the caller) or call q.Close ourselves (which is required
|
|
// in the case of a panic).
|
|
defer func() {
|
|
if result.finalizer == nil {
|
|
q.Close()
|
|
}
|
|
}()
|
|
closer := func() {
|
|
q.Close()
|
|
}
|
|
|
|
var (
|
|
vals []string
|
|
warnings annotations.Annotations
|
|
)
|
|
if len(matcherSets) > 1 {
|
|
var callWarnings annotations.Annotations
|
|
labelValuesSet := make(map[string]struct{})
|
|
for _, matchers := range matcherSets {
|
|
vals, callWarnings, err = q.LabelValues(ctx, name, hints, matchers...)
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorExec, err}, warnings, closer}
|
|
}
|
|
warnings.Merge(callWarnings)
|
|
for _, val := range vals {
|
|
labelValuesSet[val] = struct{}{}
|
|
}
|
|
}
|
|
|
|
vals = make([]string, 0, len(labelValuesSet))
|
|
for val := range labelValuesSet {
|
|
vals = append(vals, val)
|
|
}
|
|
} else {
|
|
var matchers []*labels.Matcher
|
|
if len(matcherSets) == 1 {
|
|
matchers = matcherSets[0]
|
|
}
|
|
vals, warnings, err = q.LabelValues(ctx, name, hints, matchers...)
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorExec, err}, warnings, closer}
|
|
}
|
|
|
|
if vals == nil {
|
|
vals = []string{}
|
|
}
|
|
}
|
|
|
|
slices.Sort(vals)
|
|
|
|
if limit > 0 && len(vals) > limit {
|
|
vals = vals[:limit]
|
|
warnings = warnings.Add(errors.New("results truncated due to limit"))
|
|
}
|
|
|
|
return apiFuncResult{vals, nil, warnings, closer}
|
|
}
|
|
|
|
var (
|
|
// MinTime is the default timestamp used for the start of optional time ranges.
|
|
// Exposed to let downstream projects reference it.
|
|
//
|
|
// Historical note: This should just be time.Unix(math.MinInt64/1000, 0).UTC(),
|
|
// but it was set to a higher value in the past due to a misunderstanding.
|
|
// The value is still low enough for practical purposes, so we don't want
|
|
// to change it now, avoiding confusion for importers of this variable.
|
|
MinTime = time.Unix(math.MinInt64/1000+62135596801, 0).UTC()
|
|
|
|
// MaxTime is the default timestamp used for the end of optional time ranges.
|
|
// Exposed to let downstream projects to reference it.
|
|
//
|
|
// Historical note: This should just be time.Unix(math.MaxInt64/1000, 0).UTC(),
|
|
// but it was set to a lower value in the past due to a misunderstanding.
|
|
// The value is still high enough for practical purposes, so we don't want
|
|
// to change it now, avoiding confusion for importers of this variable.
|
|
MaxTime = time.Unix(math.MaxInt64/1000-62135596801, 999999999).UTC()
|
|
|
|
minTimeFormatted = MinTime.Format(time.RFC3339Nano)
|
|
maxTimeFormatted = MaxTime.Format(time.RFC3339Nano)
|
|
)
|
|
|
|
func (api *API) series(r *http.Request) (result apiFuncResult) {
|
|
ctx := r.Context()
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("error parsing form values: %w", err)}, nil, nil}
|
|
}
|
|
if len(r.Form["match[]"]) == 0 {
|
|
return apiFuncResult{nil, &apiError{errorBadData, errors.New("no match[] parameter provided")}, nil, nil}
|
|
}
|
|
|
|
limit, err := parseLimitParam(r.FormValue("limit"))
|
|
if err != nil {
|
|
return invalidParamError(err, "limit")
|
|
}
|
|
|
|
start, err := parseTimeParam(r, "start", MinTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "start")
|
|
}
|
|
end, err := parseTimeParam(r, "end", MaxTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "end")
|
|
}
|
|
|
|
matcherSets, err := parseMatchersParam(r.Form["match[]"])
|
|
if err != nil {
|
|
return invalidParamError(err, "match[]")
|
|
}
|
|
|
|
q, err := api.Queryable.Querier(timestamp.FromTime(start), timestamp.FromTime(end))
|
|
if err != nil {
|
|
return apiFuncResult{nil, returnAPIError(err), nil, nil}
|
|
}
|
|
// From now on, we must only return with a finalizer in the result (to
|
|
// be called by the caller) or call q.Close ourselves (which is required
|
|
// in the case of a panic).
|
|
defer func() {
|
|
if result.finalizer == nil {
|
|
q.Close()
|
|
}
|
|
}()
|
|
closer := func() {
|
|
q.Close()
|
|
}
|
|
|
|
hints := &storage.SelectHints{
|
|
Start: timestamp.FromTime(start),
|
|
End: timestamp.FromTime(end),
|
|
Func: "series", // There is no series function, this token is used for lookups that don't need samples.
|
|
Limit: toHintLimit(limit),
|
|
}
|
|
var set storage.SeriesSet
|
|
|
|
if len(matcherSets) > 1 {
|
|
var sets []storage.SeriesSet
|
|
for _, mset := range matcherSets {
|
|
// We need to sort this select results to merge (deduplicate) the series sets later.
|
|
s := q.Select(ctx, true, hints, mset...)
|
|
sets = append(sets, s)
|
|
}
|
|
set = storage.NewMergeSeriesSet(sets, storage.ChainedSeriesMerge)
|
|
} else {
|
|
// At this point at least one match exists.
|
|
set = q.Select(ctx, false, hints, matcherSets[0]...)
|
|
}
|
|
|
|
metrics := []labels.Labels{}
|
|
|
|
warnings := set.Warnings()
|
|
|
|
for set.Next() {
|
|
if err := ctx.Err(); err != nil {
|
|
return apiFuncResult{nil, returnAPIError(err), warnings, closer}
|
|
}
|
|
metrics = append(metrics, set.At().Labels())
|
|
|
|
if limit > 0 && len(metrics) > limit {
|
|
metrics = metrics[:limit]
|
|
warnings.Add(errors.New("results truncated due to limit"))
|
|
return apiFuncResult{metrics, nil, warnings, closer}
|
|
}
|
|
}
|
|
if set.Err() != nil {
|
|
return apiFuncResult{nil, returnAPIError(set.Err()), warnings, closer}
|
|
}
|
|
|
|
return apiFuncResult{metrics, nil, warnings, closer}
|
|
}
|
|
|
|
func (api *API) dropSeries(_ *http.Request) apiFuncResult {
|
|
return apiFuncResult{nil, &apiError{errorInternal, errors.New("not implemented")}, nil, nil}
|
|
}
|
|
|
|
// Target has the information for one target.
|
|
type Target struct {
|
|
// Labels before any processing.
|
|
DiscoveredLabels labels.Labels `json:"discoveredLabels"`
|
|
// Any labels that are added to this target and its metrics.
|
|
Labels labels.Labels `json:"labels"`
|
|
|
|
ScrapePool string `json:"scrapePool"`
|
|
ScrapeURL string `json:"scrapeUrl"`
|
|
GlobalURL string `json:"globalUrl"`
|
|
|
|
LastError string `json:"lastError"`
|
|
LastScrape time.Time `json:"lastScrape"`
|
|
LastScrapeDuration float64 `json:"lastScrapeDuration"`
|
|
Health scrape.TargetHealth `json:"health"`
|
|
|
|
ScrapeInterval string `json:"scrapeInterval"`
|
|
ScrapeTimeout string `json:"scrapeTimeout"`
|
|
}
|
|
|
|
type ScrapePoolsDiscovery struct {
|
|
ScrapePools []string `json:"scrapePools"`
|
|
}
|
|
|
|
// DroppedTarget has the information for one target that was dropped during relabelling.
|
|
type DroppedTarget struct {
|
|
// Labels before any processing.
|
|
DiscoveredLabels labels.Labels `json:"discoveredLabels"`
|
|
}
|
|
|
|
// TargetDiscovery has all the active targets.
|
|
type TargetDiscovery struct {
|
|
ActiveTargets []*Target `json:"activeTargets"`
|
|
DroppedTargets []*DroppedTarget `json:"droppedTargets"`
|
|
DroppedTargetCounts map[string]int `json:"droppedTargetCounts"`
|
|
}
|
|
|
|
// GlobalURLOptions contains fields used for deriving the global URL for local targets.
|
|
type GlobalURLOptions struct {
|
|
ListenAddress string
|
|
Host string
|
|
Scheme string
|
|
}
|
|
|
|
// sanitizeSplitHostPort acts like net.SplitHostPort.
|
|
// Additionally, if there is no port in the host passed as input, we return the
|
|
// original host, making sure that IPv6 addresses are not surrounded by square
|
|
// brackets.
|
|
func sanitizeSplitHostPort(input string) (string, string, error) {
|
|
host, port, err := net.SplitHostPort(input)
|
|
if err != nil && strings.HasSuffix(err.Error(), "missing port in address") {
|
|
var errWithPort error
|
|
host, _, errWithPort = net.SplitHostPort(input + ":80")
|
|
if errWithPort == nil {
|
|
err = nil
|
|
}
|
|
}
|
|
return host, port, err
|
|
}
|
|
|
|
func getGlobalURL(u *url.URL, opts GlobalURLOptions) (*url.URL, error) {
|
|
host, port, err := sanitizeSplitHostPort(u.Host)
|
|
if err != nil {
|
|
return u, err
|
|
}
|
|
|
|
for _, lhr := range LocalhostRepresentations {
|
|
if host == lhr {
|
|
_, ownPort, err := net.SplitHostPort(opts.ListenAddress)
|
|
if err != nil {
|
|
return u, err
|
|
}
|
|
|
|
if port == ownPort {
|
|
// Only in the case where the target is on localhost and its port is
|
|
// the same as the one we're listening on, we know for sure that
|
|
// we're monitoring our own process and that we need to change the
|
|
// scheme, hostname, and port to the externally reachable ones as
|
|
// well. We shouldn't need to touch the path at all, since if a
|
|
// path prefix is defined, the path under which we scrape ourselves
|
|
// should already contain the prefix.
|
|
u.Scheme = opts.Scheme
|
|
u.Host = opts.Host
|
|
} else {
|
|
// Otherwise, we only know that localhost is not reachable
|
|
// externally, so we replace only the hostname by the one in the
|
|
// external URL. It could be the wrong hostname for the service on
|
|
// this port, but it's still the best possible guess.
|
|
host, _, err := sanitizeSplitHostPort(opts.Host)
|
|
if err != nil {
|
|
return u, err
|
|
}
|
|
u.Host = host
|
|
if port != "" {
|
|
u.Host = net.JoinHostPort(u.Host, port)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func (api *API) scrapePools(r *http.Request) apiFuncResult {
|
|
names := api.scrapePoolsRetriever(r.Context()).ScrapePools()
|
|
sort.Strings(names)
|
|
res := &ScrapePoolsDiscovery{ScrapePools: names}
|
|
return apiFuncResult{data: res, err: nil, warnings: nil, finalizer: nil}
|
|
}
|
|
|
|
func (api *API) targets(r *http.Request) apiFuncResult {
|
|
sortKeys := func(targets map[string][]*scrape.Target) ([]string, int) {
|
|
var n int
|
|
keys := make([]string, 0, len(targets))
|
|
for k := range targets {
|
|
keys = append(keys, k)
|
|
n += len(targets[k])
|
|
}
|
|
slices.Sort(keys)
|
|
return keys, n
|
|
}
|
|
|
|
scrapePool := r.URL.Query().Get("scrapePool")
|
|
state := strings.ToLower(r.URL.Query().Get("state"))
|
|
showActive := state == "" || state == "any" || state == "active"
|
|
showDropped := state == "" || state == "any" || state == "dropped"
|
|
res := &TargetDiscovery{}
|
|
|
|
if showActive {
|
|
targetsActive := api.targetRetriever(r.Context()).TargetsActive()
|
|
activeKeys, numTargets := sortKeys(targetsActive)
|
|
res.ActiveTargets = make([]*Target, 0, numTargets)
|
|
builder := labels.NewScratchBuilder(0)
|
|
|
|
for _, key := range activeKeys {
|
|
if scrapePool != "" && key != scrapePool {
|
|
continue
|
|
}
|
|
for _, target := range targetsActive[key] {
|
|
lastErrStr := ""
|
|
lastErr := target.LastError()
|
|
if lastErr != nil {
|
|
lastErrStr = lastErr.Error()
|
|
}
|
|
|
|
globalURL, err := getGlobalURL(target.URL(), api.globalURLOptions)
|
|
|
|
res.ActiveTargets = append(res.ActiveTargets, &Target{
|
|
DiscoveredLabels: target.DiscoveredLabels(),
|
|
Labels: target.Labels(&builder),
|
|
ScrapePool: key,
|
|
ScrapeURL: target.URL().String(),
|
|
GlobalURL: globalURL.String(),
|
|
LastError: func() string {
|
|
switch {
|
|
case err == nil && lastErrStr == "":
|
|
return ""
|
|
case err != nil:
|
|
return fmt.Errorf("%s: %w", lastErrStr, err).Error()
|
|
default:
|
|
return lastErrStr
|
|
}
|
|
}(),
|
|
LastScrape: target.LastScrape(),
|
|
LastScrapeDuration: target.LastScrapeDuration().Seconds(),
|
|
Health: target.Health(),
|
|
ScrapeInterval: target.GetValue(model.ScrapeIntervalLabel),
|
|
ScrapeTimeout: target.GetValue(model.ScrapeTimeoutLabel),
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
res.ActiveTargets = []*Target{}
|
|
}
|
|
if showDropped {
|
|
res.DroppedTargetCounts = api.targetRetriever(r.Context()).TargetsDroppedCounts()
|
|
}
|
|
if showDropped {
|
|
targetsDropped := api.targetRetriever(r.Context()).TargetsDropped()
|
|
droppedKeys, numTargets := sortKeys(targetsDropped)
|
|
res.DroppedTargets = make([]*DroppedTarget, 0, numTargets)
|
|
for _, key := range droppedKeys {
|
|
if scrapePool != "" && key != scrapePool {
|
|
continue
|
|
}
|
|
for _, target := range targetsDropped[key] {
|
|
res.DroppedTargets = append(res.DroppedTargets, &DroppedTarget{
|
|
DiscoveredLabels: target.DiscoveredLabels(),
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
res.DroppedTargets = []*DroppedTarget{}
|
|
}
|
|
return apiFuncResult{res, nil, nil, nil}
|
|
}
|
|
|
|
func matchLabels(lset labels.Labels, matchers []*labels.Matcher) bool {
|
|
for _, m := range matchers {
|
|
if !m.Matches(lset.Get(m.Name)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (api *API) targetMetadata(r *http.Request) apiFuncResult {
|
|
limit := -1
|
|
if s := r.FormValue("limit"); s != "" {
|
|
var err error
|
|
if limit, err = strconv.Atoi(s); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, errors.New("limit must be a number")}, nil, nil}
|
|
}
|
|
}
|
|
|
|
matchTarget := r.FormValue("match_target")
|
|
var matchers []*labels.Matcher
|
|
var err error
|
|
if matchTarget != "" {
|
|
matchers, err = parser.ParseMetricSelector(matchTarget)
|
|
if err != nil {
|
|
return invalidParamError(err, "match_target")
|
|
}
|
|
}
|
|
|
|
builder := labels.NewScratchBuilder(0)
|
|
metric := r.FormValue("metric")
|
|
res := []metricMetadata{}
|
|
for _, tt := range api.targetRetriever(r.Context()).TargetsActive() {
|
|
for _, t := range tt {
|
|
if limit >= 0 && len(res) >= limit {
|
|
break
|
|
}
|
|
targetLabels := t.Labels(&builder)
|
|
// Filter targets that don't satisfy the label matchers.
|
|
if matchTarget != "" && !matchLabels(targetLabels, matchers) {
|
|
continue
|
|
}
|
|
// If no metric is specified, get the full list for the target.
|
|
if metric == "" {
|
|
for _, md := range t.ListMetadata() {
|
|
res = append(res, metricMetadata{
|
|
Target: targetLabels,
|
|
Metric: md.Metric,
|
|
Type: md.Type,
|
|
Help: md.Help,
|
|
Unit: md.Unit,
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
// Get metadata for the specified metric.
|
|
if md, ok := t.GetMetadata(metric); ok {
|
|
res = append(res, metricMetadata{
|
|
Target: targetLabels,
|
|
Type: md.Type,
|
|
Help: md.Help,
|
|
Unit: md.Unit,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return apiFuncResult{res, nil, nil, nil}
|
|
}
|
|
|
|
type metricMetadata struct {
|
|
Target labels.Labels `json:"target"`
|
|
Metric string `json:"metric,omitempty"`
|
|
Type model.MetricType `json:"type"`
|
|
Help string `json:"help"`
|
|
Unit string `json:"unit"`
|
|
}
|
|
|
|
// AlertmanagerDiscovery has all the active Alertmanagers.
|
|
type AlertmanagerDiscovery struct {
|
|
ActiveAlertmanagers []*AlertmanagerTarget `json:"activeAlertmanagers"`
|
|
DroppedAlertmanagers []*AlertmanagerTarget `json:"droppedAlertmanagers"`
|
|
}
|
|
|
|
// AlertmanagerTarget has info on one AM.
|
|
type AlertmanagerTarget struct {
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func (api *API) alertmanagers(r *http.Request) apiFuncResult {
|
|
urls := api.alertmanagerRetriever(r.Context()).Alertmanagers()
|
|
droppedURLS := api.alertmanagerRetriever(r.Context()).DroppedAlertmanagers()
|
|
ams := &AlertmanagerDiscovery{ActiveAlertmanagers: make([]*AlertmanagerTarget, len(urls)), DroppedAlertmanagers: make([]*AlertmanagerTarget, len(droppedURLS))}
|
|
for i, url := range urls {
|
|
ams.ActiveAlertmanagers[i] = &AlertmanagerTarget{URL: url.String()}
|
|
}
|
|
for i, url := range droppedURLS {
|
|
ams.DroppedAlertmanagers[i] = &AlertmanagerTarget{URL: url.String()}
|
|
}
|
|
return apiFuncResult{ams, nil, nil, nil}
|
|
}
|
|
|
|
// AlertDiscovery has info for all active alerts.
|
|
type AlertDiscovery struct {
|
|
Alerts []*Alert `json:"alerts"`
|
|
}
|
|
|
|
// Alert has info for an alert.
|
|
type Alert struct {
|
|
Labels labels.Labels `json:"labels"`
|
|
Annotations labels.Labels `json:"annotations"`
|
|
State string `json:"state"`
|
|
ActiveAt *time.Time `json:"activeAt,omitempty"`
|
|
KeepFiringSince *time.Time `json:"keepFiringSince,omitempty"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
func (api *API) alerts(r *http.Request) apiFuncResult {
|
|
alertingRules := api.rulesRetriever(r.Context()).AlertingRules()
|
|
alerts := []*Alert{}
|
|
|
|
for _, alertingRule := range alertingRules {
|
|
alerts = append(
|
|
alerts,
|
|
rulesAlertsToAPIAlerts(alertingRule.ActiveAlerts())...,
|
|
)
|
|
}
|
|
|
|
res := &AlertDiscovery{Alerts: alerts}
|
|
|
|
return apiFuncResult{res, nil, nil, nil}
|
|
}
|
|
|
|
func rulesAlertsToAPIAlerts(rulesAlerts []*rules.Alert) []*Alert {
|
|
apiAlerts := make([]*Alert, len(rulesAlerts))
|
|
for i, ruleAlert := range rulesAlerts {
|
|
apiAlerts[i] = &Alert{
|
|
Labels: ruleAlert.Labels,
|
|
Annotations: ruleAlert.Annotations,
|
|
State: ruleAlert.State.String(),
|
|
ActiveAt: &ruleAlert.ActiveAt,
|
|
Value: strconv.FormatFloat(ruleAlert.Value, 'e', -1, 64),
|
|
}
|
|
if !ruleAlert.KeepFiringSince.IsZero() {
|
|
apiAlerts[i].KeepFiringSince = &ruleAlert.KeepFiringSince
|
|
}
|
|
}
|
|
|
|
return apiAlerts
|
|
}
|
|
|
|
func (api *API) metricMetadata(r *http.Request) apiFuncResult {
|
|
metrics := map[string]map[metadata.Metadata]struct{}{}
|
|
|
|
limit := -1
|
|
if s := r.FormValue("limit"); s != "" {
|
|
var err error
|
|
if limit, err = strconv.Atoi(s); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, errors.New("limit must be a number")}, nil, nil}
|
|
}
|
|
}
|
|
limitPerMetric := -1
|
|
if s := r.FormValue("limit_per_metric"); s != "" {
|
|
var err error
|
|
if limitPerMetric, err = strconv.Atoi(s); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, errors.New("limit_per_metric must be a number")}, nil, nil}
|
|
}
|
|
}
|
|
|
|
metric := r.FormValue("metric")
|
|
for _, tt := range api.targetRetriever(r.Context()).TargetsActive() {
|
|
for _, t := range tt {
|
|
if metric == "" {
|
|
for _, mm := range t.ListMetadata() {
|
|
m := metadata.Metadata{Type: mm.Type, Help: mm.Help, Unit: mm.Unit}
|
|
ms, ok := metrics[mm.Metric]
|
|
|
|
if limitPerMetric > 0 && len(ms) >= limitPerMetric {
|
|
continue
|
|
}
|
|
|
|
if !ok {
|
|
ms = map[metadata.Metadata]struct{}{}
|
|
metrics[mm.Metric] = ms
|
|
}
|
|
ms[m] = struct{}{}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if md, ok := t.GetMetadata(metric); ok {
|
|
m := metadata.Metadata{Type: md.Type, Help: md.Help, Unit: md.Unit}
|
|
ms, ok := metrics[md.Metric]
|
|
|
|
if limitPerMetric > 0 && len(ms) >= limitPerMetric {
|
|
continue
|
|
}
|
|
|
|
if !ok {
|
|
ms = map[metadata.Metadata]struct{}{}
|
|
metrics[md.Metric] = ms
|
|
}
|
|
ms[m] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Put the elements from the pseudo-set into a slice for marshaling.
|
|
res := map[string][]metadata.Metadata{}
|
|
for name, set := range metrics {
|
|
if limit >= 0 && len(res) >= limit {
|
|
break
|
|
}
|
|
|
|
s := []metadata.Metadata{}
|
|
for metadata := range set {
|
|
s = append(s, metadata)
|
|
}
|
|
res[name] = s
|
|
}
|
|
|
|
return apiFuncResult{res, nil, nil, nil}
|
|
}
|
|
|
|
// RuleDiscovery has info for all rules.
|
|
type RuleDiscovery struct {
|
|
RuleGroups []*RuleGroup `json:"groups"`
|
|
GroupNextToken string `json:"groupNextToken:omitempty"`
|
|
}
|
|
|
|
// RuleGroup has info for rules which are part of a group.
|
|
type RuleGroup struct {
|
|
Name string `json:"name"`
|
|
File string `json:"file"`
|
|
// In order to preserve rule ordering, while exposing type (alerting or recording)
|
|
// specific properties, both alerting and recording rules are exposed in the
|
|
// same array.
|
|
Rules []Rule `json:"rules"`
|
|
Interval float64 `json:"interval"`
|
|
Limit int `json:"limit"`
|
|
EvaluationTime float64 `json:"evaluationTime"`
|
|
LastEvaluation time.Time `json:"lastEvaluation"`
|
|
}
|
|
|
|
type Rule interface{}
|
|
|
|
type AlertingRule struct {
|
|
// State can be "pending", "firing", "inactive".
|
|
State string `json:"state"`
|
|
Name string `json:"name"`
|
|
Query string `json:"query"`
|
|
Duration float64 `json:"duration"`
|
|
KeepFiringFor float64 `json:"keepFiringFor"`
|
|
Labels labels.Labels `json:"labels"`
|
|
Annotations labels.Labels `json:"annotations"`
|
|
Alerts []*Alert `json:"alerts"`
|
|
Health rules.RuleHealth `json:"health"`
|
|
LastError string `json:"lastError,omitempty"`
|
|
EvaluationTime float64 `json:"evaluationTime"`
|
|
LastEvaluation time.Time `json:"lastEvaluation"`
|
|
// Type of an alertingRule is always "alerting".
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type RecordingRule struct {
|
|
Name string `json:"name"`
|
|
Query string `json:"query"`
|
|
Labels labels.Labels `json:"labels,omitempty"`
|
|
Health rules.RuleHealth `json:"health"`
|
|
LastError string `json:"lastError,omitempty"`
|
|
EvaluationTime float64 `json:"evaluationTime"`
|
|
LastEvaluation time.Time `json:"lastEvaluation"`
|
|
// Type of a recordingRule is always "recording".
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
func (api *API) rules(r *http.Request) apiFuncResult {
|
|
if err := r.ParseForm(); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("error parsing form values: %w", err)}, nil, nil}
|
|
}
|
|
|
|
queryFormToSet := func(values []string) map[string]struct{} {
|
|
set := make(map[string]struct{}, len(values))
|
|
for _, v := range values {
|
|
set[v] = struct{}{}
|
|
}
|
|
return set
|
|
}
|
|
|
|
rnSet := queryFormToSet(r.Form["rule_name[]"])
|
|
rgSet := queryFormToSet(r.Form["rule_group[]"])
|
|
fSet := queryFormToSet(r.Form["file[]"])
|
|
|
|
matcherSets, err := parseMatchersParam(r.Form["match[]"])
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, err}, nil, nil}
|
|
}
|
|
|
|
ruleGroups := api.rulesRetriever(r.Context()).RuleGroups()
|
|
res := &RuleDiscovery{RuleGroups: make([]*RuleGroup, 0, len(ruleGroups))}
|
|
typ := strings.ToLower(r.URL.Query().Get("type"))
|
|
|
|
if typ != "" && typ != "alert" && typ != "record" {
|
|
return invalidParamError(fmt.Errorf("not supported value %q", typ), "type")
|
|
}
|
|
|
|
returnAlerts := typ == "" || typ == "alert"
|
|
returnRecording := typ == "" || typ == "record"
|
|
|
|
excludeAlerts, err := parseExcludeAlerts(r)
|
|
if err != nil {
|
|
return invalidParamError(err, "exclude_alerts")
|
|
}
|
|
|
|
maxGroups, nextToken, parseErr := parseListRulesPaginationRequest(r)
|
|
if parseErr != nil {
|
|
return *parseErr
|
|
}
|
|
|
|
rgs := make([]*RuleGroup, 0, len(ruleGroups))
|
|
|
|
foundToken := false
|
|
|
|
for _, grp := range ruleGroups {
|
|
if maxGroups > 0 && nextToken != "" && !foundToken {
|
|
if nextToken != getRuleGroupNextToken(grp.File(), grp.Name()) {
|
|
continue
|
|
}
|
|
foundToken = true
|
|
}
|
|
|
|
if len(rgSet) > 0 {
|
|
if _, ok := rgSet[grp.Name()]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if len(fSet) > 0 {
|
|
if _, ok := fSet[grp.File()]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
|
|
apiRuleGroup := &RuleGroup{
|
|
Name: grp.Name(),
|
|
File: grp.File(),
|
|
Interval: grp.Interval().Seconds(),
|
|
Limit: grp.Limit(),
|
|
Rules: []Rule{},
|
|
EvaluationTime: grp.GetEvaluationTime().Seconds(),
|
|
LastEvaluation: grp.GetLastEvaluation(),
|
|
}
|
|
|
|
for _, rr := range grp.Rules(matcherSets...) {
|
|
var enrichedRule Rule
|
|
|
|
if len(rnSet) > 0 {
|
|
if _, ok := rnSet[rr.Name()]; !ok {
|
|
continue
|
|
}
|
|
}
|
|
|
|
lastError := ""
|
|
if rr.LastError() != nil {
|
|
lastError = rr.LastError().Error()
|
|
}
|
|
switch rule := rr.(type) {
|
|
case *rules.AlertingRule:
|
|
if !returnAlerts {
|
|
break
|
|
}
|
|
var activeAlerts []*Alert
|
|
if !excludeAlerts {
|
|
activeAlerts = rulesAlertsToAPIAlerts(rule.ActiveAlerts())
|
|
}
|
|
|
|
enrichedRule = AlertingRule{
|
|
State: rule.State().String(),
|
|
Name: rule.Name(),
|
|
Query: rule.Query().String(),
|
|
Duration: rule.HoldDuration().Seconds(),
|
|
KeepFiringFor: rule.KeepFiringFor().Seconds(),
|
|
Labels: rule.Labels(),
|
|
Annotations: rule.Annotations(),
|
|
Alerts: activeAlerts,
|
|
Health: rule.Health(),
|
|
LastError: lastError,
|
|
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
|
|
LastEvaluation: rule.GetEvaluationTimestamp(),
|
|
Type: "alerting",
|
|
}
|
|
|
|
case *rules.RecordingRule:
|
|
if !returnRecording {
|
|
break
|
|
}
|
|
enrichedRule = RecordingRule{
|
|
Name: rule.Name(),
|
|
Query: rule.Query().String(),
|
|
Labels: rule.Labels(),
|
|
Health: rule.Health(),
|
|
LastError: lastError,
|
|
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
|
|
LastEvaluation: rule.GetEvaluationTimestamp(),
|
|
Type: "recording",
|
|
}
|
|
default:
|
|
err := fmt.Errorf("failed to assert type of rule '%v'", rule.Name())
|
|
return apiFuncResult{nil, &apiError{errorInternal, err}, nil, nil}
|
|
}
|
|
|
|
if enrichedRule != nil {
|
|
apiRuleGroup.Rules = append(apiRuleGroup.Rules, enrichedRule)
|
|
}
|
|
}
|
|
|
|
// If the rule group response has no rules, skip it - this means we filtered all the rules of this group.
|
|
if len(apiRuleGroup.Rules) > 0 {
|
|
if maxGroups > 0 && len(rgs) == int(maxGroups) {
|
|
// We've reached the capacity of our page plus one. That means that for sure there will be at least one
|
|
// rule group in a subsequent request. Therefore a next token is required.
|
|
res.GroupNextToken = getRuleGroupNextToken(grp.File(), grp.Name())
|
|
break
|
|
}
|
|
rgs = append(rgs, apiRuleGroup)
|
|
}
|
|
}
|
|
|
|
if maxGroups > 0 && nextToken != "" && !foundToken {
|
|
return invalidParamError(fmt.Errorf("invalid group_next_token '%v'. were rule groups changed?", nextToken), "group_next_token")
|
|
}
|
|
|
|
res.RuleGroups = rgs
|
|
return apiFuncResult{res, nil, nil, nil}
|
|
}
|
|
|
|
func parseExcludeAlerts(r *http.Request) (bool, error) {
|
|
excludeAlertsParam := strings.ToLower(r.URL.Query().Get("exclude_alerts"))
|
|
|
|
if excludeAlertsParam == "" {
|
|
return false, nil
|
|
}
|
|
|
|
excludeAlerts, err := strconv.ParseBool(excludeAlertsParam)
|
|
if err != nil {
|
|
return false, fmt.Errorf("error converting exclude_alerts: %w", err)
|
|
}
|
|
return excludeAlerts, nil
|
|
}
|
|
|
|
func parseListRulesPaginationRequest(r *http.Request) (int64, string, *apiFuncResult) {
|
|
var (
|
|
parsedMaxGroups int64 = -1
|
|
err error
|
|
)
|
|
maxGroups := r.URL.Query().Get("group_limit")
|
|
nextToken := r.URL.Query().Get("group_next_token")
|
|
|
|
if nextToken != "" && maxGroups == "" {
|
|
errResult := invalidParamError(fmt.Errorf("group_limit needs to be present in order to paginate over the groups"), "group_next_token")
|
|
return -1, "", &errResult
|
|
}
|
|
|
|
if maxGroups != "" {
|
|
parsedMaxGroups, err = strconv.ParseInt(maxGroups, 10, 32)
|
|
if err != nil {
|
|
errResult := invalidParamError(fmt.Errorf("group_limit needs to be a valid number: %w", err), "group_limit")
|
|
return -1, "", &errResult
|
|
}
|
|
if parsedMaxGroups <= 0 {
|
|
errResult := invalidParamError(fmt.Errorf("group_limit needs to be greater than 0"), "group_limit")
|
|
return -1, "", &errResult
|
|
}
|
|
}
|
|
|
|
if parsedMaxGroups > 0 {
|
|
return parsedMaxGroups, nextToken, nil
|
|
}
|
|
|
|
return -1, "", nil
|
|
}
|
|
|
|
func getRuleGroupNextToken(file, group string) string {
|
|
h := sha1.New()
|
|
h.Write([]byte(file + ";" + group))
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
type prometheusConfig struct {
|
|
YAML string `json:"yaml"`
|
|
}
|
|
|
|
func (api *API) serveRuntimeInfo(_ *http.Request) apiFuncResult {
|
|
status, err := api.runtimeInfo()
|
|
if err != nil {
|
|
return apiFuncResult{status, &apiError{errorInternal, err}, nil, nil}
|
|
}
|
|
return apiFuncResult{status, nil, nil, nil}
|
|
}
|
|
|
|
func (api *API) serveBuildInfo(_ *http.Request) apiFuncResult {
|
|
return apiFuncResult{api.buildInfo, nil, nil, nil}
|
|
}
|
|
|
|
func (api *API) serveConfig(_ *http.Request) apiFuncResult {
|
|
cfg := &prometheusConfig{
|
|
YAML: api.config().String(),
|
|
}
|
|
return apiFuncResult{cfg, nil, nil, nil}
|
|
}
|
|
|
|
func (api *API) serveFlags(_ *http.Request) apiFuncResult {
|
|
return apiFuncResult{api.flagsMap, nil, nil, nil}
|
|
}
|
|
|
|
// TSDBStat holds the information about individual cardinality.
|
|
type TSDBStat struct {
|
|
Name string `json:"name"`
|
|
Value uint64 `json:"value"`
|
|
}
|
|
|
|
// HeadStats has information about the TSDB head.
|
|
type HeadStats struct {
|
|
NumSeries uint64 `json:"numSeries"`
|
|
NumLabelPairs int `json:"numLabelPairs"`
|
|
ChunkCount int64 `json:"chunkCount"`
|
|
MinTime int64 `json:"minTime"`
|
|
MaxTime int64 `json:"maxTime"`
|
|
}
|
|
|
|
// TSDBStatus has information of cardinality statistics from postings.
|
|
type TSDBStatus struct {
|
|
HeadStats HeadStats `json:"headStats"`
|
|
SeriesCountByMetricName []TSDBStat `json:"seriesCountByMetricName"`
|
|
LabelValueCountByLabelName []TSDBStat `json:"labelValueCountByLabelName"`
|
|
MemoryInBytesByLabelName []TSDBStat `json:"memoryInBytesByLabelName"`
|
|
SeriesCountByLabelValuePair []TSDBStat `json:"seriesCountByLabelValuePair"`
|
|
}
|
|
|
|
// TSDBStatsFromIndexStats converts a index.Stat slice to a TSDBStat slice.
|
|
func TSDBStatsFromIndexStats(stats []index.Stat) []TSDBStat {
|
|
result := make([]TSDBStat, 0, len(stats))
|
|
for _, item := range stats {
|
|
item := TSDBStat{Name: item.Name, Value: item.Count}
|
|
result = append(result, item)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (api *API) serveTSDBStatus(r *http.Request) apiFuncResult {
|
|
limit := 10
|
|
if s := r.FormValue("limit"); s != "" {
|
|
var err error
|
|
if limit, err = strconv.Atoi(s); err != nil || limit < 1 {
|
|
return apiFuncResult{nil, &apiError{errorBadData, errors.New("limit must be a positive number")}, nil, nil}
|
|
}
|
|
}
|
|
s, err := api.db.Stats(labels.MetricName, limit)
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorInternal, err}, nil, nil}
|
|
}
|
|
metrics, err := api.gatherer.Gather()
|
|
if err != nil {
|
|
return apiFuncResult{nil, &apiError{errorInternal, fmt.Errorf("error gathering runtime status: %w", err)}, nil, nil}
|
|
}
|
|
chunkCount := int64(math.NaN())
|
|
for _, mF := range metrics {
|
|
if *mF.Name == "prometheus_tsdb_head_chunks" {
|
|
m := mF.Metric[0]
|
|
if m.Gauge != nil {
|
|
chunkCount = int64(m.Gauge.GetValue())
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return apiFuncResult{TSDBStatus{
|
|
HeadStats: HeadStats{
|
|
NumSeries: s.NumSeries,
|
|
ChunkCount: chunkCount,
|
|
MinTime: s.MinTime,
|
|
MaxTime: s.MaxTime,
|
|
NumLabelPairs: s.IndexPostingStats.NumLabelPairs,
|
|
},
|
|
SeriesCountByMetricName: TSDBStatsFromIndexStats(s.IndexPostingStats.CardinalityMetricsStats),
|
|
LabelValueCountByLabelName: TSDBStatsFromIndexStats(s.IndexPostingStats.CardinalityLabelStats),
|
|
MemoryInBytesByLabelName: TSDBStatsFromIndexStats(s.IndexPostingStats.LabelValueStats),
|
|
SeriesCountByLabelValuePair: TSDBStatsFromIndexStats(s.IndexPostingStats.LabelValuePairsStats),
|
|
}, nil, nil, nil}
|
|
}
|
|
|
|
type walReplayStatus struct {
|
|
Min int `json:"min"`
|
|
Max int `json:"max"`
|
|
Current int `json:"current"`
|
|
}
|
|
|
|
func (api *API) serveWALReplayStatus(w http.ResponseWriter, r *http.Request) {
|
|
httputil.SetCORS(w, api.CORSOrigin, r)
|
|
status, err := api.db.WALReplayStatus()
|
|
if err != nil {
|
|
api.respondError(w, &apiError{errorInternal, err}, nil)
|
|
}
|
|
api.respond(w, r, walReplayStatus{
|
|
Min: status.Min,
|
|
Max: status.Max,
|
|
Current: status.Current,
|
|
}, nil, "")
|
|
}
|
|
|
|
func (api *API) notifications(w http.ResponseWriter, r *http.Request) {
|
|
httputil.SetCORS(w, api.CORSOrigin, r)
|
|
api.respond(w, r, api.notificationsGetter(), nil, "")
|
|
}
|
|
|
|
func (api *API) notificationsSSE(w http.ResponseWriter, r *http.Request) {
|
|
httputil.SetCORS(w, api.CORSOrigin, r)
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
|
|
// Subscribe to notifications.
|
|
notifications, unsubscribe, ok := api.notificationsSub()
|
|
if !ok {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
defer unsubscribe()
|
|
|
|
// Set up a flusher to push the response to the client.
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Flush the response to ensure the headers are immediately and eventSource
|
|
// onopen is triggered client-side.
|
|
flusher.Flush()
|
|
|
|
for {
|
|
select {
|
|
case notification := <-notifications:
|
|
// Marshal the notification to JSON.
|
|
jsonData, err := json.Marshal(notification)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
continue
|
|
}
|
|
|
|
// Write the event data in SSE format with JSON content.
|
|
fmt.Fprintf(w, "data: %s\n\n", jsonData)
|
|
|
|
// Flush the response to ensure the data is sent immediately.
|
|
flusher.Flush()
|
|
case <-r.Context().Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *API) remoteRead(w http.ResponseWriter, r *http.Request) {
|
|
// This is only really for tests - this will never be nil IRL.
|
|
if api.remoteReadHandler != nil {
|
|
api.remoteReadHandler.ServeHTTP(w, r)
|
|
} else {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func (api *API) remoteWrite(w http.ResponseWriter, r *http.Request) {
|
|
if api.remoteWriteHandler != nil {
|
|
api.remoteWriteHandler.ServeHTTP(w, r)
|
|
} else {
|
|
http.Error(w, "remote write receiver needs to be enabled with --web.enable-remote-write-receiver", http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func (api *API) otlpWrite(w http.ResponseWriter, r *http.Request) {
|
|
if api.otlpWriteHandler != nil {
|
|
api.otlpWriteHandler.ServeHTTP(w, r)
|
|
} else {
|
|
http.Error(w, "otlp write receiver needs to be enabled with --web.enable-otlp-receiver", http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func (api *API) deleteSeries(r *http.Request) apiFuncResult {
|
|
if !api.enableAdmin {
|
|
return apiFuncResult{nil, &apiError{errorUnavailable, errors.New("admin APIs disabled")}, nil, nil}
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("error parsing form values: %w", err)}, nil, nil}
|
|
}
|
|
if len(r.Form["match[]"]) == 0 {
|
|
return apiFuncResult{nil, &apiError{errorBadData, errors.New("no match[] parameter provided")}, nil, nil}
|
|
}
|
|
|
|
start, err := parseTimeParam(r, "start", MinTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "start")
|
|
}
|
|
end, err := parseTimeParam(r, "end", MaxTime)
|
|
if err != nil {
|
|
return invalidParamError(err, "end")
|
|
}
|
|
|
|
for _, s := range r.Form["match[]"] {
|
|
matchers, err := parser.ParseMetricSelector(s)
|
|
if err != nil {
|
|
return invalidParamError(err, "match[]")
|
|
}
|
|
if err := api.db.Delete(r.Context(), timestamp.FromTime(start), timestamp.FromTime(end), matchers...); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorInternal, err}, nil, nil}
|
|
}
|
|
}
|
|
|
|
return apiFuncResult{nil, nil, nil, nil}
|
|
}
|
|
|
|
func (api *API) snapshot(r *http.Request) apiFuncResult {
|
|
if !api.enableAdmin {
|
|
return apiFuncResult{nil, &apiError{errorUnavailable, errors.New("admin APIs disabled")}, nil, nil}
|
|
}
|
|
var (
|
|
skipHead bool
|
|
err error
|
|
)
|
|
if r.FormValue("skip_head") != "" {
|
|
skipHead, err = strconv.ParseBool(r.FormValue("skip_head"))
|
|
if err != nil {
|
|
return invalidParamError(fmt.Errorf("unable to parse boolean: %w", err), "skip_head")
|
|
}
|
|
}
|
|
|
|
var (
|
|
snapdir = filepath.Join(api.dbDir, "snapshots")
|
|
name = fmt.Sprintf("%s-%016x",
|
|
time.Now().UTC().Format("20060102T150405Z0700"),
|
|
rand.Int63())
|
|
dir = filepath.Join(snapdir, name)
|
|
)
|
|
if err := os.MkdirAll(dir, 0o777); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorInternal, fmt.Errorf("create snapshot directory: %w", err)}, nil, nil}
|
|
}
|
|
if err := api.db.Snapshot(dir, !skipHead); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorInternal, fmt.Errorf("create snapshot: %w", err)}, nil, nil}
|
|
}
|
|
|
|
return apiFuncResult{struct {
|
|
Name string `json:"name"`
|
|
}{name}, nil, nil, nil}
|
|
}
|
|
|
|
func (api *API) cleanTombstones(*http.Request) apiFuncResult {
|
|
if !api.enableAdmin {
|
|
return apiFuncResult{nil, &apiError{errorUnavailable, errors.New("admin APIs disabled")}, nil, nil}
|
|
}
|
|
if err := api.db.CleanTombstones(); err != nil {
|
|
return apiFuncResult{nil, &apiError{errorInternal, err}, nil, nil}
|
|
}
|
|
|
|
return apiFuncResult{nil, nil, nil, nil}
|
|
}
|
|
|
|
// Query string is needed to get the position information for the annotations, and it
|
|
// can be empty if the position information isn't needed.
|
|
func (api *API) respond(w http.ResponseWriter, req *http.Request, data interface{}, warnings annotations.Annotations, query string) {
|
|
statusMessage := statusSuccess
|
|
warn, info := warnings.AsStrings(query, 10, 10)
|
|
|
|
resp := &Response{
|
|
Status: statusMessage,
|
|
Data: data,
|
|
Warnings: warn,
|
|
Infos: info,
|
|
}
|
|
|
|
codec, err := api.negotiateCodec(req, resp)
|
|
if err != nil {
|
|
api.respondError(w, &apiError{errorNotAcceptable, err}, nil)
|
|
return
|
|
}
|
|
|
|
b, err := codec.Encode(resp)
|
|
if err != nil {
|
|
api.logger.Error("error marshaling response", "url", req.URL, "err", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", codec.ContentType().String())
|
|
w.WriteHeader(http.StatusOK)
|
|
if n, err := w.Write(b); err != nil {
|
|
api.logger.Error("error writing response", "url", req.URL, "bytesWritten", n, "err", err)
|
|
}
|
|
}
|
|
|
|
func (api *API) negotiateCodec(req *http.Request, resp *Response) (Codec, error) {
|
|
for _, clause := range goautoneg.ParseAccept(req.Header.Get("Accept")) {
|
|
for _, codec := range api.codecs {
|
|
if codec.ContentType().Satisfies(clause) && codec.CanEncode(resp) {
|
|
return codec, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
defaultCodec := api.codecs[0]
|
|
if !defaultCodec.CanEncode(resp) {
|
|
return nil, fmt.Errorf("cannot encode response as %s", defaultCodec.ContentType())
|
|
}
|
|
|
|
return defaultCodec, nil
|
|
}
|
|
|
|
func (api *API) respondError(w http.ResponseWriter, apiErr *apiError, data interface{}) {
|
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
|
b, err := json.Marshal(&Response{
|
|
Status: statusError,
|
|
ErrorType: apiErr.typ,
|
|
Error: apiErr.err.Error(),
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
api.logger.Error("error marshaling json response", "err", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var code int
|
|
switch apiErr.typ {
|
|
case errorBadData:
|
|
code = http.StatusBadRequest
|
|
case errorExec:
|
|
code = http.StatusUnprocessableEntity
|
|
case errorCanceled:
|
|
code = statusClientClosedConnection
|
|
case errorTimeout:
|
|
code = http.StatusServiceUnavailable
|
|
case errorInternal:
|
|
code = http.StatusInternalServerError
|
|
case errorNotFound:
|
|
code = http.StatusNotFound
|
|
case errorNotAcceptable:
|
|
code = http.StatusNotAcceptable
|
|
default:
|
|
code = http.StatusInternalServerError
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
if n, err := w.Write(b); err != nil {
|
|
api.logger.Error("error writing response", "bytesWritten", n, "err", err)
|
|
}
|
|
}
|
|
|
|
func parseTimeParam(r *http.Request, paramName string, defaultValue time.Time) (time.Time, error) {
|
|
val := r.FormValue(paramName)
|
|
if val == "" {
|
|
return defaultValue, nil
|
|
}
|
|
result, err := parseTime(val)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("Invalid time value for '%s': %w", paramName, err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func parseTime(s string) (time.Time, error) {
|
|
if t, err := strconv.ParseFloat(s, 64); err == nil {
|
|
s, ns := math.Modf(t)
|
|
ns = math.Round(ns*1000) / 1000
|
|
return time.Unix(int64(s), int64(ns*float64(time.Second))).UTC(), nil
|
|
}
|
|
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
|
|
return t, nil
|
|
}
|
|
|
|
// Stdlib's time parser can only handle 4 digit years. As a workaround until
|
|
// that is fixed we want to at least support our own boundary times.
|
|
// Context: https://github.com/prometheus/client_golang/issues/614
|
|
// Upstream issue: https://github.com/golang/go/issues/20555
|
|
switch s {
|
|
case minTimeFormatted:
|
|
return MinTime, nil
|
|
case maxTimeFormatted:
|
|
return MaxTime, 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)
|
|
}
|
|
|
|
func parseMatchersParam(matchers []string) ([][]*labels.Matcher, error) {
|
|
matcherSets, err := parser.ParseMetricSelectors(matchers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
OUTER:
|
|
for _, ms := range matcherSets {
|
|
for _, lm := range ms {
|
|
if lm != nil && !lm.Matches("") {
|
|
continue OUTER
|
|
}
|
|
}
|
|
return nil, errors.New("match[] must contain at least one non-empty matcher")
|
|
}
|
|
return matcherSets, nil
|
|
}
|
|
|
|
// parseLimitParam returning 0 means no limit is to be applied.
|
|
func parseLimitParam(limitStr string) (limit int, err error) {
|
|
if limitStr == "" {
|
|
return limit, nil
|
|
}
|
|
|
|
limit, err = strconv.Atoi(limitStr)
|
|
if err != nil {
|
|
return limit, err
|
|
}
|
|
if limit < 0 {
|
|
return limit, errors.New("limit must be non-negative")
|
|
}
|
|
|
|
return limit, nil
|
|
}
|
|
|
|
// toHintLimit increases the API limit, as returned by parseLimitParam, by 1.
|
|
// This allows for emitting warnings when the results are truncated.
|
|
func toHintLimit(limit int) int {
|
|
// 0 means no limit and avoid int overflow
|
|
if limit > 0 && limit < math.MaxInt {
|
|
return limit + 1
|
|
}
|
|
return limit
|
|
}
|