Files
grafana/pkg/services/frontend/frontend_service.go
Josh Hunt 1bd9541507 FrontendService: Add tracing and logging middleware (#107956)
* FrontendService: Add tracing and logging middleware

* tests!

* middleware tests

* context middleware test

* revert http_server back to previous version

* fix lint

* fix test

* use http.NotFound instead of custom http handler

* use existing tracer for package

* use otel/trace.Tracer in request_tracing middleware

* tidy up tracing in contextMiddleware

* fix 404 test

* remove spans from contextMiddleware

* comment
2025-07-22 18:55:44 +01:00

138 lines
4.0 KiB
Go

package frontend
import (
"context"
"net"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/dskit/services"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/middleware/loggermw"
"github.com/grafana/grafana/pkg/middleware/requestmeta"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/frontend")
type frontendService struct {
*services.BasicService
cfg *setting.Cfg
httpServ *http.Server
features featuremgmt.FeatureToggles
log log.Logger
errChan chan error
promGatherer prometheus.Gatherer
promRegister prometheus.Registerer
tracer trace.Tracer
license licensing.Licensing
index *IndexProvider
}
func ProvideFrontendService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, promGatherer prometheus.Gatherer, promRegister prometheus.Registerer, license licensing.Licensing) (*frontendService, error) {
index, err := NewIndexProvider(cfg, license)
if err != nil {
return nil, err
}
s := &frontendService{
cfg: cfg,
features: features,
log: log.New("frontend-server"),
promGatherer: promGatherer,
promRegister: promRegister,
tracer: tracer,
license: license,
index: index,
}
s.BasicService = services.NewBasicService(s.start, s.running, s.stop)
return s, nil
}
func (s *frontendService) start(ctx context.Context) error {
s.httpServ = s.newFrontendServer(ctx)
s.errChan = make(chan error)
go func() {
s.errChan <- s.httpServ.ListenAndServe()
}()
return nil
}
func (s *frontendService) running(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
case err := <-s.errChan:
return err
}
}
func (s *frontendService) stop(failureReason error) error {
s.log.Info("stopping frontend server", "reason", failureReason)
if err := s.httpServ.Shutdown(context.Background()); err != nil {
s.log.Error("failed to shutdown frontend server", "error", err)
return err
}
return nil
}
func (s *frontendService) newFrontendServer(ctx context.Context) *http.Server {
s.log.Info("starting frontend server", "addr", ":"+s.cfg.HTTPPort)
// Use the same web.Mux as the main grafana server for consistency + middleware reuse
handler := web.New()
s.addMiddlewares(handler)
s.registerRoutes(handler)
server := &http.Server{
// 5s timeout for header reads to avoid Slowloris attacks (https://thetooth.io/blog/slowloris-attack/)
ReadHeaderTimeout: 5 * time.Second,
Addr: ":" + s.cfg.HTTPPort,
Handler: handler,
BaseContext: func(_ net.Listener) context.Context { return ctx },
}
return server
}
func (s *frontendService) routeGet(m *web.Mux, pattern string, h ...web.Handler) {
handlers := append([]web.Handler{middleware.ProvideRouteOperationName(pattern)}, h...)
m.Get(pattern, handlers...)
}
// Apply the same middleware patterns as the main HTTP server
func (s *frontendService) addMiddlewares(m *web.Mux) {
loggermiddleware := loggermw.Provide(s.cfg, s.features)
m.Use(requestmeta.SetupRequestMetadata())
m.UseMiddleware(s.contextMiddleware())
m.Use(middleware.RequestTracing(s.tracer, middleware.TraceAllPaths))
m.Use(middleware.RequestMetrics(s.features, s.cfg, s.promRegister))
m.UseMiddleware(loggermiddleware.Middleware())
m.UseMiddleware(middleware.Recovery(s.cfg, s.license))
}
func (s *frontendService) registerRoutes(m *web.Mux) {
s.routeGet(m, "/metrics", promhttp.HandlerFor(s.promGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true}))
// Frontend service doesn't (yet?) serve any assets, so explicitly 404
// them so we can get logs for them
s.routeGet(m, "/public/*", http.NotFound)
// All other requests return index.html
s.routeGet(m, "/*", s.index.HandleRequest)
}