mirror of
https://github.com/grafana/grafana.git
synced 2025-09-27 18:04:15 +08:00
FrontendMetrics: Adds new backend api that frontend can use to push frontend measurements and counters to prometheus (#32593)
* FrontendMetrics: Adds new backend api that frontend can use to push frontend measurements and counters to prometheus * FrontendMetrics: Adds new backend api that frontend can use to push frontend measurements and counters to prometheus * Fix naming * change to histogram * Fixed go lint
This commit is contained in:
@ -2,6 +2,8 @@
|
|||||||
build: docker/blocks/prometheus2
|
build: docker/blocks/prometheus2
|
||||||
ports:
|
ports:
|
||||||
- "9090:9090"
|
- "9090:9090"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
node_exporter:
|
node_exporter:
|
||||||
image: prom/node-exporter
|
image: prom/node-exporter
|
||||||
|
@ -32,7 +32,7 @@ scrape_configs:
|
|||||||
|
|
||||||
- job_name: 'grafana'
|
- job_name: 'grafana'
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['127.0.0.1:3000']
|
- targets: ['host.docker.internal:3000']
|
||||||
|
|
||||||
- job_name: 'prometheus-random-data'
|
- job_name: 'prometheus-random-data'
|
||||||
static_configs:
|
static_configs:
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/frontendlogging"
|
"github.com/grafana/grafana/pkg/api/frontendlogging"
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"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/middleware"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
)
|
)
|
||||||
@ -397,6 +398,8 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), routing.Wrap(PostGraphiteAnnotation))
|
annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), routing.Wrap(PostGraphiteAnnotation))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
apiRoute.Post("/frontend-metrics", bind(metrics.PostFrontendMetricsCommand{}), routing.Wrap(hs.PostFrontendMetrics))
|
||||||
|
|
||||||
if hs.Live.IsEnabled() {
|
if hs.Live.IsEnabled() {
|
||||||
apiRoute.Post("/live/publish", bind(dtos.LivePublishCmd{}), routing.Wrap(hs.Live.HandleHTTPPublish))
|
apiRoute.Post("/live/publish", bind(dtos.LivePublishCmd{}), routing.Wrap(hs.Live.HandleHTTPPublish))
|
||||||
}
|
}
|
||||||
|
21
pkg/api/frontend_metrics.go
Normal file
21
pkg/api/frontend_metrics.go
Normal file
@ -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)
|
||||||
|
}
|
43
pkg/infra/metrics/frontendmetrics.go
Normal file
43
pkg/infra/metrics/frontendmetrics.go
Normal file
@ -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")
|
||||||
|
}
|
@ -22,6 +22,7 @@ func (lw *logWrapper) Println(v ...interface{}) {
|
|||||||
func init() {
|
func init() {
|
||||||
registry.RegisterService(&InternalMetricsService{})
|
registry.RegisterService(&InternalMetricsService{})
|
||||||
initMetricVars()
|
initMetricVars()
|
||||||
|
initFrontendMetrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
type InternalMetricsService struct {
|
type InternalMetricsService struct {
|
||||||
|
@ -124,18 +124,22 @@ function initEchoSrv() {
|
|||||||
setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' }));
|
setEchoSrv(new Echo({ debug: process.env.NODE_ENV === 'development' }));
|
||||||
|
|
||||||
window.addEventListener('load', (e) => {
|
window.addEventListener('load', (e) => {
|
||||||
// Collecting paint metrics first
|
const loadMetricName = 'frontend_boot_load_time_seconds';
|
||||||
|
|
||||||
if (performance && performance.getEntriesByType) {
|
if (performance && performance.getEntriesByType) {
|
||||||
performance.mark('load');
|
performance.mark(loadMetricName);
|
||||||
|
|
||||||
const paintMetrics = performance.getEntriesByType('paint');
|
const paintMetrics = performance.getEntriesByType('paint');
|
||||||
|
|
||||||
for (const metric of paintMetrics) {
|
for (const metric of paintMetrics) {
|
||||||
reportPerformance(metric.name, Math.round(metric.startTime + metric.duration));
|
reportPerformance(
|
||||||
|
`frontend_boot_${metric.name}_time_seconds`,
|
||||||
|
Math.round(metric.startTime + metric.duration) / 1000
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadMetric = performance.getEntriesByName('load')[0];
|
const loadMetric = performance.getEntriesByName(loadMetricName)[0];
|
||||||
reportPerformance(loadMetric.name, Math.round(loadMetric.startTime + loadMetric.duration));
|
reportPerformance(loadMetric.name, Math.round(loadMetric.startTime + loadMetric.duration) / 1000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -150,10 +154,6 @@ function initEchoSrv() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
|
||||||
reportPerformance('dcl', Math.round(performance.now()));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addClassIfNoOverlayScrollbar() {
|
function addClassIfNoOverlayScrollbar() {
|
||||||
|
@ -6,8 +6,8 @@ export const reportPerformance = (metric: string, value: number) => {
|
|||||||
getEchoSrv().addEvent<PerformanceEvent>({
|
getEchoSrv().addEvent<PerformanceEvent>({
|
||||||
type: EchoEventType.Performance,
|
type: EchoEventType.Performance,
|
||||||
payload: {
|
payload: {
|
||||||
metricName: metric,
|
name: metric,
|
||||||
duration: value,
|
value: value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { EchoBackend, EchoEvent, EchoEventType } from '@grafana/runtime';
|
import { EchoBackend, EchoEvent, EchoEventType } from '@grafana/runtime';
|
||||||
|
import { backendSrv } from '../../backend_srv';
|
||||||
|
|
||||||
export interface PerformanceEventPayload {
|
export interface PerformanceEventPayload {
|
||||||
metricName: string;
|
name: string;
|
||||||
duration: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PerformanceEvent extends EchoEvent<EchoEventType.Performance, PerformanceEventPayload> {}
|
export interface PerformanceEvent extends EchoEvent<EchoEventType.Performance, PerformanceEventPayload> {}
|
||||||
@ -16,13 +17,13 @@ export interface PerformanceBackendOptions {
|
|||||||
* Reports performance metrics to given url (TODO)
|
* Reports performance metrics to given url (TODO)
|
||||||
*/
|
*/
|
||||||
export class PerformanceBackend implements EchoBackend<PerformanceEvent, PerformanceBackendOptions> {
|
export class PerformanceBackend implements EchoBackend<PerformanceEvent, PerformanceBackendOptions> {
|
||||||
private buffer: PerformanceEvent[] = [];
|
private buffer: PerformanceEventPayload[] = [];
|
||||||
supportedEvents = [EchoEventType.Performance];
|
supportedEvents = [EchoEventType.Performance];
|
||||||
|
|
||||||
constructor(public options: PerformanceBackendOptions) {}
|
constructor(public options: PerformanceBackendOptions) {}
|
||||||
|
|
||||||
addEvent = (e: EchoEvent) => {
|
addEvent = (e: EchoEvent) => {
|
||||||
this.buffer.push(e);
|
this.buffer.push(e.payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
flush = () => {
|
flush = () => {
|
||||||
@ -30,20 +31,17 @@ export class PerformanceBackend implements EchoBackend<PerformanceEvent, Perform
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
|
||||||
metrics: this.buffer,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Currently we don't have an API for sending the metrics hence logging to console in dev environment
|
// Currently we don't have an API for sending the metrics hence logging to console in dev environment
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.log('PerformanceBackend flushing:', result);
|
console.log('PerformanceBackend flushing:', this.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('performance', this.buffer);
|
||||||
|
|
||||||
|
backendSrv.post('/api/frontend-metrics', {
|
||||||
|
events: this.buffer,
|
||||||
|
});
|
||||||
|
|
||||||
this.buffer = [];
|
this.buffer = [];
|
||||||
|
|
||||||
// TODO: Enable backend request when we have metrics API
|
|
||||||
// if (this.options.url) {
|
|
||||||
// backendSrv.post(this.options.url, result);
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -23,8 +23,9 @@
|
|||||||
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css" />
|
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css" />
|
||||||
|
|
||||||
<script nonce="[[.Nonce]]">
|
<script nonce="[[.Nonce]]">
|
||||||
performance.mark('css done blocking');
|
performance.mark('frontend_boot_css_time_seconds');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
<meta name="msapplication-TileColor" content="#2b5797" />
|
<meta name="msapplication-TileColor" content="#2b5797" />
|
||||||
@ -314,8 +315,9 @@
|
|||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
></script>
|
></script>
|
||||||
<% } %> <% } %>
|
<% } %> <% } %>
|
||||||
|
|
||||||
<script nonce="[[.Nonce]]">
|
<script nonce="[[.Nonce]]">
|
||||||
performance.mark('js done blocking');
|
performance.mark('frontend_boot_js_done_time_seconds');
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Reference in New Issue
Block a user