github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/server/controller.go (about)

     1  package server
     2  
     3  import (
     4  	"compress/gzip"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	golog "log"
     9  	"net/http"
    10  	"net/http/pprof"
    11  	"net/url"
    12  	"os"
    13  	"path/filepath"
    14  	"sync"
    15  	"sync/atomic"
    16  	"time"
    17  
    18  	"github.com/gorilla/handlers"
    19  	"github.com/gorilla/mux"
    20  	contentencoding "github.com/johejo/go-content-encoding"
    21  	"github.com/klauspost/compress/gzhttp"
    22  	"github.com/prometheus/client_golang/prometheus"
    23  	"github.com/prometheus/client_golang/prometheus/promhttp"
    24  	"github.com/pyroscope-io/pyroscope/pkg/history"
    25  	"github.com/pyroscope-io/pyroscope/pkg/ingestion"
    26  	"github.com/sirupsen/logrus"
    27  	metrics "github.com/slok/go-http-metrics/metrics/prometheus"
    28  	"github.com/slok/go-http-metrics/middleware"
    29  	"github.com/slok/go-http-metrics/middleware/std"
    30  	"gorm.io/gorm"
    31  
    32  	"github.com/pyroscope-io/pyroscope/pkg/api"
    33  	"github.com/pyroscope-io/pyroscope/pkg/api/authz"
    34  	"github.com/pyroscope-io/pyroscope/pkg/api/router"
    35  	"github.com/pyroscope-io/pyroscope/pkg/config"
    36  	"github.com/pyroscope-io/pyroscope/pkg/model"
    37  	"github.com/pyroscope-io/pyroscope/pkg/scrape"
    38  	"github.com/pyroscope-io/pyroscope/pkg/scrape/labels"
    39  	"github.com/pyroscope-io/pyroscope/pkg/server/httputils"
    40  	"github.com/pyroscope-io/pyroscope/pkg/service"
    41  	"github.com/pyroscope-io/pyroscope/pkg/storage"
    42  	"github.com/pyroscope-io/pyroscope/pkg/util/hyperloglog"
    43  	"github.com/pyroscope-io/pyroscope/pkg/util/updates"
    44  	"github.com/pyroscope-io/pyroscope/webapp"
    45  )
    46  
    47  //revive:disable:max-public-structs TODO: we will refactor this later
    48  
    49  const (
    50  	stateCookieName            = "pyroscopeState"
    51  	gzHTTPCompressionThreshold = 2000
    52  )
    53  
    54  type Controller struct {
    55  	drained uint32
    56  
    57  	config     *config.Server
    58  	storage    *storage.Storage
    59  	ingestser  ingestion.Ingester
    60  	log        *logrus.Logger
    61  	httpServer *http.Server
    62  	db         *gorm.DB
    63  	notifier   Notifier
    64  	metricsMdw middleware.Middleware
    65  	dir        http.FileSystem
    66  
    67  	httpUtils httputils.Utils
    68  
    69  	statsMutex sync.Mutex
    70  	stats      map[string]int
    71  
    72  	appStats *hyperloglog.HyperLogLogPlus
    73  
    74  	// Exported metrics.
    75  	exportedMetrics *prometheus.Registry
    76  
    77  	// TODO: Should be moved to a separate Login handler/service.
    78  	authService        service.AuthService
    79  	userService        service.UserService
    80  	jwtTokenService    service.JWTTokenService
    81  	apiKeyService      service.APIKeyService
    82  	annotationsService service.AnnotationsService
    83  	signupDefaultRole  model.Role
    84  
    85  	scrapeManager *scrape.Manager
    86  	historyMgr    history.Manager
    87  }
    88  
    89  type Config struct {
    90  	Configuration *config.Server
    91  	Logger        *logrus.Logger
    92  	// TODO(kolesnikovae): Ideally, Storage should be decomposed.
    93  	*storage.Storage
    94  	ingestion.Ingester
    95  	*gorm.DB
    96  	Notifier
    97  
    98  	// The registerer is used for exposing server metrics.
    99  	MetricsRegisterer       prometheus.Registerer
   100  	ExportedMetricsRegistry *prometheus.Registry
   101  	ScrapeManager           *scrape.Manager
   102  	HistoryMgr              history.Manager
   103  }
   104  
   105  type StatsReceiver interface {
   106  	StatsInc(name string)
   107  }
   108  
   109  type Notifier interface {
   110  	// NotificationText returns message that will be displayed to user
   111  	// on index page load. The message should point user to a critical problem.
   112  	// TODO(kolesnikovae): we should poll for notifications (or subscribe).
   113  	NotificationText() string
   114  }
   115  
   116  type TargetsResponse struct {
   117  	Job                string              `json:"job"`
   118  	TargetURL          string              `json:"url"`
   119  	DiscoveredLabels   labels.Labels       `json:"discoveredLabels"`
   120  	Labels             labels.Labels       `json:"labels"`
   121  	Health             scrape.TargetHealth `json:"health"`
   122  	LastScrape         time.Time           `json:"lastScrape"`
   123  	LastError          string              `json:"lastError"`
   124  	LastScrapeDuration string              `json:"lastScrapeDuration"`
   125  }
   126  
   127  func New(c Config) (*Controller, error) {
   128  	if c.Configuration.BaseURL != "" {
   129  		_, err := url.Parse(c.Configuration.BaseURL)
   130  		if err != nil {
   131  			return nil, fmt.Errorf("BaseURL is invalid: %w", err)
   132  		}
   133  	}
   134  
   135  	if c.HistoryMgr == nil {
   136  		c.HistoryMgr = &history.NoopManager{}
   137  	}
   138  
   139  	ctrl := Controller{
   140  		config:    c.Configuration,
   141  		log:       c.Logger,
   142  		storage:   c.Storage,
   143  		ingestser: c.Ingester,
   144  		notifier:  c.Notifier,
   145  		stats:     make(map[string]int),
   146  		appStats:  mustNewHLL(),
   147  		httpUtils: httputils.NewDefaultHelper(c.Logger),
   148  
   149  		exportedMetrics: c.ExportedMetricsRegistry,
   150  		metricsMdw: middleware.New(middleware.Config{
   151  			Recorder: metrics.NewRecorder(metrics.Config{
   152  				Prefix:   "pyroscope",
   153  				Registry: c.MetricsRegisterer,
   154  			}),
   155  		}),
   156  
   157  		db:            c.DB,
   158  		scrapeManager: c.ScrapeManager,
   159  		historyMgr:    c.HistoryMgr,
   160  	}
   161  
   162  	var err error
   163  	if ctrl.dir, err = webapp.Assets(); err != nil {
   164  		return nil, err
   165  	}
   166  	if ctrl.signupDefaultRole, err = model.ParseRole(c.Configuration.Auth.SignupDefaultRole); err != nil {
   167  		return nil, fmt.Errorf("default signup role is invalid: %w", err)
   168  	}
   169  
   170  	return &ctrl, nil
   171  }
   172  
   173  func mustNewHLL() *hyperloglog.HyperLogLogPlus {
   174  	hll, err := hyperloglog.NewPlus(uint8(18))
   175  	if err != nil {
   176  		panic(err)
   177  	}
   178  	return hll
   179  }
   180  
   181  func (ctrl *Controller) serverMux() (http.Handler, error) {
   182  	// TODO(kolesnikovae):
   183  	//  - Move mux part to pkg/api/router.
   184  	//  - Make prometheus middleware to support gorilla patterns.
   185  	//  - Make diagnostic endpoints protection configurable.
   186  	//  - Auth middleware should never redirect - the logic should be moved to the client side.
   187  	r := mux.NewRouter()
   188  
   189  	r.Use(contentencoding.Decode())
   190  
   191  	ctrl.jwtTokenService = service.NewJWTTokenService(
   192  		[]byte(ctrl.config.Auth.JWTSecret),
   193  		24*time.Hour*time.Duration(ctrl.config.Auth.LoginMaximumLifetimeDays))
   194  
   195  	ctrl.apiKeyService = service.NewAPIKeyService(ctrl.db, ctrl.config.Auth.APIKeyBcryptCost)
   196  	ctrl.authService = service.NewAuthService(ctrl.db, ctrl.jwtTokenService, ctrl.apiKeyService)
   197  	ctrl.userService = service.NewUserService(ctrl.db)
   198  	ctrl.annotationsService = service.NewAnnotationsService(ctrl.db)
   199  
   200  	appMetadataSvc := service.NewApplicationMetadataService(ctrl.db)
   201  	appSvc := service.NewApplicationService(appMetadataSvc, ctrl.storage)
   202  
   203  	apiRouter := router.New(r.PathPrefix("/api").Subrouter(), router.Services{
   204  		Logger:             ctrl.log,
   205  		APIKeyService:      ctrl.apiKeyService,
   206  		AuthService:        ctrl.authService,
   207  		UserService:        ctrl.userService,
   208  		AnnotationsService: ctrl.annotationsService,
   209  		AdhocService: service.NewAdhocService(
   210  			ctrl.config.MaxNodesRender,
   211  			ctrl.config.AdhocDataPath),
   212  		ApplicationListerAndDeleter: appSvc,
   213  	})
   214  
   215  	apiRouter.Use(
   216  		ctrl.drainMiddleware,
   217  		ctrl.authMiddleware(nil))
   218  
   219  	if ctrl.isAuthRequired() {
   220  		apiRouter.RegisterUserHandlers()
   221  		apiRouter.RegisterAPIKeyHandlers()
   222  	}
   223  	apiRouter.RegisterAnnotationsHandlers()
   224  	if !ctrl.config.NoAdhocUI {
   225  		apiRouter.RegisterAdhocHandlers(int64(ctrl.config.AdhocMaxFileSize))
   226  	}
   227  
   228  	// FIXME: not optimal, unify this with the remoteReadHandler at the top
   229  	appsRouter := apiRouter.PathPrefix("/apps").Subrouter()
   230  	if ctrl.config.RemoteRead.Enabled {
   231  		h, err := ctrl.remoteReadHandler(ctrl.config.RemoteRead)
   232  		if err != nil {
   233  			logrus.WithError(err).Error("failed to initialize remote read handler")
   234  		} else {
   235  			appsRouter.Methods(http.MethodGet).Handler(h)
   236  			appsRouter.Methods(http.MethodDelete).Handler(h)
   237  		}
   238  	} else {
   239  		apiRouter.RegisterApplicationHandlers()
   240  	}
   241  
   242  	ingestRouter := r.Path("/ingest").Subrouter()
   243  	ingestRouter.Use(ctrl.drainMiddleware)
   244  	if ctrl.config.Auth.Ingestion.Enabled {
   245  		ingestRouter.Use(
   246  			ctrl.ingestionAuthMiddleware(),
   247  			authz.NewAuthorizer(ctrl.log, httputils.NewDefaultHelper(ctrl.log)).RequireOneOf(
   248  				authz.Role(model.AdminRole),
   249  				authz.Role(model.AgentRole),
   250  			))
   251  	}
   252  
   253  	ingestRouter.Methods(http.MethodPost).Handler(ctrl.ingestHandler())
   254  
   255  	// Routes not protected with auth. Drained at shutdown.
   256  	insecureRoutes, err := ctrl.getAuthRoutes()
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	assetsHandler := r.PathPrefix("/assets/").Handler(http.FileServer(ctrl.dir)).GetHandler().ServeHTTP
   262  	ctrl.addRoutes(r, append(insecureRoutes, []route{
   263  		{"/assets/", assetsHandler}}...),
   264  		ctrl.drainMiddleware)
   265  
   266  	// Protected pages:
   267  	// For these routes server responds with 307 and redirects to /login.
   268  	ih := ctrl.indexHandler()
   269  	ctrl.addRoutes(r, []route{
   270  		{"/", ih},
   271  		{"/comparison", ih},
   272  		{"/comparison-diff", ih},
   273  		{"/tracing", ih},
   274  		{"/service-discovery", ih},
   275  		{"/adhoc-single", ih},
   276  		{"/adhoc-comparison", ih},
   277  		{"/adhoc-comparison-diff", ih},
   278  		{"/settings", ih},
   279  		{"/settings/{page}", ih},
   280  		{"/settings/{page}/{subpage}", ih},
   281  		{"/exemplars/single", ih},
   282  		{"/exemplars/merge", ih},
   283  		{"/explore", ih}},
   284  		ctrl.drainMiddleware,
   285  		ctrl.authMiddleware(ctrl.indexHandler()))
   286  
   287  	var routes []route
   288  	// TODO(kolesnikovae): Consider implementing a middleware.
   289  	if ctrl.config.RemoteRead.Enabled {
   290  		h, err := ctrl.remoteReadHandler(ctrl.config.RemoteRead)
   291  		if err != nil {
   292  			logrus.WithError(err).Error("failed to initialize remote read handler")
   293  		} else {
   294  			routes = append(routes, []route{
   295  				{"/render", h},
   296  				{"/render-diff", h},
   297  				{"/labels", h},
   298  				{"/label-values", h},
   299  				{"/export", h},
   300  				{"/merge", h},
   301  				{"/api/exemplars:merge", h},
   302  				{"/api/exemplars:query", h},
   303  				// TODO(kolesnikovae): Add adhoc endpoints
   304  			}...)
   305  		}
   306  	} else {
   307  		routes = append(routes, []route{
   308  			{"/render", ctrl.renderHandler()},
   309  			{"/render-diff", ctrl.renderDiffHandler()},
   310  			{"/labels", ctrl.labelsHandler()},
   311  			{"/label-values", ctrl.labelValuesHandler()},
   312  			{"/export", ctrl.exportHandler()},
   313  			{"/merge", ctrl.exemplarsHandler().MergeExemplars},
   314  			{"/api/exemplars:merge", ctrl.exemplarsHandler().MergeExemplars},
   315  			{"/api/exemplars:query", ctrl.exemplarsHandler().QueryExemplars},
   316  		}...)
   317  	}
   318  
   319  	// For these routes server responds with 401.
   320  	ctrl.addRoutes(r, routes,
   321  		ctrl.drainMiddleware,
   322  		ctrl.authMiddleware(nil))
   323  
   324  	// TODO(kolesnikovae):
   325  	//  Refactor: move mux part to pkg/api/router.
   326  	//  Make prometheus middleware to support gorilla patterns.
   327  
   328  	// TODO(kolesnikovae):
   329  	//  Make diagnostic endpoints protection configurable.
   330  
   331  	// Diagnostic secure routes: must be protected but not drained.
   332  	diagnosticSecureRoutes := []route{
   333  		{"/config", ctrl.configHandler},
   334  		{"/build", ctrl.buildHandler},
   335  		{"/targets", ctrl.activeTargetsHandler},
   336  		{"/debug/storage/export/{db}", ctrl.storage.DebugExport},
   337  	}
   338  	if !ctrl.config.DisablePprofEndpoint {
   339  		diagnosticSecureRoutes = append(diagnosticSecureRoutes, []route{
   340  			{"/debug/pprof/", pprof.Index},
   341  			{"/debug/pprof/cmdline", pprof.Cmdline},
   342  			{"/debug/pprof/profile", pprof.Profile},
   343  			{"/debug/pprof/symbol", pprof.Symbol},
   344  			{"/debug/pprof/trace", pprof.Trace},
   345  			{"/debug/pprof/allocs", pprof.Index},
   346  			{"/debug/pprof/goroutine", pprof.Index},
   347  			{"/debug/pprof/heap", pprof.Index},
   348  			{"/debug/pprof/threadcreate", pprof.Index},
   349  			{"/debug/pprof/block", pprof.Index},
   350  			{"/debug/pprof/mutex", pprof.Index},
   351  		}...)
   352  	}
   353  
   354  	ctrl.addRoutes(r, diagnosticSecureRoutes, ctrl.authMiddleware(nil))
   355  	ctrl.addRoutes(r, []route{
   356  		{"/metrics", promhttp.Handler().ServeHTTP},
   357  		{"/exported-metrics", ctrl.exportedMetricsHandler},
   358  		{"/healthz", ctrl.healthz},
   359  	})
   360  
   361  	// Respond with 404 for all other routes.
   362  	r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   363  		w.WriteHeader(http.StatusNotFound)
   364  		ih(w, r)
   365  	})
   366  
   367  	return r, nil
   368  }
   369  
   370  func (ctrl *Controller) activeTargetsHandler(w http.ResponseWriter, r *http.Request) {
   371  	targets := ctrl.scrapeManager.TargetsActive()
   372  	resp := []TargetsResponse{}
   373  	for k, v := range targets {
   374  		for _, t := range v {
   375  			var lastError string
   376  			if t.LastError() != nil {
   377  				lastError = t.LastError().Error()
   378  			}
   379  			resp = append(resp, TargetsResponse{
   380  				Job:                k,
   381  				TargetURL:          t.URL().String(),
   382  				DiscoveredLabels:   t.DiscoveredLabels(),
   383  				Labels:             t.Labels(),
   384  				Health:             t.Health(),
   385  				LastScrape:         t.LastScrape(),
   386  				LastError:          lastError,
   387  				LastScrapeDuration: t.LastScrapeDuration().String(),
   388  			})
   389  		}
   390  	}
   391  	ctrl.httpUtils.WriteResponseJSON(r, w, resp)
   392  }
   393  
   394  func (ctrl *Controller) exportedMetricsHandler(w http.ResponseWriter, r *http.Request) {
   395  	promhttp.InstrumentMetricHandler(ctrl.exportedMetrics,
   396  		promhttp.HandlerFor(ctrl.exportedMetrics, promhttp.HandlerOpts{})).
   397  		ServeHTTP(w, r)
   398  }
   399  
   400  func (ctrl *Controller) getAuthRoutes() ([]route, error) {
   401  	authRoutes := []route{
   402  		{"/login", ctrl.loginHandler},
   403  		{"/logout", ctrl.logoutHandler},
   404  		{"/signup", ctrl.signupHandler},
   405  	}
   406  
   407  	if ctrl.config.Auth.Google.Enabled {
   408  		googleHandler, err := newOauthGoogleHandler(ctrl.config.Auth.Google, ctrl.config.BaseURL, ctrl.log)
   409  		if err != nil {
   410  			return nil, err
   411  		}
   412  
   413  		authRoutes = append(authRoutes, []route{
   414  			{"/auth/google/login", ctrl.oauthLoginHandler(googleHandler)},
   415  			{"/auth/google/callback", ctrl.callbackHandler(googleHandler.redirectRoute)},
   416  			{"/auth/google/redirect", ctrl.callbackRedirectHandler(googleHandler)},
   417  		}...)
   418  	}
   419  
   420  	if ctrl.config.Auth.Github.Enabled {
   421  		githubHandler, err := newGithubHandler(ctrl.config.Auth.Github, ctrl.config.BaseURL, ctrl.log)
   422  		if err != nil {
   423  			return nil, err
   424  		}
   425  
   426  		authRoutes = append(authRoutes, []route{
   427  			{"/auth/github/login", ctrl.oauthLoginHandler(githubHandler)},
   428  			{"/auth/github/callback", ctrl.callbackHandler(githubHandler.redirectRoute)},
   429  			{"/auth/github/redirect", ctrl.callbackRedirectHandler(githubHandler)},
   430  		}...)
   431  	}
   432  
   433  	if ctrl.config.Auth.Gitlab.Enabled {
   434  		gitlabHandler, err := newOauthGitlabHandler(ctrl.config.Auth.Gitlab, ctrl.config.BaseURL, ctrl.log)
   435  		if err != nil {
   436  			return nil, err
   437  		}
   438  
   439  		authRoutes = append(authRoutes, []route{
   440  			{"/auth/gitlab/login", ctrl.oauthLoginHandler(gitlabHandler)},
   441  			{"/auth/gitlab/callback", ctrl.callbackHandler(gitlabHandler.redirectRoute)},
   442  			{"/auth/gitlab/redirect", ctrl.callbackRedirectHandler(gitlabHandler)},
   443  		}...)
   444  	}
   445  
   446  	return authRoutes, nil
   447  }
   448  
   449  func (ctrl *Controller) getHandler() (http.Handler, error) {
   450  	handler, err := ctrl.serverMux()
   451  	if err != nil {
   452  		return nil, err
   453  	}
   454  
   455  	gzhttpMiddleware, err := gzhttp.NewWrapper(gzhttp.MinSize(gzHTTPCompressionThreshold), gzhttp.CompressionLevel(gzip.BestSpeed))
   456  	if err != nil {
   457  		return nil, err
   458  	}
   459  
   460  	h := ctrl.corsMiddleware()(gzhttpMiddleware(handler))
   461  	h = ctrl.logginMiddleware(h)
   462  
   463  	return h, nil
   464  }
   465  
   466  func (ctrl *Controller) Start() error {
   467  	return ctrl.startSync(nil)
   468  }
   469  
   470  func (ctrl *Controller) startSync(serveSync chan struct{}) error {
   471  	logger := logrus.New()
   472  	w := logger.Writer()
   473  	defer w.Close()
   474  	handler, err := ctrl.getHandler()
   475  	if err != nil {
   476  		return err
   477  	}
   478  
   479  	ctrl.httpServer = &http.Server{
   480  		Addr:           ctrl.config.APIBindAddr,
   481  		Handler:        handler,
   482  		ReadTimeout:    10 * time.Second,
   483  		WriteTimeout:   15 * time.Second,
   484  		IdleTimeout:    30 * time.Second,
   485  		MaxHeaderBytes: 1 << 20,
   486  		ErrorLog:       golog.New(w, "", 0),
   487  	}
   488  
   489  	updates.StartVersionUpdateLoop()
   490  
   491  	if serveSync != nil {
   492  		serveSync <- struct{}{}
   493  	}
   494  
   495  	if ctrl.config.TLSCertificateFile != "" && ctrl.config.TLSKeyFile != "" {
   496  		err = ctrl.httpServer.ListenAndServeTLS(ctrl.config.TLSCertificateFile, ctrl.config.TLSKeyFile)
   497  	} else {
   498  		err = ctrl.httpServer.ListenAndServe()
   499  	}
   500  
   501  	// ListenAndServe always returns a non-nil error. After Shutdown or Close,
   502  	// the returned error is ErrServerClosed.
   503  	if errors.Is(err, http.ErrServerClosed) {
   504  		return nil
   505  	}
   506  	return err
   507  }
   508  
   509  func (ctrl *Controller) Stop() error {
   510  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   511  	defer cancel()
   512  	return ctrl.httpServer.Shutdown(ctx)
   513  }
   514  
   515  func (ctrl *Controller) corsMiddleware() mux.MiddlewareFunc {
   516  	if len(ctrl.config.CORS.AllowedOrigins) > 0 {
   517  		options := []handlers.CORSOption{
   518  			handlers.AllowedOrigins(ctrl.config.CORS.AllowedOrigins),
   519  			handlers.AllowedMethods(ctrl.config.CORS.AllowedMethods),
   520  			handlers.AllowedHeaders(ctrl.config.CORS.AllowedHeaders),
   521  			handlers.MaxAge(ctrl.config.CORS.MaxAge),
   522  		}
   523  		if ctrl.config.CORS.AllowCredentials {
   524  			options = append(options, handlers.AllowCredentials())
   525  		}
   526  		return handlers.CORS(options...)
   527  	}
   528  	return func(next http.Handler) http.Handler {
   529  		return next
   530  	}
   531  }
   532  
   533  func (ctrl *Controller) Drain() {
   534  	atomic.StoreUint32(&ctrl.drained, 1)
   535  }
   536  
   537  func (ctrl *Controller) drainMiddleware(next http.Handler) http.Handler {
   538  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   539  		if atomic.LoadUint32(&ctrl.drained) > 0 {
   540  			w.WriteHeader(http.StatusServiceUnavailable)
   541  			return
   542  		}
   543  		next.ServeHTTP(w, r)
   544  	})
   545  }
   546  
   547  func (ctrl *Controller) trackMetrics(route string) func(next http.Handler) http.Handler {
   548  	return func(next http.Handler) http.Handler {
   549  		return std.Handler(route, ctrl.metricsMdw, next)
   550  	}
   551  }
   552  
   553  func (ctrl *Controller) redirectPreservingBaseURL(w http.ResponseWriter, r *http.Request, urlStr string, status int) {
   554  	if ctrl.config.BaseURL != "" {
   555  		// we're modifying the URL here so I'm not memoizing it and instead parsing it all over again to create a new object
   556  		u, err := url.Parse(ctrl.config.BaseURL)
   557  		if err != nil {
   558  			// TODO: technically this should never happen because NewController would return an error
   559  			logrus.Error("base URL is invalid, some redirects might not work as expected")
   560  		} else {
   561  			urlStr = filepath.Join(u.Path, urlStr)
   562  		}
   563  	}
   564  
   565  	http.Redirect(w, r, urlStr, status)
   566  }
   567  
   568  func (ctrl *Controller) loginRedirect(w http.ResponseWriter, r *http.Request) {
   569  	ctrl.redirectPreservingBaseURL(w, r, "/login", http.StatusTemporaryRedirect)
   570  }
   571  
   572  func (ctrl *Controller) authMiddleware(redirect http.HandlerFunc) mux.MiddlewareFunc {
   573  	if ctrl.isAuthRequired() {
   574  		return api.AuthMiddleware(redirect, ctrl.authService, ctrl.httpUtils)
   575  	}
   576  	return func(next http.Handler) http.Handler {
   577  		return next
   578  	}
   579  }
   580  
   581  func (ctrl *Controller) ingestionAuthMiddleware() mux.MiddlewareFunc {
   582  	if ctrl.config.Auth.Ingestion.Enabled {
   583  		asConfig := service.CachingAuthServiceConfig{
   584  			Size: ctrl.config.Auth.Ingestion.CacheSize,
   585  			TTL:  ctrl.config.Auth.Ingestion.CacheTTL,
   586  		}
   587  		as := service.NewCachingAuthService(ctrl.authService, asConfig)
   588  		return api.AuthMiddleware(nil, as, ctrl.httpUtils)
   589  	}
   590  	return func(next http.Handler) http.Handler {
   591  		return next
   592  	}
   593  }
   594  
   595  func expectFormats(format string) error {
   596  	switch format {
   597  	case "json", "pprof", "collapsed", "html", "":
   598  		return nil
   599  	default:
   600  		return errUnknownFormat
   601  	}
   602  }
   603  
   604  func (ctrl *Controller) logginMiddleware(next http.Handler) http.Handler {
   605  	if ctrl.config.LogLevel == "debug" {
   606  		// log to Stdout using Apache Common Log Format
   607  		// TODO maybe use JSON?
   608  		return handlers.LoggingHandler(os.Stdout, next)
   609  	}
   610  
   611  	return next
   612  }