Frontend service: Add /-/fe-boot-error endpoint to track errors starting the frontend service (#112375)

* add /-/fe-boot-error endpoint to track errors starting the frontend service

* use promauto package

* move metric to root

* check error of r.Body.Close()
This commit is contained in:
Ashley Harrison
2025-10-15 13:35:24 +01:00
committed by GitHub
parent 6baa257f91
commit d3cd401734
2 changed files with 62 additions and 2 deletions
+47
View File
@@ -2,11 +2,13 @@ package frontend
import (
"context"
"io"
"net"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
@@ -25,6 +27,14 @@ import (
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/frontend")
// Initialize metrics
var bootErrorMetric = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "grafana",
Subsystem: "frontend",
Name: "boot_errors_total",
Help: "Total number of frontend boot errors",
})
type frontendService struct {
*services.BasicService
cfg *setting.Cfg
@@ -117,6 +127,11 @@ func (s *frontendService) routeGet(m *web.Mux, pattern string, h ...web.Handler)
m.Get(pattern, handlers...)
}
func (s *frontendService) routePost(m *web.Mux, pattern string, h ...web.Handler) {
handlers := append([]web.Handler{middleware.ProvideRouteOperationName(pattern)}, h...)
m.Post(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)
@@ -148,6 +163,38 @@ func (s *frontendService) registerRoutes(m *web.Mux) {
}
})
// Frontend boot error reporting endpoint
s.routePost(m, "/-/fe-boot-error", s.handleBootError)
// All other requests return index.html
s.routeGet(m, "/*", s.index.HandleRequest)
}
// handleBootError handles frontend boot error reports
func (s *frontendService) handleBootError(w http.ResponseWriter, r *http.Request) {
// Read the request body
body, err := io.ReadAll(r.Body)
if err != nil {
s.log.Error("failed to read boot error request body", "error", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
defer func() {
if err := r.Body.Close(); err != nil {
s.log.Warn("Failed to close response body", "err", err)
}
}()
// Increment the Prometheus counter
bootErrorMetric.Inc()
// Log the error details
s.log.Error("frontend boot error reported", "error", body)
// Return success response
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte("OK")); err != nil {
s.log.Error("failed to write boot error response", "error", err)
}
}
+15 -2
View File
@@ -169,10 +169,23 @@
// Wrap in an IIFE to avoid polluting the global scope. Intentionally global-scope properties
// are explicitly assigned to the `window` object.
(() => {
window.__grafana_load_failed = function(...args) {
console.error('Failed to load Grafana', ...args);
window.__grafana_load_failed = function(err) {
console.error('Failed to load Grafana', err);
document.querySelector('.fs-variant-loader').classList.add('fs-hidden');
document.querySelector('.fs-variant-error').classList.remove('fs-hidden');
// Report the error to the backend
const errorMessage = err ? err.message : 'Unknown error';
fetch('/-/fe-boot-error', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: errorMessage,
}).catch(err => {
console.error('Failed to report boot error to backend: ', err);
});
};
window.onload = function() {