FEMT: Add feature toggle and expose the service in regular grafana (#104428)

This commit is contained in:
Ryan McKinley
2025-04-25 12:24:25 +03:00
committed by GitHub
parent 29b3738bc8
commit 7b492d7e16
11 changed files with 225 additions and 44 deletions
+7 -1
View File
@@ -1738,13 +1738,19 @@ var (
FrontendOnly: true,
},
{
Name: "pluginsAutoUpdate",
Description: "Enables auto-updating of users installed plugins",
Stage: FeatureStageExperimental,
FrontendOnly: false,
Owner: grafanaPluginsPlatformSquad,
},
{
Name: "multiTenantFrontend",
Description: "Register MT frontend",
Stage: FeatureStageExperimental,
FrontendOnly: false,
Owner: grafanaFrontendPlatformSquad,
},
{
Name: "alertingListViewV2PreviewToggle",
Description: "Enables the alerting list view v2 preview toggle",
+1
View File
@@ -228,6 +228,7 @@ unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true
logsPanelControls,preview,@grafana/observability-logs,false,false,true
metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true
pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false
multiTenantFrontend,experimental,@grafana/grafana-frontend-platform,false,false,false
alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true
alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false
alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
228 logsPanelControls preview @grafana/observability-logs false false true
229 metricsFromProfiles experimental @grafana/observability-traces-and-profiling false false true
230 pluginsAutoUpdate experimental @grafana/plugins-platform-backend false false false
231 multiTenantFrontend experimental @grafana/grafana-frontend-platform false false false
232 alertingListViewV2PreviewToggle privatePreview @grafana/alerting-squad false false true
233 alertRuleUseFiredAtForStartsAt experimental @grafana/alerting-squad false false false
234 alertingBulkActionsInUI GA @grafana/alerting-squad false false true
+4
View File
@@ -923,6 +923,10 @@ const (
// Enables auto-updating of users installed plugins
FlagPluginsAutoUpdate = "pluginsAutoUpdate"
// FlagMultiTenantFrontend
// Register MT frontend
FlagMultiTenantFrontend = "multiTenantFrontend"
// FlagAlertingListViewV2PreviewToggle
// Enables the alerting list view v2 preview toggle
FlagAlertingListViewV2PreviewToggle = "alertingListViewV2PreviewToggle"
+25
View File
@@ -1976,6 +1976,18 @@
"codeowner": "@grafana/alerting-squad"
}
},
{
"metadata": {
"name": "multiTenantFrontend",
"resourceVersion": "1745438197175",
"creationTimestamp": "2025-04-23T19:56:37Z"
},
"spec": {
"description": "Register MT frontend",
"stage": "experimental",
"codeowner": "@grafana/grafana-frontend-platform"
}
},
{
"metadata": {
"name": "multiTenantTempCredentials",
@@ -1989,6 +2001,19 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "multitenantFrontend",
"resourceVersion": "1745438122785",
"creationTimestamp": "2025-04-23T19:55:22Z",
"deletionTimestamp": "2025-04-23T19:56:37Z"
},
"spec": {
"description": "Register MT frontend",
"stage": "experimental",
"codeowner": "@grafana/grafana-frontend-platform"
}
},
{
"metadata": {
"name": "mysqlAnsiQuotes",
+15 -36
View File
@@ -6,11 +6,13 @@ import (
"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"
"github.com/grafana/dskit/services"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/setting"
)
type frontendService struct {
@@ -20,13 +22,21 @@ type frontendService struct {
log log.Logger
errChan chan error
promGatherer prometheus.Gatherer
index *IndexProvider
}
func ProvideFrontendService(cfg *setting.Cfg, promGatherer prometheus.Gatherer) (*frontendService, error) {
func ProvideFrontendService(cfg *setting.Cfg, promGatherer prometheus.Gatherer, license licensing.Licensing) (*frontendService, error) {
index, err := NewIndexProvider(cfg, license)
if err != nil {
return nil, err
}
s := &frontendService{
cfg: cfg,
log: log.New("frontend-server"),
promGatherer: promGatherer,
index: index,
}
s.BasicService = services.NewBasicService(s.start, s.running, s.stop)
return s, nil
@@ -64,7 +74,7 @@ func (s *frontendService) newFrontendServer(ctx context.Context) *http.Server {
router := http.NewServeMux()
router.Handle("/metrics", promhttp.HandlerFor(s.promGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true}))
router.HandleFunc("/", s.handleRequest)
router.HandleFunc("/", s.index.HandleRequest)
server := &http.Server{
// 5s timeout for header reads to avoid Slowloris attacks (https://thetooth.io/blog/slowloris-attack/)
@@ -76,34 +86,3 @@ func (s *frontendService) newFrontendServer(ctx context.Context) *http.Server {
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 := `<!DOCTYPE html>
<html>
<head>
<title>Grafana Frontend Server</title>
<style>
body {
font-family: sans-serif;
}
</style>
</head>
<body>
<h1>Grafana Frontend Server</h1>
<p>This is a simple static HTML page served by the Grafana frontend server module.</p>
</body>
</html>`
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)
}
}
+108
View File
@@ -0,0 +1,108 @@
package frontend
import (
"context"
"embed"
"errors"
"fmt"
"html/template"
"net/http"
"syscall"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/webassets"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/setting"
)
type IndexProvider struct {
log logging.Logger
index *template.Template
data IndexViewData
}
type IndexViewData struct {
CSPContent string
CSPEnabled bool
IsDevelopmentEnv bool
AppSubUrl string
BuildVersion string
BuildCommit string
AppTitle string
Assets *dtos.EntryPointAssets // Includes CDN info
// Nonce is a cryptographic identifier for use with Content Security Policy.
Nonce string
}
// Templates setup.
var (
//go:embed *.html
templatesFS embed.FS
// templates
htmlTemplates = template.Must(template.New("html").Delims("[[", "]]").ParseFS(templatesFS, `*.html`))
)
func NewIndexProvider(cfg *setting.Cfg, license licensing.Licensing) (*IndexProvider, error) {
assets, err := webassets.GetWebAssets(context.Background(), cfg, license)
if err != nil {
return nil, err
}
t := htmlTemplates.Lookup("index.html")
if t == nil {
return nil, fmt.Errorf("missing index template")
}
return &IndexProvider{
log: logging.DefaultLogger.With("logger", "index-provider"),
index: t,
data: IndexViewData{
AppTitle: "Grafana",
AppSubUrl: cfg.AppSubURL, // Based on the request?
BuildVersion: cfg.BuildVersion,
BuildCommit: cfg.BuildCommit,
Assets: assets,
CSPEnabled: cfg.CSPEnabled,
CSPContent: cfg.CSPTemplate,
IsDevelopmentEnv: cfg.Env == setting.Dev,
},
}, nil
}
func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.Request) {
if request.Method != "GET" {
writer.WriteHeader(http.StatusMethodNotAllowed)
return
}
nonce, err := middleware.GenerateNonce()
if err != nil {
p.log.Error("error creating nonce", "err", err)
writer.WriteHeader(500)
return
}
// TODO -- restructure so the static stuff is under one variable and the rest is dynamic
data := p.data // copy everything
data.Nonce = nonce
if data.CSPEnabled {
data.CSPContent = middleware.ReplacePolicyVariables(p.data.CSPContent, p.data.AppSubUrl, data.Nonce)
}
writer.Header().Set("Content-Type", "text/html; charset=UTF-8")
writer.WriteHeader(200)
if err := p.index.Execute(writer, &data); err != nil {
if errors.Is(err, syscall.EPIPE) { // Client has stopped listening.
return
}
panic(fmt.Sprintf("Error rendering index\n %s", err.Error()))
}
}
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
[[ if and .CSPEnabled .IsDevelopmentEnv ]]
<!-- Cypress overwrites CSP headers in HTTP requests, so this is required for e2e tests-->
<meta http-equiv="Content-Security-Policy" content="[[.CSPContent]]"/>
[[ end ]]
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#000" />
<title>[[.AppTitle]]</title>
<base href="[[.AppSubUrl]]/" />
<link rel="mask-icon" href="[[.Assets.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28" />
[[range $asset := .Assets.CSSFiles]]
<link rel="stylesheet" href="[[$asset.FilePath]]" />
[[end]]
<script nonce="[[.Nonce]]">
performance.mark('frontend_boot_css_time_seconds');
</script>
</head>
<body>
<h1>Grafana Frontend Server ([[.BuildVersion]])</h1>
<p>This is a simple static HTML page served by the Grafana frontend server module.</p>
</body>
</html>