github.com/thanos-io/thanos@v0.32.5/pkg/api/api.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  // Copyright 2016 The Prometheus Authors
     5  // Licensed under the Apache License, Version 2.0 (the "License");
     6  // you may not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing, software
    12  // distributed under the License is distributed on an "AS IS" BASIS,
    13  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  // See the License for the specific language governing permissions and
    15  // limitations under the License.
    16  
    17  // This package is a modified copy from
    18  // github.com/prometheus/prometheus/web/api/v1@2121b4628baa7d9d9406aa468712a6a332e77aff.
    19  
    20  package api
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"net/http"
    26  	"os"
    27  	"runtime"
    28  	"time"
    29  
    30  	"github.com/go-kit/log"
    31  	"github.com/go-kit/log/level"
    32  	"github.com/klauspost/compress/gzhttp"
    33  	"github.com/opentracing/opentracing-go"
    34  	"github.com/prometheus/common/route"
    35  	"github.com/prometheus/common/version"
    36  
    37  	extpromhttp "github.com/thanos-io/thanos/pkg/extprom/http"
    38  	"github.com/thanos-io/thanos/pkg/logging"
    39  	"github.com/thanos-io/thanos/pkg/server/http/middleware"
    40  	"github.com/thanos-io/thanos/pkg/tracing"
    41  )
    42  
    43  type status string
    44  
    45  const (
    46  	StatusSuccess status = "success"
    47  	StatusError   status = "error"
    48  )
    49  
    50  type ErrorType string
    51  
    52  const (
    53  	ErrorNone     ErrorType = ""
    54  	ErrorTimeout  ErrorType = "timeout"
    55  	ErrorCanceled ErrorType = "canceled"
    56  	ErrorExec     ErrorType = "execution"
    57  	ErrorBadData  ErrorType = "bad_data"
    58  	ErrorInternal ErrorType = "internal"
    59  )
    60  
    61  var corsHeaders = map[string]string{
    62  	"Access-Control-Allow-Headers":  "Accept, Accept-Encoding, Authorization, Content-Type, Origin",
    63  	"Access-Control-Allow-Methods":  "GET, OPTIONS",
    64  	"Access-Control-Allow-Origin":   "*",
    65  	"Access-Control-Expose-Headers": "Date",
    66  }
    67  
    68  // ThanosVersion contains build information about Thanos.
    69  type ThanosVersion struct {
    70  	Version   string `json:"version"`
    71  	Revision  string `json:"revision"`
    72  	Branch    string `json:"branch"`
    73  	BuildUser string `json:"buildUser"`
    74  	BuildDate string `json:"buildDate"`
    75  	GoVersion string `json:"goVersion"`
    76  }
    77  
    78  var BuildInfo = &ThanosVersion{
    79  	Version:   version.Version,
    80  	Revision:  version.Revision,
    81  	Branch:    version.Branch,
    82  	BuildUser: version.BuildUser,
    83  	BuildDate: version.BuildDate,
    84  	GoVersion: version.GoVersion,
    85  }
    86  
    87  type ApiError struct {
    88  	Typ ErrorType
    89  	Err error
    90  }
    91  
    92  func (e *ApiError) Error() string {
    93  	return fmt.Sprintf("%s: %s", e.Typ, e.Err)
    94  }
    95  
    96  // RuntimeInfo contains runtime information about Thanos.
    97  type RuntimeInfo struct {
    98  	StartTime      time.Time `json:"startTime"`
    99  	CWD            string    `json:"CWD"`
   100  	GoroutineCount int       `json:"goroutineCount"`
   101  	GOMAXPROCS     int       `json:"GOMAXPROCS"`
   102  	GOGC           string    `json:"GOGC"`
   103  	GODEBUG        string    `json:"GODEBUG"`
   104  }
   105  
   106  // RuntimeInfoFn returns updated runtime information about Thanos.
   107  type RuntimeInfoFn func() RuntimeInfo
   108  
   109  type response struct {
   110  	Status    status      `json:"status"`
   111  	Data      interface{} `json:"data,omitempty"`
   112  	ErrorType ErrorType   `json:"errorType,omitempty"`
   113  	Error     string      `json:"error,omitempty"`
   114  	Warnings  []string    `json:"warnings,omitempty"`
   115  }
   116  
   117  // SetCORS enables cross-site script calls.
   118  func SetCORS(w http.ResponseWriter) {
   119  	for h, v := range corsHeaders {
   120  		w.Header().Set(h, v)
   121  	}
   122  }
   123  
   124  type ApiFunc func(r *http.Request) (interface{}, []error, *ApiError, func())
   125  
   126  type BaseAPI struct {
   127  	logger      log.Logger
   128  	flagsMap    map[string]string
   129  	runtimeInfo RuntimeInfoFn
   130  	buildInfo   *ThanosVersion
   131  	Now         func() time.Time
   132  	disableCORS bool
   133  }
   134  
   135  // NewBaseAPI returns a new initialized BaseAPI type.
   136  func NewBaseAPI(logger log.Logger, disableCORS bool, flagsMap map[string]string) *BaseAPI {
   137  
   138  	return &BaseAPI{
   139  		logger:      logger,
   140  		flagsMap:    flagsMap,
   141  		runtimeInfo: GetRuntimeInfoFunc(logger),
   142  		buildInfo:   BuildInfo,
   143  		disableCORS: disableCORS,
   144  		Now:         time.Now,
   145  	}
   146  }
   147  
   148  // Register registers the common API endpoints.
   149  func (api *BaseAPI) Register(r *route.Router, tracer opentracing.Tracer, logger log.Logger, ins extpromhttp.InstrumentationMiddleware, logMiddleware *logging.HTTPServerMiddleware) {
   150  	instr := GetInstr(tracer, logger, ins, logMiddleware, api.disableCORS)
   151  
   152  	r.Options("/*path", instr("options", api.options))
   153  
   154  	r.Get("/status/flags", instr("status_flags", api.flags))
   155  	r.Get("/status/runtimeinfo", instr("status_runtime", api.serveRuntimeInfo))
   156  	r.Get("/status/buildinfo", instr("status_build", api.serveBuildInfo))
   157  }
   158  
   159  func (api *BaseAPI) options(r *http.Request) (interface{}, []error, *ApiError, func()) {
   160  	return nil, nil, nil, func() {}
   161  }
   162  
   163  func (api *BaseAPI) flags(r *http.Request) (interface{}, []error, *ApiError, func()) {
   164  	return api.flagsMap, nil, nil, func() {}
   165  }
   166  
   167  func (api *BaseAPI) serveRuntimeInfo(r *http.Request) (interface{}, []error, *ApiError, func()) {
   168  	return api.runtimeInfo(), nil, nil, func() {}
   169  }
   170  
   171  func (api *BaseAPI) serveBuildInfo(r *http.Request) (interface{}, []error, *ApiError, func()) {
   172  	return api.buildInfo, nil, nil, func() {}
   173  }
   174  
   175  func GetRuntimeInfoFunc(logger log.Logger) RuntimeInfoFn {
   176  	CWD, err := os.Getwd()
   177  	if err != nil {
   178  		CWD = "<error retrieving current working directory>"
   179  		level.Warn(logger).Log("msg", "failed to retrieve current working directory", "err", err)
   180  	}
   181  
   182  	birth := time.Now()
   183  
   184  	return func() RuntimeInfo {
   185  		return RuntimeInfo{
   186  			StartTime:      birth,
   187  			CWD:            CWD,
   188  			GoroutineCount: runtime.NumGoroutine(),
   189  			GOMAXPROCS:     runtime.GOMAXPROCS(0),
   190  			GOGC:           os.Getenv("GOGC"),
   191  			GODEBUG:        os.Getenv("GODEBUG"),
   192  		}
   193  	}
   194  }
   195  
   196  type InstrFunc func(name string, f ApiFunc) http.HandlerFunc
   197  
   198  // GetInstr returns a http HandlerFunc with the instrumentation middleware.
   199  func GetInstr(
   200  	tracer opentracing.Tracer,
   201  	logger log.Logger,
   202  	ins extpromhttp.InstrumentationMiddleware,
   203  	logMiddleware *logging.HTTPServerMiddleware,
   204  	disableCORS bool,
   205  ) InstrFunc {
   206  	instr := func(name string, f ApiFunc) http.HandlerFunc {
   207  		hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   208  			if !disableCORS {
   209  				SetCORS(w)
   210  			}
   211  			if data, warnings, err, releaseResources := f(r); err != nil {
   212  				RespondError(w, err, data)
   213  				releaseResources()
   214  			} else if data != nil {
   215  				Respond(w, data, warnings)
   216  				releaseResources()
   217  			} else {
   218  				w.WriteHeader(http.StatusNoContent)
   219  				releaseResources()
   220  			}
   221  		})
   222  
   223  		return tracing.HTTPMiddleware(tracer, name, logger,
   224  			ins.NewHandler(name,
   225  				gzhttp.GzipHandler(
   226  					middleware.RequestID(
   227  						logMiddleware.HTTPMiddleware(name, hf),
   228  					),
   229  				),
   230  			),
   231  		)
   232  	}
   233  	return instr
   234  }
   235  
   236  func Respond(w http.ResponseWriter, data interface{}, warnings []error) {
   237  	w.Header().Set("Content-Type", "application/json")
   238  	if len(warnings) > 0 {
   239  		w.Header().Set("Cache-Control", "no-store")
   240  	}
   241  	w.WriteHeader(http.StatusOK)
   242  
   243  	resp := &response{
   244  		Status: StatusSuccess,
   245  		Data:   data,
   246  	}
   247  	for _, warn := range warnings {
   248  		resp.Warnings = append(resp.Warnings, warn.Error())
   249  	}
   250  	_ = json.NewEncoder(w).Encode(resp)
   251  }
   252  
   253  func RespondError(w http.ResponseWriter, apiErr *ApiError, data interface{}) {
   254  	w.Header().Set("Content-Type", "application/json")
   255  	w.Header().Set("Cache-Control", "no-store")
   256  
   257  	var code int
   258  	switch apiErr.Typ {
   259  	case ErrorBadData:
   260  		code = http.StatusBadRequest
   261  	case ErrorExec:
   262  		code = 422
   263  	case ErrorCanceled, ErrorTimeout:
   264  		code = http.StatusServiceUnavailable
   265  	case ErrorInternal:
   266  		code = http.StatusInternalServerError
   267  	default:
   268  		code = http.StatusInternalServerError
   269  	}
   270  	w.WriteHeader(code)
   271  
   272  	_ = json.NewEncoder(w).Encode(&response{
   273  		Status:    StatusError,
   274  		ErrorType: apiErr.Typ,
   275  		Error:     apiErr.Err.Error(),
   276  		Data:      data,
   277  	})
   278  }