Files
grafana/pkg/api/http_server.go
T

556 lines
19 KiB
Go
Raw Normal View History

2016-12-21 14:36:32 +01:00
package api
import (
"context"
"crypto/tls"
2020-11-19 13:34:28 +01:00
"errors"
2016-12-21 14:36:32 +01:00
"fmt"
"net"
2016-12-21 14:36:32 +01:00
"net/http"
"os"
"path"
"path/filepath"
"strings"
"sync"
2016-12-21 14:36:32 +01:00
2019-01-16 14:53:59 +01:00
"github.com/grafana/grafana/pkg/api/routing"
2016-12-21 14:36:32 +01:00
httpstatic "github.com/grafana/grafana/pkg/api/static"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/expr"
2019-06-13 10:55:38 +02:00
"github.com/grafana/grafana/pkg/infra/localcache"
2019-05-13 14:45:54 +08:00
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/login/social"
2016-12-21 14:36:32 +01:00
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
2021-03-23 20:24:08 +03:00
"github.com/grafana/grafana/pkg/plugins/plugincontext"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/contexthandler"
"github.com/grafana/grafana/pkg/services/datasourceproxy"
2018-10-26 10:40:33 +02:00
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/hooks"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/live"
2021-04-26 13:17:49 +03:00
"github.com/grafana/grafana/pkg/services/live/pushhttp"
2019-06-13 17:47:52 +03:00
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/quota"
2018-05-24 15:26:27 +02:00
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/schemaloader"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/searchusers"
2021-11-04 18:47:21 +02:00
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/shorturls"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/updatechecker"
2016-12-21 14:36:32 +01:00
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/web"
2021-03-23 20:24:08 +03:00
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
2016-12-21 14:36:32 +01:00
)
2018-03-22 22:13:46 +01:00
type HTTPServer struct {
log log.Logger
web *web.Mux
context context.Context
httpSrv *http.Server
middlewares []web.Handler
2017-02-06 09:40:07 +01:00
PluginContextProvider *plugincontext.Provider
RouteRegister routing.RouteRegister
Bus bus.Bus
RenderService rendering.Service
Cfg *setting.Cfg
SettingsProvider setting.Provider
HooksService *hooks.HooksService
CacheService *localcache.CacheService
DataSourceCache datasources.CacheService
AuthTokenService models.UserTokenService
QuotaService *quota.QuotaService
RemoteCacheService *remotecache.RemoteCache
ProvisioningService provisioning.ProvisioningService
Login login.Service
License models.Licensing
AccessControl accesscontrol.AccessControl
DataProxy *datasourceproxy.DataSourceProxyService
PluginRequestValidator models.PluginRequestValidator
pluginClient plugins.Client
pluginStore plugins.Store
pluginDashboardManager plugins.PluginDashboardManager
pluginStaticRouteResolver plugins.StaticRouteResolver
pluginErrorResolver plugins.ErrorResolver
SearchService *search.SearchService
ShortURLService shorturls.Service
Live *live.GrafanaLive
LivePushGateway *pushhttp.Gateway
ContextHandler *contexthandler.ContextHandler
SQLStore *sqlstore.SQLStore
AlertEngine *alerting.AlertEngine
LoadSchemaService *schemaloader.SchemaLoaderService
AlertNG *ngalert.AlertNG
LibraryPanelService librarypanels.Service
LibraryElementService libraryelements.Service
notificationService *notifications.NotificationService
SocialService social.Service
OAuthTokenService oauthtoken.OAuthTokenService
Listener net.Listener
EncryptionService encryption.Internal
2021-11-04 18:47:21 +02:00
SecretsService secrets.Service
DataSourcesService *datasources.Service
cleanUpService *cleanup.CleanUpService
tracingService *tracing.TracingService
internalMetricsSvc *metrics.InternalMetricsService
updateChecker *updatechecker.Service
searchUsersService searchusers.Service
expressionService *expr.Service
}
type ServerOptions struct {
Listener net.Listener
2016-12-21 14:36:32 +01:00
}
func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routing.RouteRegister, bus bus.Bus,
renderService rendering.Service, licensing models.Licensing, hooksService *hooks.HooksService,
2021-11-29 14:21:54 +01:00
cacheService *localcache.CacheService, sqlStore *sqlstore.SQLStore, alertEngine *alerting.AlertEngine,
pluginRequestValidator models.PluginRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver,
pluginDashboardManager plugins.PluginDashboardManager, pluginStore plugins.Store, pluginClient plugins.Client,
pluginErrorResolver plugins.ErrorResolver, settingsProvider setting.Provider,
dataSourceCache datasources.CacheService, userTokenService models.UserTokenService,
cleanUpService *cleanup.CleanUpService, shortURLService shorturls.Service,
remoteCache *remotecache.RemoteCache, provisioningService provisioning.ProvisioningService,
loginService login.Service, accessControl accesscontrol.AccessControl,
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
contextHandler *contexthandler.ContextHandler,
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
notificationService *notifications.NotificationService, tracingService *tracing.TracingService,
internalMetricsSvc *metrics.InternalMetricsService, quotaService *quota.QuotaService,
socialService social.Service, oauthTokenService oauthtoken.OAuthTokenService,
encryptionService encryption.Internal, updateChecker *updatechecker.Service, searchUsersService searchusers.Service,
dataSourcesService *datasources.Service, secretsService secrets.Service, expressionService *expr.Service) (*HTTPServer, error) {
web.Env = cfg.Env
m := web.New()
2018-05-01 15:51:15 +02:00
hs := &HTTPServer{
Cfg: cfg,
RouteRegister: routeRegister,
Bus: bus,
RenderService: renderService,
License: licensing,
HooksService: hooksService,
CacheService: cacheService,
SQLStore: sqlStore,
AlertEngine: alertEngine,
PluginRequestValidator: pluginRequestValidator,
pluginClient: pluginClient,
pluginStore: pluginStore,
pluginStaticRouteResolver: pluginStaticRouteResolver,
pluginDashboardManager: pluginDashboardManager,
pluginErrorResolver: pluginErrorResolver,
updateChecker: updateChecker,
SettingsProvider: settingsProvider,
DataSourceCache: dataSourceCache,
AuthTokenService: userTokenService,
cleanUpService: cleanUpService,
ShortURLService: shortURLService,
RemoteCacheService: remoteCache,
ProvisioningService: provisioningService,
Login: loginService,
AccessControl: accessControl,
DataProxy: dataSourceProxy,
SearchService: searchService,
Live: live,
LivePushGateway: livePushGateway,
PluginContextProvider: plugCtxProvider,
ContextHandler: contextHandler,
LoadSchemaService: schemaService,
AlertNG: alertNG,
LibraryPanelService: libraryPanelService,
LibraryElementService: libraryElementService,
QuotaService: quotaService,
notificationService: notificationService,
tracingService: tracingService,
internalMetricsSvc: internalMetricsSvc,
log: log.New("http.server"),
web: m,
Listener: opts.Listener,
SocialService: socialService,
OAuthTokenService: oauthTokenService,
EncryptionService: encryptionService,
2021-11-04 18:47:21 +02:00
SecretsService: secretsService,
DataSourcesService: dataSourcesService,
searchUsersService: searchUsersService,
expressionService: expressionService,
}
if hs.Listener != nil {
hs.log.Debug("Using provided listener")
}
2018-07-01 16:01:43 +02:00
hs.registerRoutes()
if err := hs.declareFixedRoles(); err != nil {
return nil, err
}
return hs, nil
2016-12-21 14:36:32 +01:00
}
func (hs *HTTPServer) AddMiddleware(middleware web.Handler) {
hs.middlewares = append(hs.middlewares, middleware)
}
2018-05-01 15:51:15 +02:00
func (hs *HTTPServer) Run(ctx context.Context) error {
2016-12-21 14:36:32 +01:00
hs.context = ctx
2018-07-01 16:01:43 +02:00
hs.applyRoutes()
2016-12-21 14:36:32 +01:00
// Remove any square brackets enclosing IPv6 addresses, a format we support for backwards compatibility
host := strings.TrimSuffix(strings.TrimPrefix(hs.Cfg.HTTPAddr, "["), "]")
hs.httpSrv = &http.Server{
Addr: net.JoinHostPort(host, hs.Cfg.HTTPPort),
Handler: hs.web,
ReadTimeout: hs.Cfg.ReadTimeout,
}
switch hs.Cfg.Protocol {
case setting.HTTP2Scheme:
if err := hs.configureHttp2(); err != nil {
return err
2019-08-16 16:06:54 +01:00
}
case setting.HTTPSScheme:
if err := hs.configureHttps(); err != nil {
return err
}
2020-12-01 09:53:27 +01:00
default:
}
listener, err := hs.getListener()
if err != nil {
return err
}
2018-03-22 14:14:44 +01:00
hs.log.Info("HTTP Server Listen", "address", listener.Addr().String(), "protocol",
hs.Cfg.Protocol, "subUrl", hs.Cfg.AppSubURL, "socket", hs.Cfg.SocketPath)
var wg sync.WaitGroup
wg.Add(1)
// handle http shutdown on server context done
go func() {
defer wg.Done()
<-ctx.Done()
if err := hs.httpSrv.Shutdown(context.Background()); err != nil {
hs.log.Error("Failed to shutdown server", "error", err)
}
}()
switch hs.Cfg.Protocol {
case setting.HTTPScheme, setting.SocketScheme:
if err := hs.httpSrv.Serve(listener); err != nil {
2020-11-19 13:34:28 +01:00
if errors.Is(err, http.ErrServerClosed) {
hs.log.Debug("server was shutdown gracefully")
return nil
}
return err
}
case setting.HTTP2Scheme, setting.HTTPSScheme:
if err := hs.httpSrv.ServeTLS(listener, hs.Cfg.CertFile, hs.Cfg.KeyFile); err != nil {
2020-11-19 13:34:28 +01:00
if errors.Is(err, http.ErrServerClosed) {
hs.log.Debug("server was shutdown gracefully")
return nil
}
return err
}
2016-12-21 14:36:32 +01:00
default:
panic(fmt.Sprintf("Unhandled protocol %q", hs.Cfg.Protocol))
2016-12-21 14:36:32 +01:00
}
wg.Wait()
return nil
2016-12-21 14:36:32 +01:00
}
func (hs *HTTPServer) getListener() (net.Listener, error) {
if hs.Listener != nil {
return hs.Listener, nil
}
switch hs.Cfg.Protocol {
case setting.HTTPScheme, setting.HTTPSScheme, setting.HTTP2Scheme:
listener, err := net.Listen("tcp", hs.httpSrv.Addr)
if err != nil {
return nil, errutil.Wrapf(err, "failed to open listener on address %s", hs.httpSrv.Addr)
}
return listener, nil
case setting.SocketScheme:
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: hs.Cfg.SocketPath, Net: "unix"})
if err != nil {
return nil, errutil.Wrapf(err, "failed to open listener for socket %s", hs.Cfg.SocketPath)
}
// Make socket writable by group
// nolint:gosec
if err := os.Chmod(hs.Cfg.SocketPath, 0660); err != nil {
return nil, errutil.Wrapf(err, "failed to change socket permissions")
}
return listener, nil
default:
hs.log.Error("Invalid protocol", "protocol", hs.Cfg.Protocol)
return nil, fmt.Errorf("invalid protocol %q", hs.Cfg.Protocol)
}
}
func (hs *HTTPServer) configureHttps() error {
if hs.Cfg.CertFile == "" {
2016-12-21 14:36:32 +01:00
return fmt.Errorf("cert_file cannot be empty when using HTTPS")
}
if hs.Cfg.KeyFile == "" {
2016-12-21 14:36:32 +01:00
return fmt.Errorf("cert_key cannot be empty when using HTTPS")
}
if _, err := os.Stat(hs.Cfg.CertFile); os.IsNotExist(err) {
return fmt.Errorf(`cannot find SSL cert_file at %q`, hs.Cfg.CertFile)
2016-12-21 14:36:32 +01:00
}
if _, err := os.Stat(hs.Cfg.KeyFile); os.IsNotExist(err) {
return fmt.Errorf(`cannot find SSL key_file at %q`, hs.Cfg.KeyFile)
2016-12-21 14:36:32 +01:00
}
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS12,
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
}
2017-06-17 23:10:12 +02:00
hs.httpSrv.TLSConfig = tlsCfg
2018-04-16 20:22:12 +02:00
hs.httpSrv.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
2017-06-17 23:10:12 +02:00
return nil
2016-12-21 14:36:32 +01:00
}
func (hs *HTTPServer) configureHttp2() error {
if hs.Cfg.CertFile == "" {
2019-08-16 16:06:54 +01:00
return fmt.Errorf("cert_file cannot be empty when using HTTP2")
}
if hs.Cfg.KeyFile == "" {
2019-08-16 16:06:54 +01:00
return fmt.Errorf("cert_key cannot be empty when using HTTP2")
}
if _, err := os.Stat(hs.Cfg.CertFile); os.IsNotExist(err) {
return fmt.Errorf(`cannot find SSL cert_file at %q`, hs.Cfg.CertFile)
2019-08-16 16:06:54 +01:00
}
if _, err := os.Stat(hs.Cfg.KeyFile); os.IsNotExist(err) {
return fmt.Errorf(`cannot find SSL key_file at %q`, hs.Cfg.KeyFile)
2019-08-16 16:06:54 +01:00
}
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS12,
2020-11-25 14:56:34 +01:00
PreferServerCipherSuites: true,
2019-08-16 16:06:54 +01:00
CipherSuites: []uint16{
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
2019-08-16 16:06:54 +01:00
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
2019-08-16 16:06:54 +01:00
},
NextProtos: []string{"h2", "http/1.1"},
}
hs.httpSrv.TLSConfig = tlsCfg
return nil
2019-08-16 16:06:54 +01:00
}
func (hs *HTTPServer) applyRoutes() {
// start with middlewares & static routes
hs.addMiddlewaresAndStaticRoutes()
// then add view routes & api routes
hs.RouteRegister.Register(hs.web)
// then custom app proxy routes
hs.initAppPluginRoutes(hs.web)
// lastly not found route
hs.web.NotFound(middleware.ReqSignedIn, hs.NotFoundHandler)
}
func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m := hs.web
m.Use(middleware.RequestTracing())
m.Use(middleware.Logger(hs.Cfg))
2016-12-21 14:36:32 +01:00
if hs.Cfg.EnableGzip {
m.UseMiddleware(middleware.Gziper())
2016-12-21 14:36:32 +01:00
}
m.Use(middleware.Recovery(hs.Cfg))
hs.mapStatic(m, hs.Cfg.StaticRootPath, "build", "public/build")
hs.mapStatic(m, hs.Cfg.StaticRootPath, "", "public")
hs.mapStatic(m, hs.Cfg.StaticRootPath, "robots.txt", "robots.txt")
2016-12-21 14:36:32 +01:00
if hs.Cfg.ImageUploadProvider == "local" {
2018-05-24 15:26:27 +02:00
hs.mapStatic(m, hs.Cfg.ImagesDir, "", "/public/img/attachments")
}
m.Use(middleware.AddDefaultResponseHeaders(hs.Cfg))
if hs.Cfg.ServeFromSubPath && hs.Cfg.AppSubURL != "" {
m.SetURLPrefix(hs.Cfg.AppSubURL)
}
m.UseMiddleware(web.Renderer(filepath.Join(hs.Cfg.StaticRootPath, "views"), "[[", "]]"))
2016-12-21 14:36:32 +01:00
// These endpoints are used for monitoring the Grafana instance
// and should not be redirected or rejected.
m.Use(hs.healthzHandler)
m.Use(hs.apiHealthHandler)
2017-09-06 22:24:10 +02:00
m.Use(hs.metricsEndpoint)
m.Use(hs.ContextHandler.Middleware)
2020-12-15 19:09:04 +01:00
m.Use(middleware.OrgRedirect(hs.Cfg))
2016-12-21 14:36:32 +01:00
// needs to be after context handler
if hs.Cfg.EnforceDomain {
2020-12-15 19:09:04 +01:00
m.Use(middleware.ValidateHostHeader(hs.Cfg))
2016-12-21 14:36:32 +01:00
}
2021-01-12 07:42:32 +01:00
m.Use(middleware.HandleNoCacheHeader)
2021-08-10 09:03:22 +02:00
m.UseMiddleware(middleware.AddCSPHeader(hs.Cfg, hs.log))
for _, mw := range hs.middlewares {
m.Use(mw)
}
2016-12-21 14:36:32 +01:00
}
func (hs *HTTPServer) metricsEndpoint(ctx *web.Context) {
if !hs.Cfg.MetricsEndpointEnabled {
return
}
2019-06-12 07:27:47 +02:00
if ctx.Req.Method != http.MethodGet || ctx.Req.URL.Path != "/metrics" {
2017-09-06 22:24:10 +02:00
return
}
if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) {
2018-11-14 17:37:32 -05:00
ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
promhttp.
HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true}).
ServeHTTP(ctx.Resp, ctx.Req)
2017-09-06 22:24:10 +02:00
}
2020-09-18 13:03:18 +02:00
// healthzHandler always return 200 - Ok if Grafana's web server is running
func (hs *HTTPServer) healthzHandler(ctx *web.Context) {
notHeadOrGet := ctx.Req.Method != http.MethodGet && ctx.Req.Method != http.MethodHead
if notHeadOrGet || ctx.Req.URL.Path != "/healthz" {
return
}
ctx.Resp.WriteHeader(200)
2020-09-18 13:03:18 +02:00
_, err := ctx.Resp.Write([]byte("Ok"))
if err != nil {
hs.log.Error("could not write to response", "err", err)
}
2020-09-18 13:03:18 +02:00
}
2020-09-18 13:03:18 +02:00
// apiHealthHandler will return ok if Grafana's web server is running and it
// can access the database. If the database cannot be accessed it will return
2020-09-18 13:03:18 +02:00
// http status code 503.
func (hs *HTTPServer) apiHealthHandler(ctx *web.Context) {
notHeadOrGet := ctx.Req.Method != http.MethodGet && ctx.Req.Method != http.MethodHead
if notHeadOrGet || ctx.Req.URL.Path != "/api/health" {
return
}
data := simplejson.New()
data.Set("database", "ok")
if !hs.Cfg.AnonymousHideVersion {
data.Set("version", hs.Cfg.BuildVersion)
data.Set("commit", hs.Cfg.BuildCommit)
}
if !hs.databaseHealthy(ctx.Req.Context()) {
data.Set("database", "failing")
ctx.Resp.Header().Set("Content-Type", "application/json; charset=UTF-8")
ctx.Resp.WriteHeader(503)
} else {
ctx.Resp.Header().Set("Content-Type", "application/json; charset=UTF-8")
ctx.Resp.WriteHeader(200)
}
2020-08-18 14:58:08 +02:00
dataBytes, err := data.EncodePretty()
if err != nil {
hs.log.Error("Failed to encode data", "err", err)
return
}
2019-10-08 18:57:53 +02:00
if _, err := ctx.Resp.Write(dataBytes); err != nil {
hs.log.Error("Failed to write to response", "err", err)
}
}
func (hs *HTTPServer) mapStatic(m *web.Mux, rootDir string, dir string, prefix string) {
headers := func(c *web.Context) {
2016-12-21 14:36:32 +01:00
c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
}
if prefix == "public/build" {
headers = func(c *web.Context) {
c.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
}
}
if hs.Cfg.Env == setting.Dev {
headers = func(c *web.Context) {
2016-12-21 14:36:32 +01:00
c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")
}
}
m.Use(httpstatic.Static(
path.Join(rootDir, dir),
httpstatic.StaticOptions{
SkipLogging: true,
Prefix: prefix,
AddHeaders: headers,
},
))
}
func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool {
return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != ""
}