From a8aa6b74a8ec34896958ec3964b08fd53c92fe82 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 24 Apr 2025 13:14:21 +0100 Subject: [PATCH] FEMT: Basic frontend-service implementation (#104229) * create the most basic frontend-server module * expose prom metrics?? * add todo list * move frontend-service to its own folder in services * check error from writer.Write * reword comment, add launch config --- .github/CODEOWNERS | 1 + .vscode/launch.json | 10 ++ pkg/modules/dependencies.go | 2 + pkg/server/module_server.go | 7 +- pkg/services/frontend/frontend_service.go | 109 ++++++++++++++++++++++ 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 pkg/services/frontend/frontend_service.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 23e9fe99a3f..c8fdc572b60 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -138,6 +138,7 @@ /pkg/services/dashboardversion/ @grafana/grafana-backend-group /pkg/services/encryption/ @grafana/grafana-operator-experience-squad /pkg/services/folder/ @grafana/grafana-search-and-storage +/pkg/services/frontend/ @grafana/grafana-frontend-platform /pkg/services/apiserver @grafana/grafana-app-platform-squad /pkg/services/hooks/ @grafana/grafana-backend-group /pkg/services/kmsproviders/ @grafana/grafana-operator-experience-squad diff --git a/.vscode/launch.json b/.vscode/launch.json index 50393b02272..88a99c05737 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -82,6 +82,16 @@ "cwd": "${workspaceFolder}", "args": ["server", "target", "--homepath", "${workspaceFolder}", "--packaging", "dev"] }, + { + "name": "Run Frontend Server", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/pkg/cmd/grafana/", + "cwd": "${workspaceFolder}", + "env": { "GF_DEFAULT_TARGET": "frontend-server", "GF_SERVER_HTTP_PORT": "3003" }, + "args": ["server", "target", "--homepath", "${workspaceFolder}", "--packaging", "dev"] + }, { "name": "Attach to Chrome", "port": 9222, diff --git a/pkg/modules/dependencies.go b/pkg/modules/dependencies.go index f18b5f2c15f..7645386d823 100644 --- a/pkg/modules/dependencies.go +++ b/pkg/modules/dependencies.go @@ -9,6 +9,7 @@ const ( StorageServer string = "storage-server" ZanzanaServer string = "zanzana-server" InstrumentationServer string = "instrumentation-server" + FrontendServer string = "frontend-server" ) var dependencyMap = map[string][]string{ @@ -17,4 +18,5 @@ var dependencyMap = map[string][]string{ ZanzanaServer: {InstrumentationServer}, Core: {}, All: {Core}, + FrontendServer: {}, } diff --git a/pkg/server/module_server.go b/pkg/server/module_server.go index 11db7c204d3..7142b4b8c63 100644 --- a/pkg/server/module_server.go +++ b/pkg/server/module_server.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/modules" "github.com/grafana/grafana/pkg/services/authz" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/frontend" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/storage/unified/sql" @@ -120,7 +121,7 @@ func (s *ModuleServer) Run() error { // only run the instrumentation server module if were not running a module that already contains an http server m.RegisterInvisibleModule(modules.InstrumentationServer, func() (services.Service, error) { - if m.IsModuleEnabled(modules.All) || m.IsModuleEnabled(modules.Core) { + if m.IsModuleEnabled(modules.All) || m.IsModuleEnabled(modules.Core) || m.IsModuleEnabled(modules.FrontendServer) { return services.NewBasicService(nil, nil, nil).WithName(modules.InstrumentationServer), nil } return NewInstrumentationService(s.log, s.cfg, s.promGatherer) @@ -151,6 +152,10 @@ func (s *ModuleServer) Run() error { return authz.ProvideZanzanaService(s.cfg, s.features) }) + m.RegisterModule(modules.FrontendServer, func() (services.Service, error) { + return frontend.ProvideFrontendService(s.cfg, s.promGatherer) + }) + m.RegisterModule(modules.All, nil) return m.Run(s.context) diff --git a/pkg/services/frontend/frontend_service.go b/pkg/services/frontend/frontend_service.go new file mode 100644 index 00000000000..3658d4097c0 --- /dev/null +++ b/pkg/services/frontend/frontend_service.go @@ -0,0 +1,109 @@ +package frontend + +import ( + "context" + "net" + "net/http" + "time" + + "github.com/grafana/dskit/services" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type frontendService struct { + *services.BasicService + cfg *setting.Cfg + httpServ *http.Server + log log.Logger + errChan chan error + promGatherer prometheus.Gatherer +} + +func ProvideFrontendService(cfg *setting.Cfg, promGatherer prometheus.Gatherer) (*frontendService, error) { + s := &frontendService{ + cfg: cfg, + log: log.New("frontend-server"), + promGatherer: promGatherer, + } + 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) + + router := http.NewServeMux() + router.Handle("/metrics", promhttp.HandlerFor(s.promGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true})) + router.HandleFunc("/", s.handleRequest) + + 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: router, + BaseContext: func(_ net.Listener) context.Context { return ctx }, + } + + return server +} + +func (s *frontendService) handleRequest(writer http.ResponseWriter, request *http.Request) { + // This should: + // - get correct asset urls from fs or cdn + // - generate a nonce + // - render them into the index.html + // - and return it to the user! + + s.log.Info("handling request", "method", request.Method, "url", request.URL.String()) + htmlContent := ` + + + Grafana Frontend Server + + + +

Grafana Frontend Server

+

This is a simple static HTML page served by the Grafana frontend server module.

+ +` + + writer.Header().Set("Content-Type", "text/html; charset=utf-8") + _, err := writer.Write([]byte(htmlContent)) + if err != nil { + s.log.Error("could not write to response", "err", err) + } +}