diff --git a/devenv/docker/blocks/prometheus2/docker-compose.yaml b/devenv/docker/blocks/prometheus2/docker-compose.yaml index e1b8a0a4d6f..0b1834a8e00 100644 --- a/devenv/docker/blocks/prometheus2/docker-compose.yaml +++ b/devenv/docker/blocks/prometheus2/docker-compose.yaml @@ -2,6 +2,8 @@ build: docker/blocks/prometheus2 ports: - "9090:9090" + extra_hosts: + - "host.docker.internal:host-gateway" node_exporter: image: prom/node-exporter diff --git a/devenv/docker/blocks/prometheus2/prometheus.yml b/devenv/docker/blocks/prometheus2/prometheus.yml index 494736ea0f7..5fe6e9ac988 100644 --- a/devenv/docker/blocks/prometheus2/prometheus.yml +++ b/devenv/docker/blocks/prometheus2/prometheus.yml @@ -32,7 +32,7 @@ scrape_configs: - job_name: 'grafana' static_configs: - - targets: ['grafana:3000'] + - targets: ['host.docker.internal:3000'] - job_name: 'prometheus-random-data' static_configs: diff --git a/pkg/api/api.go b/pkg/api/api.go index 5b65fbf2dd6..14f9ad81453 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/api/frontendlogging" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" ) @@ -397,6 +398,8 @@ func (hs *HTTPServer) registerRoutes() { annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), routing.Wrap(PostGraphiteAnnotation)) }) + apiRoute.Post("/frontend-metrics", bind(metrics.PostFrontendMetricsCommand{}), routing.Wrap(hs.PostFrontendMetrics)) + // short urls apiRoute.Post("/short-urls", bind(dtos.CreateShortURLCmd{}), routing.Wrap(hs.createShortURL)) }, reqSignedIn) diff --git a/pkg/api/frontend_metrics.go b/pkg/api/frontend_metrics.go new file mode 100644 index 00000000000..68f760fd02a --- /dev/null +++ b/pkg/api/frontend_metrics.go @@ -0,0 +1,21 @@ +package api + +import ( + "strings" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/models" +) + +func (hs *HTTPServer) PostFrontendMetrics(c *models.ReqContext, cmd metrics.PostFrontendMetricsCommand) response.Response { + for _, event := range cmd.Events { + name := strings.Replace(event.Name, "-", "_", -1) + if recorder, ok := metrics.FrontendMetrics[name]; ok { + recorder(event) + } else { + c.Logger.Debug("Received unknown frontend metric", "metric", name) + } + } + return response.Empty(200) +} diff --git a/pkg/infra/metrics/frontendmetrics.go b/pkg/infra/metrics/frontendmetrics.go new file mode 100644 index 00000000000..885e22e5d02 --- /dev/null +++ b/pkg/infra/metrics/frontendmetrics.go @@ -0,0 +1,43 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +// PostPostFrontendMetricsCommand sent by frontend to record frontend metrics +type PostFrontendMetricsCommand struct { + Events []FrontendMetricEvent `json:"events"` +} + +// FrontendMetricEvent a single metric measurement event +type FrontendMetricEvent struct { + Name string `json:"name"` + Value float64 `json:"value"` +} + +// FrontendMetricsRecorder handles the recording of the event, ie passes it to a prometheus metric +type FrontendMetricsRecorder func(event FrontendMetricEvent) + +// FrontendMetrics contains all the valid frontend metrics and a handler function for recording events +var FrontendMetrics map[string]FrontendMetricsRecorder = map[string]FrontendMetricsRecorder{} + +func registerFrontendHistogram(name string, help string) { + defBuckets := []float64{.1, .25, .5, 1, 1.5, 2, 5, 10, 20, 40} + + histogram := prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: name, + Help: help, + Buckets: defBuckets, + Namespace: ExporterName, + }) + + FrontendMetrics[name] = func(event FrontendMetricEvent) { + histogram.Observe(event.Value) + } + + prometheus.MustRegister(histogram) +} + +func initFrontendMetrics() { + registerFrontendHistogram("frontend_boot_load_time_seconds", "Frontend boot time measurement") + registerFrontendHistogram("frontend_boot_first_paint_time_seconds", "Frontend boot first paint") + registerFrontendHistogram("frontend_boot_js_done_time_seconds", "Frontend boot initial js load") +} diff --git a/pkg/infra/metrics/service.go b/pkg/infra/metrics/service.go index ded52dfc00b..eff4949261c 100644 --- a/pkg/infra/metrics/service.go +++ b/pkg/infra/metrics/service.go @@ -22,6 +22,7 @@ func (lw *logWrapper) Println(v ...interface{}) { func init() { registry.RegisterService(&InternalMetricsService{}) initMetricVars() + initFrontendMetrics() } type InternalMetricsService struct { diff --git a/public/app/app.ts b/public/app/app.ts index f7ee13e5ac4..c51a353541e 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -192,17 +192,28 @@ export class GrafanaApp { initEchoSrv() { setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' })); - ttiPolyfill.getFirstConsistentlyInteractive().then((tti: any) => { - // Collecting paint metrics first - const paintMetrics = performance && performance.getEntriesByType ? performance.getEntriesByType('paint') : []; + window.addEventListener('load', (e) => { + const loadMetricName = 'frontend_boot_load_time_seconds'; - for (const metric of paintMetrics) { - reportPerformance(metric.name, Math.round(metric.startTime + metric.duration)); + if (performance && performance.getEntriesByType) { + performance.mark(loadMetricName); + + const paintMetrics = performance.getEntriesByType('paint'); + + for (const metric of paintMetrics) { + reportPerformance( + `frontend_boot_${metric.name}_time_seconds`, + Math.round(metric.startTime + metric.duration) / 1000 + ); + } + + const loadMetric = performance.getEntriesByName(loadMetricName)[0]; + reportPerformance(loadMetric.name, Math.round(loadMetric.startTime + loadMetric.duration) / 1000); } - reportPerformance('tti', tti); }); registerEchoBackend(new PerformanceBackend({})); + if (config.sentry.enabled) { registerEchoBackend( new SentryEchoBackend({ diff --git a/public/app/core/services/echo/EchoSrv.ts b/public/app/core/services/echo/EchoSrv.ts index e6d793f7a2c..1f230eca4a5 100644 --- a/public/app/core/services/echo/EchoSrv.ts +++ b/public/app/core/services/echo/EchoSrv.ts @@ -6,8 +6,8 @@ export const reportPerformance = (metric: string, value: number) => { getEchoSrv().addEvent({ type: EchoEventType.Performance, payload: { - metricName: metric, - duration: value, + name: metric, + value: value, }, }); }; diff --git a/public/app/core/services/echo/backends/PerformanceBackend.ts b/public/app/core/services/echo/backends/PerformanceBackend.ts index 69ae40e2a16..a610f17e41e 100644 --- a/public/app/core/services/echo/backends/PerformanceBackend.ts +++ b/public/app/core/services/echo/backends/PerformanceBackend.ts @@ -1,8 +1,9 @@ import { EchoBackend, EchoEvent, EchoEventType } from '@grafana/runtime'; +import { backendSrv } from '../../backend_srv'; export interface PerformanceEventPayload { - metricName: string; - duration: number; + name: string; + value: number; } export interface PerformanceEvent extends EchoEvent {} @@ -16,13 +17,13 @@ export interface PerformanceBackendOptions { * Reports performance metrics to given url (TODO) */ export class PerformanceBackend implements EchoBackend { - private buffer: PerformanceEvent[] = []; + private buffer: PerformanceEventPayload[] = []; supportedEvents = [EchoEventType.Performance]; constructor(public options: PerformanceBackendOptions) {} addEvent = (e: EchoEvent) => { - this.buffer.push(e); + this.buffer.push(e.payload); }; flush = () => { @@ -30,20 +31,17 @@ export class PerformanceBackend implements EchoBackend + @@ -296,7 +297,7 @@ <% } %> <% } %> \ No newline at end of file