mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Frontend Logging: Integrate grafana javascript agent (#50801)
Add Grafana Javascript Agent integration to Grafana
This commit is contained in:
parent
849d4a3c56
commit
7c886fb6f9
@ -687,9 +687,9 @@ exports[`better eslint`] = {
|
|||||||
"packages/grafana-data/src/types/app.ts:2148970488": [
|
"packages/grafana-data/src/types/app.ts:2148970488": [
|
||||||
[75, 48, 3, "Unexpected any. Specify a different type.", "193409811"]
|
[75, 48, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||||
],
|
],
|
||||||
"packages/grafana-data/src/types/config.ts:1574035243": [
|
"packages/grafana-data/src/types/config.ts:2312759525": [
|
||||||
[171, 11, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[185, 11, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
[184, 16, 3, "Unexpected any. Specify a different type.", "193409811"]
|
[199, 16, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||||
],
|
],
|
||||||
"packages/grafana-data/src/types/dashboard.ts:1867834569": [
|
"packages/grafana-data/src/types/dashboard.ts:1867834569": [
|
||||||
[12, 39, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[12, 39, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
@ -1214,19 +1214,19 @@ exports[`better eslint`] = {
|
|||||||
[31, 49, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[31, 49, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
[31, 73, 3, "Unexpected any. Specify a different type.", "193409811"]
|
[31, 73, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||||
],
|
],
|
||||||
"packages/grafana-runtime/src/config.ts:2061755826": [
|
"packages/grafana-runtime/src/config.ts:4181107692": [
|
||||||
[66, 11, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[66, 11, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
[75, 29, 17, "Do not use any type assertions.", "4278379396"],
|
[75, 29, 17, "Do not use any type assertions.", "4278379396"],
|
||||||
[98, 16, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[106, 16, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
[183, 18, 13, "Do not use any type assertions.", "538937261"],
|
[191, 18, 13, "Do not use any type assertions.", "538937261"],
|
||||||
[183, 28, 3, "Unexpected any. Specify a different type.", "193409811"]
|
[191, 28, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||||
],
|
],
|
||||||
"packages/grafana-runtime/src/services/AngularLoader.ts:3455177907": [
|
"packages/grafana-runtime/src/services/AngularLoader.ts:3455177907": [
|
||||||
[45, 14, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[45, 14, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
[61, 13, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[61, 13, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
[61, 30, 3, "Unexpected any. Specify a different type.", "193409811"]
|
[61, 30, 3, "Unexpected any. Specify a different type.", "193409811"]
|
||||||
],
|
],
|
||||||
"packages/grafana-runtime/src/services/EchoSrv.ts:2163840677": [
|
"packages/grafana-runtime/src/services/EchoSrv.ts:347668574": [
|
||||||
[51, 51, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[51, 51, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
[51, 60, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[51, 60, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
[63, 53, 3, "Unexpected any. Specify a different type.", "193409811"],
|
[63, 53, 3, "Unexpected any. Specify a different type.", "193409811"],
|
||||||
|
@ -733,13 +733,16 @@ tag =
|
|||||||
# Should Sentry javascript agent be initialized
|
# Should Sentry javascript agent be initialized
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
|
# Defines which provider to use sentry or grafana
|
||||||
|
provider = sentry
|
||||||
|
|
||||||
# Sentry DSN if you want to send events to Sentry.
|
# Sentry DSN if you want to send events to Sentry.
|
||||||
sentry_dsn =
|
sentry_dsn =
|
||||||
|
|
||||||
# Custom HTTP endpoint to send events captured by the Sentry agent to. Default will log the events to stdout.
|
# Custom HTTP endpoint to send events to. Default will log the events to stdout.
|
||||||
custom_endpoint = /log
|
custom_endpoint =
|
||||||
|
|
||||||
# Rate of events to be reported between 0 (none) and 1 (all), float
|
# Rate of events to be reported to Sentry between 0 (none) and 1 (all), float
|
||||||
sample_rate = 1.0
|
sample_rate = 1.0
|
||||||
|
|
||||||
# Requests per second limit enforced per an extended period, for Grafana backend log ingestion endpoint (/log).
|
# Requests per second limit enforced per an extended period, for Grafana backend log ingestion endpoint (/log).
|
||||||
@ -748,6 +751,18 @@ log_endpoint_requests_per_second_limit = 3
|
|||||||
# Max requests accepted per short interval of time for Grafana backend log ingestion endpoint (/log)
|
# Max requests accepted per short interval of time for Grafana backend log ingestion endpoint (/log)
|
||||||
log_endpoint_burst_limit = 15
|
log_endpoint_burst_limit = 15
|
||||||
|
|
||||||
|
# Should error instrumentation be enabled, only affects Grafana Javascript Agent
|
||||||
|
instrumentations_errors_enabled = true
|
||||||
|
|
||||||
|
# Should console instrumentation be enabled, only affects Grafana Javascript Agent
|
||||||
|
instrumentations_console_enabled = false
|
||||||
|
|
||||||
|
# Should webvitals instrumentation be enabled, only affects Grafana Javascript Agent
|
||||||
|
instrumentations_webvitals_enabled = false
|
||||||
|
|
||||||
|
# Api Key, only applies to Grafana Javascript Agent provider
|
||||||
|
api_key =
|
||||||
|
|
||||||
#################################### Usage Quotas ########################
|
#################################### Usage Quotas ########################
|
||||||
[quota]
|
[quota]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
@ -720,6 +720,9 @@
|
|||||||
# Should Sentry javascript agent be initialized
|
# Should Sentry javascript agent be initialized
|
||||||
;enabled = false
|
;enabled = false
|
||||||
|
|
||||||
|
# Defines which provider to use, default is Sentry
|
||||||
|
;provider = sentry
|
||||||
|
|
||||||
# Sentry DSN if you want to send events to Sentry.
|
# Sentry DSN if you want to send events to Sentry.
|
||||||
;sentry_dsn =
|
;sentry_dsn =
|
||||||
|
|
||||||
@ -735,6 +738,18 @@
|
|||||||
# Max requests accepted per short interval of time for Grafana backend log ingestion endpoint (/log).
|
# Max requests accepted per short interval of time for Grafana backend log ingestion endpoint (/log).
|
||||||
;log_endpoint_burst_limit = 15
|
;log_endpoint_burst_limit = 15
|
||||||
|
|
||||||
|
# Should error instrumentation be enabled, only affects Grafana Javascript Agent
|
||||||
|
;instrumentations_errors_enabled = true
|
||||||
|
|
||||||
|
# Should console instrumentation be enabled, only affects Grafana Javascript Agent
|
||||||
|
;instrumentations_console_enabled = false
|
||||||
|
|
||||||
|
# Should webvitals instrumentation be enabled, only affects Grafana Javascript Agent
|
||||||
|
;instrumentations_webvitals_enabled = false
|
||||||
|
|
||||||
|
# Api Key, only applies to Grafana Javascript Agent provider
|
||||||
|
;api_key = testApiKey
|
||||||
|
|
||||||
#################################### Usage Quotas ########################
|
#################################### Usage Quotas ########################
|
||||||
[quota]
|
[quota]
|
||||||
; enabled = false
|
; enabled = false
|
||||||
|
@ -1103,6 +1103,10 @@ Syslog tag. By default, the process's `argv[0]` is used.
|
|||||||
|
|
||||||
Sentry javascript agent is initialized. Default is `false`.
|
Sentry javascript agent is initialized. Default is `false`.
|
||||||
|
|
||||||
|
### provider
|
||||||
|
|
||||||
|
Defines which provider to use `sentry` or `grafana`. Default is `sentry`
|
||||||
|
|
||||||
### sentry_dsn
|
### sentry_dsn
|
||||||
|
|
||||||
Sentry DSN if you want to send events to Sentry
|
Sentry DSN if you want to send events to Sentry
|
||||||
@ -1123,6 +1127,22 @@ Requests per second limit enforced per an extended period, for Grafana backend l
|
|||||||
|
|
||||||
Maximum requests accepted per short interval of time for Grafana backend log ingestion endpoint, `/log`. Default is `15`.
|
Maximum requests accepted per short interval of time for Grafana backend log ingestion endpoint, `/log`. Default is `15`.
|
||||||
|
|
||||||
|
### instrumentations_errors_enabled
|
||||||
|
|
||||||
|
Turn on error instrumentation. Only affects Grafana Javascript Agent.
|
||||||
|
|
||||||
|
### instrumentations_console_enabled
|
||||||
|
|
||||||
|
Turn on console instrumentation. Only affects Grafana Javascript Agent
|
||||||
|
|
||||||
|
### instrumentations_webvitals_enabled
|
||||||
|
|
||||||
|
Turn on webvitals instrumentation. Only affects Grafana Javascript Agent
|
||||||
|
|
||||||
|
### api_key
|
||||||
|
|
||||||
|
If `custom_endpoint` required authentication, you can set the api key here. Only relevant for Grafana Javascript Agent provider.
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
## [quota]
|
## [quota]
|
||||||
|
1
go.mod
1
go.mod
@ -308,6 +308,7 @@ require (
|
|||||||
github.com/segmentio/asm v1.1.1 // indirect
|
github.com/segmentio/asm v1.1.1 // indirect
|
||||||
github.com/smartystreets/goconvey v1.7.2 // indirect
|
github.com/smartystreets/goconvey v1.7.2 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||||
|
github.com/wk8/go-ordered-map v1.0.0
|
||||||
github.com/xlab/treeprint v1.1.0 // indirect
|
github.com/xlab/treeprint v1.1.0 // indirect
|
||||||
github.com/yudai/pp v2.0.1+incompatible // indirect
|
github.com/yudai/pp v2.0.1+incompatible // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.6.3 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.6.3 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -2740,6 +2740,8 @@ github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPyS
|
|||||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
|
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
|
||||||
|
github.com/wk8/go-ordered-map v1.0.0 h1:BV7z+2PaK8LTSd/mWgY12HyMAo5CEgkHqbkVq2thqr8=
|
||||||
|
github.com/wk8/go-ordered-map v1.0.0/go.mod h1:9ZIbRunKbuvfPKyBP1SIKLcXNlv74YCOZ3t3VTS6gRk=
|
||||||
github.com/wvanbergen/kafka v0.0.0-20171203153745-e2edea948ddf/go.mod h1:nxx7XRXbR9ykhnC8lXqQyJS0rfvJGxKyKw/sT1YOttg=
|
github.com/wvanbergen/kafka v0.0.0-20171203153745-e2edea948ddf/go.mod h1:nxx7XRXbR9ykhnC8lXqQyJS0rfvJGxKyKw/sT1YOttg=
|
||||||
github.com/wvanbergen/kazoo-go v0.0.0-20180202103751-f72d8611297a/go.mod h1:vQQATAGxVK20DC1rRubTJbZDDhhpA4QfU02pMdPxGO4=
|
github.com/wvanbergen/kazoo-go v0.0.0-20180202103751-f72d8611297a/go.mod h1:vQQATAGxVK20DC1rRubTJbZDDhhpA4QfU02pMdPxGO4=
|
||||||
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
|
github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
|
||||||
|
@ -252,6 +252,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/css": "11.9.0",
|
"@emotion/css": "11.9.0",
|
||||||
"@emotion/react": "11.9.0",
|
"@emotion/react": "11.9.0",
|
||||||
|
"@grafana/agent-core": "^0.3.0",
|
||||||
|
"@grafana/agent-web": "^0.3.0",
|
||||||
"@grafana/aws-sdk": "0.0.36",
|
"@grafana/aws-sdk": "0.0.36",
|
||||||
"@grafana/data": "workspace:*",
|
"@grafana/data": "workspace:*",
|
||||||
"@grafana/e2e-selectors": "workspace:*",
|
"@grafana/e2e-selectors": "workspace:*",
|
||||||
|
@ -59,6 +59,20 @@ export interface SentryConfig {
|
|||||||
sampleRate: number;
|
sampleRate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes GrafanaJavascriptAgentConfig integration config
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface GrafanaJavascriptAgentConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
customEndpoint: string;
|
||||||
|
errorInstrumentalizationEnabled: boolean;
|
||||||
|
consoleInstrumentalizationEnabled: boolean;
|
||||||
|
webVitalsInstrumentalizationEnabled: boolean;
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the plugins that should be preloaded prior to start Grafana.
|
* Describes the plugins that should be preloaded prior to start Grafana.
|
||||||
*
|
*
|
||||||
@ -182,6 +196,7 @@ export interface GrafanaConfig {
|
|||||||
http2Enabled: boolean;
|
http2Enabled: boolean;
|
||||||
dateFormats?: SystemDateFormatSettings;
|
dateFormats?: SystemDateFormatSettings;
|
||||||
sentry: SentryConfig;
|
sentry: SentryConfig;
|
||||||
|
grafanaJavascriptAgent: GrafanaJavascriptAgentConfig;
|
||||||
customTheme?: any;
|
customTheme?: any;
|
||||||
geomapDefaultBaseLayer?: MapLayerOptions;
|
geomapDefaultBaseLayer?: MapLayerOptions;
|
||||||
geomapDisableCustomBaseLayer?: boolean;
|
geomapDisableCustomBaseLayer?: boolean;
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@grafana/agent-web": "^0.3.0",
|
||||||
"@grafana/data": "9.1.0-pre",
|
"@grafana/data": "9.1.0-pre",
|
||||||
"@grafana/e2e-selectors": "9.1.0-pre",
|
"@grafana/e2e-selectors": "9.1.0-pre",
|
||||||
"@grafana/ui": "9.1.0-pre",
|
"@grafana/ui": "9.1.0-pre",
|
||||||
|
@ -91,6 +91,14 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
|||||||
customEndpoint: '',
|
customEndpoint: '',
|
||||||
sampleRate: 1,
|
sampleRate: 1,
|
||||||
};
|
};
|
||||||
|
grafanaJavascriptAgent = {
|
||||||
|
enabled: false,
|
||||||
|
customEndpoint: '',
|
||||||
|
apiKey: '',
|
||||||
|
errorInstrumentalizationEnabled: true,
|
||||||
|
consoleInstrumentalizationEnabled: false,
|
||||||
|
webVitalsInstrumentalizationEnabled: false,
|
||||||
|
};
|
||||||
pluginCatalogURL = 'https://grafana.com/grafana/plugins/';
|
pluginCatalogURL = 'https://grafana.com/grafana/plugins/';
|
||||||
pluginAdminEnabled = true;
|
pluginAdminEnabled = true;
|
||||||
pluginAdminExternalManageEnabled = false;
|
pluginAdminExternalManageEnabled = false;
|
||||||
|
@ -82,6 +82,7 @@ export enum EchoEventType {
|
|||||||
Pageview = 'pageview',
|
Pageview = 'pageview',
|
||||||
Interaction = 'interaction',
|
Interaction = 'interaction',
|
||||||
ExperimentView = 'experimentview',
|
ExperimentView = 'experimentview',
|
||||||
|
GrafanaJavascriptAgent = 'grafana-javascript-agent',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { captureMessage, captureException, Severity as LogLevel } from '@sentry/browser';
|
import { captureMessage, captureException, Severity as LogLevel } from '@sentry/browser';
|
||||||
|
|
||||||
|
import { agent, LogLevel as GrafanaLogLevel } from '@grafana/agent-web';
|
||||||
|
|
||||||
|
import { config } from '../config';
|
||||||
|
|
||||||
export { LogLevel };
|
export { LogLevel };
|
||||||
|
|
||||||
// a bit stricter than what Sentry allows
|
// a bit stricter than what Sentry allows
|
||||||
@ -10,10 +15,18 @@ type Contexts = Record<string, Record<string, number | string | Record<string, s
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function logInfo(message: string, contexts?: Contexts) {
|
export function logInfo(message: string, contexts?: Contexts) {
|
||||||
captureMessage(message, {
|
if (config.grafanaJavascriptAgent.enabled) {
|
||||||
level: LogLevel.Info,
|
agent.api.pushLog([message], {
|
||||||
contexts,
|
level: GrafanaLogLevel.INFO,
|
||||||
});
|
context: contexts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (config.sentry.enabled) {
|
||||||
|
captureMessage(message, {
|
||||||
|
level: LogLevel.Info,
|
||||||
|
contexts,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,10 +35,18 @@ export function logInfo(message: string, contexts?: Contexts) {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function logWarning(message: string, contexts?: Contexts) {
|
export function logWarning(message: string, contexts?: Contexts) {
|
||||||
captureMessage(message, {
|
if (config.grafanaJavascriptAgent.enabled) {
|
||||||
level: LogLevel.Warning,
|
agent.api.pushLog([message], {
|
||||||
contexts,
|
level: GrafanaLogLevel.WARN,
|
||||||
});
|
context: contexts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (config.sentry.enabled) {
|
||||||
|
captureMessage(message, {
|
||||||
|
level: LogLevel.Warning,
|
||||||
|
contexts,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,10 +55,18 @@ export function logWarning(message: string, contexts?: Contexts) {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function logDebug(message: string, contexts?: Contexts) {
|
export function logDebug(message: string, contexts?: Contexts) {
|
||||||
captureMessage(message, {
|
if (config.grafanaJavascriptAgent.enabled) {
|
||||||
level: LogLevel.Debug,
|
agent.api.pushLog([message], {
|
||||||
contexts,
|
level: GrafanaLogLevel.DEBUG,
|
||||||
});
|
context: contexts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (config.sentry.enabled) {
|
||||||
|
captureMessage(message, {
|
||||||
|
level: LogLevel.Debug,
|
||||||
|
contexts,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,5 +75,13 @@ export function logDebug(message: string, contexts?: Contexts) {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function logError(err: Error, contexts?: Contexts) {
|
export function logError(err: Error, contexts?: Contexts) {
|
||||||
captureException(err, { contexts });
|
if (config.grafanaJavascriptAgent.enabled) {
|
||||||
|
agent.api.pushLog([err.message], {
|
||||||
|
level: GrafanaLogLevel.ERROR,
|
||||||
|
context: contexts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (config.sentry.enabled) {
|
||||||
|
captureException(err, { contexts });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -616,4 +616,6 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS)
|
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS)
|
||||||
r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now),
|
r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now),
|
||||||
routing.Wrap(NewFrontendLogMessageHandler(sourceMapStore)))
|
routing.Wrap(NewFrontendLogMessageHandler(sourceMapStore)))
|
||||||
|
r.Post("/log-grafana-javascript-agent", middleware.RateLimit(hs.Cfg.GrafanaJavascriptAgent.EndpointRPS, hs.Cfg.GrafanaJavascriptAgent.EndpointBurst, time.Now),
|
||||||
|
routing.Wrap(GrafanaJavascriptAgentLogMessageHandler(sourceMapStore)))
|
||||||
}
|
}
|
||||||
|
@ -46,3 +46,72 @@ func NewFrontendLogMessageHandler(store *frontendlogging.SourceMapStore) fronten
|
|||||||
return response.Success("ok")
|
return response.Success("ok")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GrafanaJavascriptAgentLogMessageHandler(store *frontendlogging.SourceMapStore) frontendLogMessageHandler {
|
||||||
|
return func(c *models.ReqContext) response.Response {
|
||||||
|
event := frontendlogging.FrontendGrafanaJavascriptAgentEvent{}
|
||||||
|
if err := web.Bind(c.Req, &event); err != nil {
|
||||||
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta object is standard across event types, adding it globally.
|
||||||
|
|
||||||
|
if event.Logs != nil && len(event.Logs) > 0 {
|
||||||
|
for _, logEntry := range event.Logs {
|
||||||
|
var ctx = frontendlogging.CtxVector{}
|
||||||
|
ctx = event.AddMetaToContext(ctx)
|
||||||
|
ctx = append(ctx, "kind", "log", "original_timestamp", logEntry.Timestamp)
|
||||||
|
|
||||||
|
for k, v := range frontendlogging.KeyValToInterfaceMap(logEntry.KeyValContext()) {
|
||||||
|
ctx = append(ctx, k, v)
|
||||||
|
}
|
||||||
|
switch logEntry.LogLevel {
|
||||||
|
case frontendlogging.LogLevelDebug, frontendlogging.LogLevelTrace:
|
||||||
|
{
|
||||||
|
ctx = append(ctx, "original_log_level", logEntry.LogLevel)
|
||||||
|
frontendLogger.Debug(logEntry.Message, ctx...)
|
||||||
|
}
|
||||||
|
case frontendlogging.LogLevelError:
|
||||||
|
{
|
||||||
|
ctx = append(ctx, "original_log_level", logEntry.LogLevel)
|
||||||
|
frontendLogger.Error(logEntry.Message, ctx...)
|
||||||
|
}
|
||||||
|
case frontendlogging.LogLevelWarning:
|
||||||
|
{
|
||||||
|
ctx = append(ctx, "original_log_level", logEntry.LogLevel)
|
||||||
|
frontendLogger.Warn(logEntry.Message, ctx...)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
ctx = append(ctx, "original_log_level", logEntry.LogLevel)
|
||||||
|
frontendLogger.Info(logEntry.Message, ctx...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Measurements != nil && len(event.Measurements) > 0 {
|
||||||
|
for _, measurementEntry := range event.Measurements {
|
||||||
|
for measurementName, measurementValue := range measurementEntry.Values {
|
||||||
|
var ctx = frontendlogging.CtxVector{}
|
||||||
|
ctx = event.AddMetaToContext(ctx)
|
||||||
|
ctx = append(ctx, measurementName, measurementValue)
|
||||||
|
ctx = append(ctx, "kind", "measurement", "original_timestamp", measurementEntry.Timestamp)
|
||||||
|
frontendLogger.Info("Measurement: "+measurementEntry.Type, ctx...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if event.Exceptions != nil && len(event.Exceptions) > 0 {
|
||||||
|
for _, exception := range event.Exceptions {
|
||||||
|
var ctx = frontendlogging.CtxVector{}
|
||||||
|
ctx = event.AddMetaToContext(ctx)
|
||||||
|
exception := exception
|
||||||
|
transformedException := frontendlogging.TransformException(&exception, store)
|
||||||
|
ctx = append(ctx, "kind", "exception", "type", transformedException.Type, "value", transformedException.Value, "stacktrace", transformedException.String())
|
||||||
|
ctx = append(ctx, "original_timestamp", exception.Timestamp)
|
||||||
|
frontendLogger.Error(exception.Message(), ctx...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.Success("ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -102,7 +102,79 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFrontendLoggingEndpoint(t *testing.T) {
|
func logGrafanaJavascriptAgentEventScenario(t *testing.T, desc string, event frontendlogging.FrontendGrafanaJavascriptAgentEvent, fn logScenarioFunc) {
|
||||||
|
t.Run(desc, func(t *testing.T) {
|
||||||
|
var logcontent = make(map[string]interface{})
|
||||||
|
logcontent["logger"] = "frontend"
|
||||||
|
newfrontendLogger := log.Logger(log.LoggerFunc(func(keyvals ...interface{}) error {
|
||||||
|
for i := 0; i < len(keyvals); i += 2 {
|
||||||
|
logcontent[keyvals[i].(string)] = keyvals[i+1]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
origHandler := frontendLogger.GetLogger()
|
||||||
|
frontendLogger.Swap(level.NewFilter(newfrontendLogger, level.AllowInfo()))
|
||||||
|
sourceMapReads := []SourceMapReadRecord{}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
frontendLogger.Swap(origHandler)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc := setupScenarioContext(t, "/log-grafana-javascript-agent")
|
||||||
|
|
||||||
|
cdnRootURL, e := url.Parse("https://storage.googleapis.com/grafana-static-assets")
|
||||||
|
require.NoError(t, e)
|
||||||
|
|
||||||
|
cfg := &setting.Cfg{
|
||||||
|
StaticRootPath: "/staticroot",
|
||||||
|
CDNRootURL: cdnRootURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
readSourceMap := func(dir string, path string) ([]byte, error) {
|
||||||
|
sourceMapReads = append(sourceMapReads, SourceMapReadRecord{
|
||||||
|
dir: dir,
|
||||||
|
path: path,
|
||||||
|
})
|
||||||
|
if strings.Contains(path, "error") {
|
||||||
|
return nil, errors.New("epic hard drive failure")
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(path, "foo.js.map") {
|
||||||
|
f, err := ioutil.ReadFile("./frontendlogging/test-data/foo.js.map")
|
||||||
|
require.NoError(t, err)
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// fake plugin route so we will try to find a source map there
|
||||||
|
pm := fakePluginStaticRouteResolver{
|
||||||
|
routes: []*plugins.StaticRoute{
|
||||||
|
{
|
||||||
|
Directory: "/usr/local/telepathic-panel",
|
||||||
|
PluginID: "telepathic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceMapStore := frontendlogging.NewSourceMapStore(cfg, &pm, readSourceMap)
|
||||||
|
|
||||||
|
loggingHandler := GrafanaJavascriptAgentLogMessageHandler(sourceMapStore)
|
||||||
|
|
||||||
|
handler := routing.Wrap(func(c *models.ReqContext) response.Response {
|
||||||
|
sc.context = c
|
||||||
|
c.Req.Body = mockRequestBody(event)
|
||||||
|
c.Req.Header.Add("Content-Type", "application/json")
|
||||||
|
return loggingHandler(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Post(sc.url, handler)
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
fn(sc, logcontent, sourceMapReads)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFrontendLoggingEndpointSentry(t *testing.T) {
|
||||||
ts, err := time.Parse("2006-01-02T15:04:05.000Z", "2020-10-22T06:29:29.078Z")
|
ts, err := time.Parse("2006-01-02T15:04:05.000Z", "2020-10-22T06:29:29.078Z")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -312,6 +384,196 @@ func TestFrontendLoggingEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFrontendLoggingEndpointGrafanaJavascriptAgent(t *testing.T) {
|
||||||
|
ts, err := time.Parse("2006-01-02T15:04:05.000Z", "2020-10-22T06:29:29.078Z")
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Run("FrontendLoggingEndpointGrafanaJavascriptAgent", func(t *testing.T) {
|
||||||
|
user := frontendlogging.User{
|
||||||
|
Email: "test@example.com",
|
||||||
|
ID: "45",
|
||||||
|
}
|
||||||
|
meta := frontendlogging.Meta{
|
||||||
|
User: user,
|
||||||
|
Page: frontendlogging.Page{
|
||||||
|
URL: "http://localhost:3000/dashboard/db/test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
errorEvent := frontendlogging.FrontendGrafanaJavascriptAgentEvent{
|
||||||
|
Meta: meta,
|
||||||
|
Exceptions: []frontendlogging.Exception{
|
||||||
|
{
|
||||||
|
Type: "UserError",
|
||||||
|
Value: "Please replace user and try again\n at foofn (foo.js:123:23)\n at barfn (bar.js:113:231)",
|
||||||
|
Stacktrace: &frontendlogging.Stacktrace{
|
||||||
|
Frames: []frontendlogging.Frame{{
|
||||||
|
Function: "bla",
|
||||||
|
Filename: "http://localhost:3000/public/build/foo.js",
|
||||||
|
Lineno: 20,
|
||||||
|
Colno: 30,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timestamp: ts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logGrafanaJavascriptAgentEventScenario(t, "Should log received error event", errorEvent,
|
||||||
|
func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
|
||||||
|
assert.Equal(t, 200, sc.resp.Code)
|
||||||
|
assertContextContains(t, logs, "logger", "frontend")
|
||||||
|
assertContextContains(t, logs, "page_url", errorEvent.Meta.Page.URL)
|
||||||
|
assertContextContains(t, logs, "user_email", errorEvent.Meta.User.Email)
|
||||||
|
assertContextContains(t, logs, "user_id", errorEvent.Meta.User.ID)
|
||||||
|
assertContextContains(t, logs, "original_timestamp", errorEvent.Exceptions[0].Timestamp)
|
||||||
|
assertContextContains(t, logs, "msg", `UserError: Please replace user and try again
|
||||||
|
at foofn (foo.js:123:23)
|
||||||
|
at barfn (bar.js:113:231)`)
|
||||||
|
assert.NotContains(t, logs, "context")
|
||||||
|
})
|
||||||
|
|
||||||
|
logEvent := frontendlogging.FrontendGrafanaJavascriptAgentEvent{
|
||||||
|
Meta: meta,
|
||||||
|
Logs: []frontendlogging.Log{{
|
||||||
|
Message: "This is a test log message",
|
||||||
|
Timestamp: ts,
|
||||||
|
LogLevel: "info",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
logGrafanaJavascriptAgentEventScenario(t, "Should log received log event", logEvent,
|
||||||
|
func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
|
||||||
|
assert.Equal(t, 200, sc.resp.Code)
|
||||||
|
assert.Len(t, logs, 11)
|
||||||
|
assertContextContains(t, logs, "logger", "frontend")
|
||||||
|
assertContextContains(t, logs, "msg", "This is a test log message")
|
||||||
|
assertContextContains(t, logs, "original_log_level", frontendlogging.LogLevel("info"))
|
||||||
|
assertContextContains(t, logs, "original_timestamp", ts)
|
||||||
|
assert.NotContains(t, logs, "stacktrace")
|
||||||
|
assert.NotContains(t, logs, "context")
|
||||||
|
})
|
||||||
|
|
||||||
|
logEventWithContext := frontendlogging.FrontendGrafanaJavascriptAgentEvent{
|
||||||
|
Meta: meta,
|
||||||
|
Logs: []frontendlogging.Log{{
|
||||||
|
Message: "This is a test log message",
|
||||||
|
Timestamp: ts,
|
||||||
|
LogLevel: "info",
|
||||||
|
Context: map[string]string{
|
||||||
|
"one": "two",
|
||||||
|
"bar": "baz",
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
logGrafanaJavascriptAgentEventScenario(t, "Should log received log context", logEventWithContext,
|
||||||
|
func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
|
||||||
|
assert.Equal(t, 200, sc.resp.Code)
|
||||||
|
assertContextContains(t, logs, "context_one", "two")
|
||||||
|
assertContextContains(t, logs, "context_bar", "baz")
|
||||||
|
})
|
||||||
|
errorEventForSourceMapping := frontendlogging.FrontendGrafanaJavascriptAgentEvent{
|
||||||
|
Meta: meta,
|
||||||
|
Exceptions: []frontendlogging.Exception{
|
||||||
|
{
|
||||||
|
Type: "UserError",
|
||||||
|
Value: "Please replace user and try again",
|
||||||
|
Stacktrace: &frontendlogging.Stacktrace{
|
||||||
|
Frames: []frontendlogging.Frame{
|
||||||
|
{
|
||||||
|
Function: "foofn",
|
||||||
|
Filename: "http://localhost:3000/public/build/moo/foo.js", // source map found and mapped, core
|
||||||
|
Lineno: 2,
|
||||||
|
Colno: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: "foofn",
|
||||||
|
Filename: "http://localhost:3000/public/plugins/telepathic/foo.js", // plugin, source map found and mapped
|
||||||
|
Lineno: 3,
|
||||||
|
Colno: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: "explode",
|
||||||
|
Filename: "http://localhost:3000/public/build/error.js", // reading source map throws error
|
||||||
|
Lineno: 3,
|
||||||
|
Colno: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: "wat",
|
||||||
|
Filename: "http://localhost:3000/public/build/bar.js", // core, but source map not found on fs
|
||||||
|
Lineno: 3,
|
||||||
|
Colno: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: "nope",
|
||||||
|
Filename: "http://localhost:3000/baz.js", // not core or plugin, wont even attempt to get source map
|
||||||
|
Lineno: 3,
|
||||||
|
Colno: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: "fake",
|
||||||
|
Filename: "http://localhost:3000/public/build/../../secrets.txt", // path will be sanitized
|
||||||
|
Lineno: 3,
|
||||||
|
Colno: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Function: "cdn",
|
||||||
|
Filename: "https://storage.googleapis.com/grafana-static-assets/grafana-oss/pre-releases/7.5.0-11925pre/public/build/foo.js", // source map found and mapped
|
||||||
|
Lineno: 3,
|
||||||
|
Colno: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Timestamp: ts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logGrafanaJavascriptAgentEventScenario(t, "Should load sourcemap and transform stacktrace line when possible", errorEventForSourceMapping,
|
||||||
|
func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
|
||||||
|
assert.Equal(t, 200, sc.resp.Code)
|
||||||
|
assertContextContains(t, logs, "stacktrace", `UserError: Please replace user and try again
|
||||||
|
at ? (webpack:///./some_source.ts:2:2)
|
||||||
|
at ? (webpack:///./some_source.ts:3:2)
|
||||||
|
at explode (http://localhost:3000/public/build/error.js:3:10)
|
||||||
|
at wat (http://localhost:3000/public/build/bar.js:3:10)
|
||||||
|
at nope (http://localhost:3000/baz.js:3:10)
|
||||||
|
at fake (http://localhost:3000/public/build/../../secrets.txt:3:10)
|
||||||
|
at ? (webpack:///./some_source.ts:3:2)`)
|
||||||
|
assert.Len(t, sourceMapReads, 6)
|
||||||
|
assert.Equal(t, "/staticroot", sourceMapReads[0].dir)
|
||||||
|
assert.Equal(t, "build/moo/foo.js.map", sourceMapReads[0].path)
|
||||||
|
assert.Equal(t, "/usr/local/telepathic-panel", sourceMapReads[1].dir)
|
||||||
|
assert.Equal(t, "/foo.js.map", sourceMapReads[1].path)
|
||||||
|
assert.Equal(t, "/staticroot", sourceMapReads[2].dir)
|
||||||
|
assert.Equal(t, "build/error.js.map", sourceMapReads[2].path)
|
||||||
|
assert.Equal(t, "/staticroot", sourceMapReads[3].dir)
|
||||||
|
assert.Equal(t, "build/bar.js.map", sourceMapReads[3].path)
|
||||||
|
assert.Equal(t, "/staticroot", sourceMapReads[4].dir)
|
||||||
|
assert.Equal(t, "secrets.txt.map", sourceMapReads[4].path)
|
||||||
|
assert.Equal(t, "/staticroot", sourceMapReads[5].dir)
|
||||||
|
assert.Equal(t, "build/foo.js.map", sourceMapReads[5].path)
|
||||||
|
})
|
||||||
|
|
||||||
|
logWebVitals := frontendlogging.FrontendGrafanaJavascriptAgentEvent{
|
||||||
|
Meta: meta,
|
||||||
|
Measurements: []frontendlogging.Measurement{{
|
||||||
|
Values: map[string]float64{
|
||||||
|
"CLS": 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
logGrafanaJavascriptAgentEventScenario(t, "Should log web vitals as context", logWebVitals,
|
||||||
|
func(sc *scenarioContext, logs map[string]interface{}, sourceMapReads []SourceMapReadRecord) {
|
||||||
|
assert.Equal(t, 200, sc.resp.Code)
|
||||||
|
assertContextContains(t, logs, "CLS", float64(1))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func assertContextContains(t *testing.T, logRecord map[string]interface{}, label string, value interface{}) {
|
func assertContextContains(t *testing.T, logRecord map[string]interface{}, label string, value interface{}) {
|
||||||
assert.Contains(t, logRecord, label)
|
assert.Contains(t, logRecord, label)
|
||||||
assert.Equal(t, value, logRecord[label])
|
assert.Equal(t, value, logRecord[label])
|
||||||
|
28
pkg/api/frontendlogging/grafana_javascript_agent.go
Normal file
28
pkg/api/frontendlogging/grafana_javascript_agent.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package frontendlogging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FrontendGrafanaJavascriptAgentEvent struct {
|
||||||
|
Exceptions []Exception `json:"exceptions,omitempty"`
|
||||||
|
Logs []Log `json:"logs,omitempty"`
|
||||||
|
Measurements []Measurement `json:"measurements,omitempty"`
|
||||||
|
Meta Meta `json:"meta,omitempty"`
|
||||||
|
Traces *Traces `json:"traces,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyValToInterfaceMap converts KeyVal to map[string]interface
|
||||||
|
func KeyValToInterfaceMap(kv *KeyVal) map[string]interface{} {
|
||||||
|
retv := make(map[string]interface{})
|
||||||
|
for el := kv.Oldest(); el != nil; el = el.Next() {
|
||||||
|
retv[fmt.Sprint(el.Key)] = el.Value
|
||||||
|
}
|
||||||
|
return retv
|
||||||
|
}
|
||||||
|
func (event *FrontendGrafanaJavascriptAgentEvent) AddMetaToContext(ctx CtxVector) []interface{} {
|
||||||
|
for k, v := range KeyValToInterfaceMap(event.Meta.KeyVal()) {
|
||||||
|
ctx = append(ctx, k, v)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
419
pkg/api/frontendlogging/grafana_javascript_agent_payload.go
Normal file
419
pkg/api/frontendlogging/grafana_javascript_agent_payload.go
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
/* This file is mostly copied over from https://github.com/grafana/agent/tree/main/pkg/integrations/v2/app_agent_receiver,
|
||||||
|
as soon as we can use agent as a dependency this can be refactored
|
||||||
|
*/
|
||||||
|
|
||||||
|
package frontendlogging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
om "github.com/wk8/go-ordered-map"
|
||||||
|
otlp "go.opentelemetry.io/collector/model/otlp"
|
||||||
|
otelpdata "go.opentelemetry.io/collector/model/pdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyVal is an ordered map of string to interface
|
||||||
|
type KeyVal = om.OrderedMap
|
||||||
|
|
||||||
|
// NewKeyVal creates new empty KeyVal
|
||||||
|
func NewKeyVal() *KeyVal {
|
||||||
|
return om.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyValAdd(kv *KeyVal, key string, value string) {
|
||||||
|
if len(value) > 0 {
|
||||||
|
kv.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeKeyVal will merge source in target
|
||||||
|
func MergeKeyVal(target *KeyVal, source *KeyVal) {
|
||||||
|
for el := source.Oldest(); el != nil; el = el.Next() {
|
||||||
|
target.Set(el.Key, el.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyValFromMap will instantiate KeyVal from a map[string]string
|
||||||
|
func KeyValFromMap(m map[string]string) *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
KeyValAdd(kv, k, m[k])
|
||||||
|
}
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload is the body of the receiver request
|
||||||
|
type Payload struct {
|
||||||
|
Exceptions []Exception `json:"exceptions,omitempty"`
|
||||||
|
Logs []Log `json:"logs,omitempty"`
|
||||||
|
Measurements []Measurement `json:"measurements,omitempty"`
|
||||||
|
Meta Meta `json:"meta,omitempty"`
|
||||||
|
Traces *Traces `json:"traces,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frame struct represents a single stacktrace frame
|
||||||
|
type Frame struct {
|
||||||
|
Function string `json:"function,omitempty"`
|
||||||
|
Module string `json:"module,omitempty"`
|
||||||
|
Filename string `json:"filename,omitempty"`
|
||||||
|
Lineno int `json:"lineno,omitempty"`
|
||||||
|
Colno int `json:"colno,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// String function converts a Frame into a human readable string
|
||||||
|
func (frame Frame) String() string {
|
||||||
|
module := ""
|
||||||
|
if len(frame.Module) > 0 {
|
||||||
|
module = frame.Module + "|"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\n at %s (%s%s:%v:%v)", frame.Function, module, frame.Filename, frame.Lineno, frame.Colno)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeKeyValWithPrefix will merge source in target, adding a prefix to each key being merged in
|
||||||
|
func MergeKeyValWithPrefix(target *KeyVal, source *KeyVal, prefix string) {
|
||||||
|
for el := source.Oldest(); el != nil; el = el.Next() {
|
||||||
|
target.Set(fmt.Sprintf("%s%s", prefix, el.Key), el.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stacktrace is a collection of Frames
|
||||||
|
type Stacktrace struct {
|
||||||
|
Frames []Frame `json:"frames,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exception struct controls all the data regarding an exception
|
||||||
|
type Exception struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Trace TraceContext `json:"trace,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message string is concatenating of the Exception.Type and Exception.Value
|
||||||
|
func (e Exception) Message() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Type, e.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is the string representation of an Exception
|
||||||
|
func (e Exception) String() string {
|
||||||
|
var stacktrace = e.Message()
|
||||||
|
if e.Stacktrace != nil {
|
||||||
|
for _, frame := range e.Stacktrace.Frames {
|
||||||
|
stacktrace += frame.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stacktrace
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal representation of the exception object
|
||||||
|
func (e Exception) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
KeyValAdd(kv, "timestamp", e.Timestamp.String())
|
||||||
|
KeyValAdd(kv, "kind", "exception")
|
||||||
|
KeyValAdd(kv, "type", e.Type)
|
||||||
|
KeyValAdd(kv, "value", e.Value)
|
||||||
|
KeyValAdd(kv, "stacktrace", e.String())
|
||||||
|
MergeKeyVal(kv, e.Trace.KeyVal())
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceContext holds trace id and span id associated to an entity (log, exception, measurement...).
|
||||||
|
type TraceContext struct {
|
||||||
|
TraceID string `json:"trace_id"`
|
||||||
|
SpanID string `json:"span_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal representation of the trace context object.
|
||||||
|
func (tc TraceContext) KeyVal() *KeyVal {
|
||||||
|
retv := NewKeyVal()
|
||||||
|
KeyValAdd(retv, "traceID", tc.TraceID)
|
||||||
|
KeyValAdd(retv, "spanID", tc.SpanID)
|
||||||
|
return retv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traces wraps the otel traces model.
|
||||||
|
type Traces struct {
|
||||||
|
otelpdata.Traces
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON unmarshals Traces model.
|
||||||
|
func (t *Traces) UnmarshalJSON(b []byte) error {
|
||||||
|
unmarshaler := otlp.NewJSONTracesUnmarshaler()
|
||||||
|
td, err := unmarshaler.UnmarshalTraces(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*t = Traces{td}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON marshals Traces model to json.
|
||||||
|
func (t Traces) MarshalJSON() ([]byte, error) {
|
||||||
|
marshaler := otlp.NewJSONTracesMarshaler()
|
||||||
|
return marshaler.MarshalTraces(t.Traces)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpanSlice unpacks Traces entity into a slice of Spans.
|
||||||
|
func (t Traces) SpanSlice() []otelpdata.Span {
|
||||||
|
spans := make([]otelpdata.Span, 0)
|
||||||
|
rss := t.ResourceSpans()
|
||||||
|
for i := 0; i < rss.Len(); i++ {
|
||||||
|
rs := rss.At(i)
|
||||||
|
ilss := rs.InstrumentationLibrarySpans()
|
||||||
|
for j := 0; j < ilss.Len(); j++ {
|
||||||
|
s := ilss.At(j).Spans()
|
||||||
|
for si := 0; si < s.Len(); si++ {
|
||||||
|
spans = append(spans, s.At(si))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return spans
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpanToKeyVal returns KeyVal representation of a Span.
|
||||||
|
func SpanToKeyVal(s otelpdata.Span) *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
if s.StartTimestamp() > 0 {
|
||||||
|
KeyValAdd(kv, "timestamp", s.StartTimestamp().AsTime().String())
|
||||||
|
}
|
||||||
|
if s.EndTimestamp() > 0 {
|
||||||
|
KeyValAdd(kv, "end_timestamp", s.StartTimestamp().AsTime().String())
|
||||||
|
}
|
||||||
|
KeyValAdd(kv, "kind", "span")
|
||||||
|
KeyValAdd(kv, "traceID", s.TraceID().HexString())
|
||||||
|
KeyValAdd(kv, "spanID", s.SpanID().HexString())
|
||||||
|
KeyValAdd(kv, "span_kind", s.Kind().String())
|
||||||
|
KeyValAdd(kv, "name", s.Name())
|
||||||
|
KeyValAdd(kv, "parent_spanID", s.ParentSpanID().HexString())
|
||||||
|
s.Attributes().Range(func(k string, v otelpdata.AttributeValue) bool {
|
||||||
|
KeyValAdd(kv, "attr_"+k, fmt.Sprintf("%v", v))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogLevel is log level enum for incoming app logs
|
||||||
|
type LogLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LogLevelTrace is "trace"
|
||||||
|
LogLevelTrace LogLevel = "trace"
|
||||||
|
// LogLevelDebug is "debug"
|
||||||
|
LogLevelDebug LogLevel = "debug"
|
||||||
|
// LogLevelInfo is "info"
|
||||||
|
LogLevelInfo LogLevel = "info"
|
||||||
|
// LogLevelWarning is "warning"
|
||||||
|
LogLevelWarning LogLevel = "warn"
|
||||||
|
// LogLevelError is "error"
|
||||||
|
LogLevelError LogLevel = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogContext is a string to string map structure that
|
||||||
|
// represents the context of a log message
|
||||||
|
type LogContext map[string]string
|
||||||
|
|
||||||
|
// Log struct controls the data that come into a Log message
|
||||||
|
type Log struct {
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
LogLevel LogLevel `json:"level,omitempty"`
|
||||||
|
Context LogContext `json:"context,omitempty"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Trace TraceContext `json:"trace,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal representation of a Log object
|
||||||
|
func (l Log) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
KeyValAdd(kv, "timestamp", l.Timestamp.String())
|
||||||
|
KeyValAdd(kv, "kind", "log")
|
||||||
|
KeyValAdd(kv, "message", l.Message)
|
||||||
|
KeyValAdd(kv, "level", string(l.LogLevel))
|
||||||
|
MergeKeyValWithPrefix(kv, KeyValFromMap(l.Context), "context_")
|
||||||
|
MergeKeyVal(kv, l.Trace.KeyVal())
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Log) KeyValContext() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
MergeKeyValWithPrefix(kv, KeyValFromMap(l.Context), "context_")
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measurement holds the data for user provided measurements
|
||||||
|
type Measurement struct {
|
||||||
|
Values map[string]float64 `json:"values,omitempty"`
|
||||||
|
Timestamp time.Time `json:"timestamp,omitempty"`
|
||||||
|
Trace TraceContext `json:"trace,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal representation of the Measurement object
|
||||||
|
func (m Measurement) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
|
||||||
|
KeyValAdd(kv, "timestamp", m.Timestamp.String())
|
||||||
|
KeyValAdd(kv, "kind", "measurement")
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(m.Values))
|
||||||
|
for k := range m.Values {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
KeyValAdd(kv, k, fmt.Sprintf("%f", m.Values[k]))
|
||||||
|
}
|
||||||
|
MergeKeyVal(kv, m.Trace.KeyVal())
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDK holds metadata about the app agent that produced the event
|
||||||
|
type SDK struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Integrations []SDKIntegration `json:"integrations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal produces key->value representation of Sdk metadata
|
||||||
|
func (sdk SDK) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
KeyValAdd(kv, "name", sdk.Name)
|
||||||
|
KeyValAdd(kv, "version", sdk.Version)
|
||||||
|
|
||||||
|
if len(sdk.Integrations) > 0 {
|
||||||
|
integrations := make([]string, len(sdk.Integrations))
|
||||||
|
|
||||||
|
for i, integration := range sdk.Integrations {
|
||||||
|
integrations[i] = integration.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValAdd(kv, "integrations", strings.Join(integrations, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// SDKIntegration holds metadata about a plugin/integration on the app agent that collected and sent the event
|
||||||
|
type SDKIntegration struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i SDKIntegration) String() string {
|
||||||
|
return fmt.Sprintf("%s:%s", i.Name, i.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User holds metadata about the user related to an app event
|
||||||
|
type User struct {
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Attributes map[string]string `json:"attributes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal produces a key->value representation User metadata
|
||||||
|
func (u User) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
KeyValAdd(kv, "email", u.Email)
|
||||||
|
KeyValAdd(kv, "id", u.ID)
|
||||||
|
KeyValAdd(kv, "username", u.Username)
|
||||||
|
MergeKeyValWithPrefix(kv, KeyValFromMap(u.Attributes), "attr_")
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta holds metadata about an app event
|
||||||
|
type Meta struct {
|
||||||
|
SDK SDK `json:"sdk,omitempty"`
|
||||||
|
App App `json:"app,omitempty"`
|
||||||
|
User User `json:"user,omitempty"`
|
||||||
|
Session Session `json:"session,omitempty"`
|
||||||
|
Page Page `json:"page,omitempty"`
|
||||||
|
Browser Browser `json:"browser,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal produces key->value representation of the app event metadatga
|
||||||
|
func (m Meta) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
MergeKeyValWithPrefix(kv, m.SDK.KeyVal(), "sdk_")
|
||||||
|
MergeKeyValWithPrefix(kv, m.App.KeyVal(), "app_")
|
||||||
|
MergeKeyValWithPrefix(kv, m.User.KeyVal(), "user_")
|
||||||
|
MergeKeyValWithPrefix(kv, m.Session.KeyVal(), "session_")
|
||||||
|
MergeKeyValWithPrefix(kv, m.Page.KeyVal(), "page_")
|
||||||
|
MergeKeyValWithPrefix(kv, m.Browser.KeyVal(), "browser_")
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session holds metadata about the browser session the event originates from
|
||||||
|
type Session struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Attributes map[string]string `json:"attributes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal produces key->value representation of the Session metadata
|
||||||
|
func (s Session) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
KeyValAdd(kv, "id", s.ID)
|
||||||
|
MergeKeyValWithPrefix(kv, KeyValFromMap(s.Attributes), "attr_")
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page holds metadata about the web page event originates from
|
||||||
|
type Page struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Attributes map[string]string `json:"attributes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal produces key->val representation of Page metadata
|
||||||
|
func (p Page) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
KeyValAdd(kv, "id", p.ID)
|
||||||
|
KeyValAdd(kv, "url", p.URL)
|
||||||
|
MergeKeyValWithPrefix(kv, KeyValFromMap(p.Attributes), "attr_")
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// App holds metadata about the application event originates from
|
||||||
|
type App struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Release string `json:"release,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Environment string `json:"environment,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal produces key-> value representation of App metadata
|
||||||
|
func (a App) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
KeyValAdd(kv, "name", a.Name)
|
||||||
|
KeyValAdd(kv, "release", a.Release)
|
||||||
|
KeyValAdd(kv, "version", a.Version)
|
||||||
|
KeyValAdd(kv, "environment", a.Environment)
|
||||||
|
return kv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser holds metadata about a client's browser
|
||||||
|
type Browser struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
OS string `json:"os,omitempty"`
|
||||||
|
Mobile bool `json:"mobile,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyVal produces key->value representation of the Browser metadata
|
||||||
|
func (b Browser) KeyVal() *KeyVal {
|
||||||
|
kv := NewKeyVal()
|
||||||
|
KeyValAdd(kv, "name", b.Name)
|
||||||
|
KeyValAdd(kv, "version", b.Version)
|
||||||
|
KeyValAdd(kv, "os", b.OS)
|
||||||
|
KeyValAdd(kv, "mobile", fmt.Sprintf("%v", b.Mobile))
|
||||||
|
return kv
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package frontendlogging
|
||||||
|
|
||||||
|
// ResolveSourceLocation resolves minified source location to original source location
|
||||||
|
func ResolveSourceLocation(store *SourceMapStore, frame *Frame) (*Frame, error) {
|
||||||
|
smap, err := store.getSourceMap(frame.Filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if smap == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, function, line, col, ok := smap.consumer.Source(frame.Lineno, frame.Colno)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unfortunately in many cases go-sourcemap fails to determine the original function name.
|
||||||
|
// not a big issue as long as file, line and column are correct
|
||||||
|
if len(function) == 0 {
|
||||||
|
function = "?"
|
||||||
|
}
|
||||||
|
return &Frame{
|
||||||
|
Filename: file,
|
||||||
|
Lineno: line,
|
||||||
|
Colno: col,
|
||||||
|
Function: function,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransformException will attempt to resolved all monified source locations in the stacktrace with original source locations
|
||||||
|
func TransformException(ex *Exception, store *SourceMapStore) *Exception {
|
||||||
|
if ex.Stacktrace == nil {
|
||||||
|
return ex
|
||||||
|
}
|
||||||
|
frames := []Frame{}
|
||||||
|
|
||||||
|
for _, frame := range ex.Stacktrace.Frames {
|
||||||
|
frame := frame
|
||||||
|
mappedFrame, err := ResolveSourceLocation(store, &frame)
|
||||||
|
if err != nil {
|
||||||
|
frames = append(frames, frame)
|
||||||
|
} else if mappedFrame != nil {
|
||||||
|
frames = append(frames, *mappedFrame)
|
||||||
|
} else {
|
||||||
|
frames = append(frames, frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Exception{
|
||||||
|
Type: ex.Type,
|
||||||
|
Value: ex.Value,
|
||||||
|
Stacktrace: &Stacktrace{Frames: frames},
|
||||||
|
Timestamp: ex.Timestamp,
|
||||||
|
}
|
||||||
|
}
|
@ -114,7 +114,7 @@ func (store *SourceMapStore) getSourceMap(sourceURL string) (*sourceMap, error)
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
path := strings.ReplaceAll(sourceMapLocation.path, "../", "") // just in case
|
path := strings.ReplaceAll(sourceMapLocation.path, "../", "") // just in case
|
||||||
b, err := store.readSourceMap(sourceMapLocation.dir, path)
|
content, err := store.readSourceMap(sourceMapLocation.dir, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// Cache nil value for sourceURL, since we want to flag that it wasn't found in the filesystem and not try again
|
// Cache nil value for sourceURL, since we want to flag that it wasn't found in the filesystem and not try again
|
||||||
@ -124,7 +124,7 @@ func (store *SourceMapStore) getSourceMap(sourceURL string) (*sourceMap, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
consumer, err := sourcemap.Parse(sourceURL+".map", b)
|
consumer, err := sourcemap.Parse(sourceURL+".map", content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -154,6 +154,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
|||||||
"rendererVersion": hs.RenderService.Version(),
|
"rendererVersion": hs.RenderService.Version(),
|
||||||
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
||||||
"sentry": hs.Cfg.Sentry,
|
"sentry": hs.Cfg.Sentry,
|
||||||
|
"grafanaJavascriptAgent": hs.Cfg.GrafanaJavascriptAgent,
|
||||||
"pluginCatalogURL": hs.Cfg.PluginCatalogURL,
|
"pluginCatalogURL": hs.Cfg.PluginCatalogURL,
|
||||||
"pluginAdminEnabled": hs.Cfg.PluginAdminEnabled,
|
"pluginAdminEnabled": hs.Cfg.PluginAdminEnabled,
|
||||||
"pluginAdminExternalManageEnabled": hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled,
|
"pluginAdminExternalManageEnabled": hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled,
|
||||||
|
@ -371,6 +371,9 @@ type Cfg struct {
|
|||||||
// Sentry config
|
// Sentry config
|
||||||
Sentry Sentry
|
Sentry Sentry
|
||||||
|
|
||||||
|
// GrafanaJavascriptAgent config
|
||||||
|
GrafanaJavascriptAgent GrafanaJavascriptAgent
|
||||||
|
|
||||||
// Data sources
|
// Data sources
|
||||||
DataSourceLimit int
|
DataSourceLimit int
|
||||||
|
|
||||||
@ -1054,6 +1057,7 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
|
|||||||
|
|
||||||
cfg.readDateFormats()
|
cfg.readDateFormats()
|
||||||
cfg.readSentryConfig()
|
cfg.readSentryConfig()
|
||||||
|
cfg.readGrafanaJavascriptAgentConfig()
|
||||||
|
|
||||||
if err := cfg.readLiveSettings(iniFile); err != nil {
|
if err := cfg.readLiveSettings(iniFile); err != nil {
|
||||||
return err
|
return err
|
||||||
|
29
pkg/setting/setting_grafana_javascript_agent.go
Normal file
29
pkg/setting/setting_grafana_javascript_agent.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
type GrafanaJavascriptAgent struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CustomEndpoint string `json:"customEndpoint"`
|
||||||
|
EndpointRPS int `json:"-"`
|
||||||
|
EndpointBurst int `json:"-"`
|
||||||
|
ErrorInstrumentalizationEnabled bool `json:"errorInstrumentalizationEnabled"`
|
||||||
|
ConsoleInstrumentalizationEnabled bool `json:"consoleInstrumentalizationEnabled"`
|
||||||
|
WebVitalsInstrumentalizationEnabled bool `json:"webVitalsInstrumentalizationEnabled"`
|
||||||
|
ApiKey string `json:"apiKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Cfg) readGrafanaJavascriptAgentConfig() {
|
||||||
|
raw := cfg.Raw.Section("log.frontend")
|
||||||
|
provider := raw.Key("provider").MustString("sentry")
|
||||||
|
if provider == "grafana" {
|
||||||
|
cfg.GrafanaJavascriptAgent = GrafanaJavascriptAgent{
|
||||||
|
Enabled: raw.Key("enabled").MustBool(true),
|
||||||
|
CustomEndpoint: raw.Key("custom_endpoint").MustString("/log-grafana-javascript-agent"),
|
||||||
|
EndpointRPS: raw.Key("log_endpoint_requests_per_second_limit").MustInt(3),
|
||||||
|
EndpointBurst: raw.Key("log_endpoint_burst_limit").MustInt(15),
|
||||||
|
ErrorInstrumentalizationEnabled: raw.Key("instrumentations_errors_enabled").MustBool(true),
|
||||||
|
ConsoleInstrumentalizationEnabled: raw.Key("instrumentations_console_enabled").MustBool(true),
|
||||||
|
WebVitalsInstrumentalizationEnabled: raw.Key("instrumentations_webvitals_enabled").MustBool(true),
|
||||||
|
ApiKey: raw.Key("api_key").String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,12 +11,15 @@ type Sentry struct {
|
|||||||
|
|
||||||
func (cfg *Cfg) readSentryConfig() {
|
func (cfg *Cfg) readSentryConfig() {
|
||||||
raw := cfg.Raw.Section("log.frontend")
|
raw := cfg.Raw.Section("log.frontend")
|
||||||
cfg.Sentry = Sentry{
|
provider := raw.Key("provider").MustString("sentry")
|
||||||
Enabled: raw.Key("enabled").MustBool(true),
|
if provider == "sentry" || provider != "grafana" {
|
||||||
DSN: raw.Key("sentry_dsn").String(),
|
cfg.Sentry = Sentry{
|
||||||
CustomEndpoint: raw.Key("custom_endpoint").String(),
|
Enabled: raw.Key("enabled").MustBool(true),
|
||||||
SampleRate: raw.Key("sample_rate").MustFloat64(),
|
DSN: raw.Key("sentry_dsn").String(),
|
||||||
EndpointRPS: raw.Key("log_endpoint_requests_per_second_limit").MustInt(),
|
CustomEndpoint: raw.Key("custom_endpoint").MustString("/log"),
|
||||||
EndpointBurst: raw.Key("log_endpoint_burst_limit").MustInt(),
|
SampleRate: raw.Key("sample_rate").MustFloat64(),
|
||||||
|
EndpointRPS: raw.Key("log_endpoint_requests_per_second_limit").MustInt(3),
|
||||||
|
EndpointBurst: raw.Key("log_endpoint_burst_limit").MustInt(15),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ import { PerformanceBackend } from './core/services/echo/backends/PerformanceBac
|
|||||||
import { ApplicationInsightsBackend } from './core/services/echo/backends/analytics/ApplicationInsightsBackend';
|
import { ApplicationInsightsBackend } from './core/services/echo/backends/analytics/ApplicationInsightsBackend';
|
||||||
import { GAEchoBackend } from './core/services/echo/backends/analytics/GABackend';
|
import { GAEchoBackend } from './core/services/echo/backends/analytics/GABackend';
|
||||||
import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend';
|
import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend';
|
||||||
|
import { GrafanaJavascriptAgentBackend } from './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend';
|
||||||
import { SentryEchoBackend } from './core/services/echo/backends/sentry/SentryBackend';
|
import { SentryEchoBackend } from './core/services/echo/backends/sentry/SentryBackend';
|
||||||
import { initDevFeatures } from './dev';
|
import { initDevFeatures } from './dev';
|
||||||
import { getTimeSrv } from './features/dashboard/services/TimeSrv';
|
import { getTimeSrv } from './features/dashboard/services/TimeSrv';
|
||||||
@ -203,6 +204,22 @@ function initEchoSrv() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (config.grafanaJavascriptAgent.enabled) {
|
||||||
|
registerEchoBackend(
|
||||||
|
new GrafanaJavascriptAgentBackend({
|
||||||
|
...config.grafanaJavascriptAgent,
|
||||||
|
app: {
|
||||||
|
version: config.buildInfo.version,
|
||||||
|
environment: config.buildInfo.env,
|
||||||
|
},
|
||||||
|
buildInfo: config.buildInfo,
|
||||||
|
user: {
|
||||||
|
id: String(config.bootData.user?.id),
|
||||||
|
email: config.bootData.user?.email,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (config.googleAnalyticsId) {
|
if (config.googleAnalyticsId) {
|
||||||
registerEchoBackend(
|
registerEchoBackend(
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { BaseTransport, TransportItem } from '@grafana/agent-core';
|
||||||
|
import { getEchoSrv, EchoEventType } from '@grafana/runtime';
|
||||||
|
export class EchoSrvTransport extends BaseTransport {
|
||||||
|
send(event: TransportItem) {
|
||||||
|
getEchoSrv().addEvent({
|
||||||
|
type: EchoEventType.GrafanaJavascriptAgent,
|
||||||
|
payload: event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getIgnoreUrls() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,229 @@
|
|||||||
|
import { BaseTransport } from '@grafana/agent-core';
|
||||||
|
import { FetchTransport, initializeAgent } from '@grafana/agent-web';
|
||||||
|
import { BuildInfo } from '@grafana/data';
|
||||||
|
import { GrafanaEdition } from '@grafana/data/src/types/config';
|
||||||
|
import { EchoEventType, EchoMeta } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { GrafanaJavascriptAgentBackend, GrafanaJavascriptAgentBackendOptions } from './GrafanaJavascriptAgentBackend';
|
||||||
|
import { GrafanaJavascriptAgentEchoEvent } from './types';
|
||||||
|
|
||||||
|
jest.mock('@grafana/agent-web', () => {
|
||||||
|
const originalModule = jest.requireActual('@grafana/agent-web');
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...originalModule,
|
||||||
|
initializeAgent: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GrafanaJavascriptAgentEchoBackend', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
window.fetch = jest.fn();
|
||||||
|
jest.resetModules();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const buildInfo: BuildInfo = {
|
||||||
|
version: '1.0',
|
||||||
|
commit: 'abcd123',
|
||||||
|
env: 'production',
|
||||||
|
edition: GrafanaEdition.OpenSource,
|
||||||
|
latestVersion: 'ba',
|
||||||
|
hasUpdate: false,
|
||||||
|
hideVersion: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: GrafanaJavascriptAgentBackendOptions = {
|
||||||
|
buildInfo,
|
||||||
|
app: {
|
||||||
|
version: '1.0',
|
||||||
|
},
|
||||||
|
errorInstrumentalizationEnabled: true,
|
||||||
|
consoleInstrumentalizationEnabled: true,
|
||||||
|
webVitalsInstrumentalizationEnabled: true,
|
||||||
|
customEndpoint: '/log-grafana-javascript-agent',
|
||||||
|
user: {
|
||||||
|
email: 'darth.vader@sith.glx',
|
||||||
|
id: '504',
|
||||||
|
orgId: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('will set up FetchTransport if customEndpoint is provided', async () => {
|
||||||
|
// arrange
|
||||||
|
const originalModule = jest.requireActual('@grafana/agent-web');
|
||||||
|
jest.mocked(initializeAgent).mockImplementation(originalModule.initializeAgent);
|
||||||
|
|
||||||
|
//act
|
||||||
|
const backend = new GrafanaJavascriptAgentBackend(options);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
expect(backend.transports.length).toEqual(1);
|
||||||
|
expect(backend.transports[0]).toBeInstanceOf(FetchTransport);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will initialize GrafanaJavascriptAgent and set user', async () => {
|
||||||
|
// arrange
|
||||||
|
const mockedSetUser = jest.fn();
|
||||||
|
const mockedAgent = () => {
|
||||||
|
return {
|
||||||
|
api: {
|
||||||
|
setUser: mockedSetUser,
|
||||||
|
pushLog: jest.fn(),
|
||||||
|
callOriginalConsoleMethod: jest.fn(),
|
||||||
|
pushError: jest.fn(),
|
||||||
|
pushMeasurement: jest.fn(),
|
||||||
|
pushTraces: jest.fn(),
|
||||||
|
initOTEL: jest.fn(),
|
||||||
|
getOTEL: jest.fn(),
|
||||||
|
getTraceContext: jest.fn(),
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
globalObjectKey: '',
|
||||||
|
instrumentations: [],
|
||||||
|
preventGlobalExposure: false,
|
||||||
|
transports: [],
|
||||||
|
metas: [],
|
||||||
|
parseStacktrace: jest.fn(),
|
||||||
|
app: jest.fn(),
|
||||||
|
},
|
||||||
|
metas: {
|
||||||
|
add: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
value: {},
|
||||||
|
},
|
||||||
|
transports: {
|
||||||
|
add: jest.fn(),
|
||||||
|
execute: jest.fn(),
|
||||||
|
transports: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
jest.mocked(initializeAgent).mockImplementation(mockedAgent);
|
||||||
|
|
||||||
|
//act
|
||||||
|
new GrafanaJavascriptAgentBackend(options);
|
||||||
|
|
||||||
|
//assert
|
||||||
|
expect(initializeAgent).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockedSetUser).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockedSetUser).toHaveBeenCalledWith({
|
||||||
|
id: '504',
|
||||||
|
email: 'darth.vader@sith.glx',
|
||||||
|
attributes: {
|
||||||
|
orgId: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('will forward events to transports', async () => {
|
||||||
|
//arrange
|
||||||
|
const mockedSetUser = jest.fn();
|
||||||
|
const mockedAgent = () => {
|
||||||
|
return {
|
||||||
|
api: {
|
||||||
|
setUser: mockedSetUser,
|
||||||
|
pushLog: jest.fn(),
|
||||||
|
callOriginalConsoleMethod: jest.fn(),
|
||||||
|
pushError: jest.fn(),
|
||||||
|
pushMeasurement: jest.fn(),
|
||||||
|
pushTraces: jest.fn(),
|
||||||
|
initOTEL: jest.fn(),
|
||||||
|
getOTEL: jest.fn(),
|
||||||
|
getTraceContext: jest.fn(),
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
globalObjectKey: '',
|
||||||
|
instrumentations: [],
|
||||||
|
preventGlobalExposure: false,
|
||||||
|
transports: [],
|
||||||
|
metas: [],
|
||||||
|
parseStacktrace: jest.fn(),
|
||||||
|
app: jest.fn(),
|
||||||
|
},
|
||||||
|
metas: {
|
||||||
|
add: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
value: {},
|
||||||
|
},
|
||||||
|
transports: {
|
||||||
|
add: jest.fn(),
|
||||||
|
execute: jest.fn(),
|
||||||
|
transports: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mocked(initializeAgent).mockImplementation(mockedAgent);
|
||||||
|
const backend = new GrafanaJavascriptAgentBackend({
|
||||||
|
...options,
|
||||||
|
preventGlobalExposure: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
backend.transports = [
|
||||||
|
/* eslint-disable */
|
||||||
|
{ send: jest.fn() } as unknown as BaseTransport,
|
||||||
|
{ send: jest.fn() } as unknown as BaseTransport,
|
||||||
|
];
|
||||||
|
const event: GrafanaJavascriptAgentEchoEvent = {
|
||||||
|
type: EchoEventType.GrafanaJavascriptAgent,
|
||||||
|
payload: { foo: 'bar' } as unknown as GrafanaJavascriptAgentEchoEvent,
|
||||||
|
meta: {} as unknown as EchoMeta,
|
||||||
|
};
|
||||||
|
/* eslint-enable */
|
||||||
|
backend.addEvent(event);
|
||||||
|
backend.transports.forEach((transport) => {
|
||||||
|
expect(transport.send).toHaveBeenCalledTimes(1);
|
||||||
|
expect(transport.send).toHaveBeenCalledWith(event.payload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//@FIXME - make integration test work
|
||||||
|
|
||||||
|
// it('integration test with EchoSrv and GrafanaJavascriptAgent', async () => {
|
||||||
|
// // sets up the whole thing between window.onerror and backend endpoint call, checks that error is reported
|
||||||
|
// // use actual GrafanaJavascriptAgent & mock window.fetch
|
||||||
|
|
||||||
|
// // arrange
|
||||||
|
// const originalModule = jest.requireActual('@grafana/agent-web');
|
||||||
|
// jest.mocked(initializeAgent).mockImplementation(originalModule.initializeAgent);
|
||||||
|
// const fetchSpy = (window.fetch = jest.fn());
|
||||||
|
// fetchSpy.mockResolvedValue({ status: 200 } as Response);
|
||||||
|
// const echo = new Echo({ debug: true });
|
||||||
|
|
||||||
|
// // act
|
||||||
|
// setEchoSrv(echo);
|
||||||
|
// const grafanaJavascriptAgentBackend = new GrafanaJavascriptAgentBackend({
|
||||||
|
// ...options,
|
||||||
|
// preventGlobalExposure: true,
|
||||||
|
// consoleInstrumentalizationEnabled: false,
|
||||||
|
// webVitalsInstrumentalizationEnabled: false,
|
||||||
|
// });
|
||||||
|
// echo.addBackend(grafanaJavascriptAgentBackend);
|
||||||
|
|
||||||
|
// // lets add another echo backend for grafana javascript agent events for good measure
|
||||||
|
// const myCustomErrorBackend: EchoBackend = {
|
||||||
|
// supportedEvents: [EchoEventType.GrafanaJavascriptAgent],
|
||||||
|
// flush: () => {},
|
||||||
|
// options: {},
|
||||||
|
// addEvent: jest.fn(),
|
||||||
|
// };
|
||||||
|
// echo.addBackend(myCustomErrorBackend);
|
||||||
|
|
||||||
|
// // fire off an error using global error handler, Grafana Javascript Agent should pick it up
|
||||||
|
// const error = new Error('test error');
|
||||||
|
// window.onerror!(error.message, undefined, undefined, undefined, error);
|
||||||
|
|
||||||
|
// // assert
|
||||||
|
// // check that error was reported to backend
|
||||||
|
// await waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(1));
|
||||||
|
// const [url, reqInit]: [string, RequestInit] = fetchSpy.mock.calls[0];
|
||||||
|
// expect(url).toEqual('/log-grafana-javascript-agent');
|
||||||
|
// // expect((JSON.parse(reqInit.body as string) as EchoEvent).exception!.values![0].value).toEqual('test error');
|
||||||
|
// console.log(JSON.parse(reqInit.body as string));
|
||||||
|
|
||||||
|
// // check that our custom backend got it too
|
||||||
|
// expect(myCustomErrorBackend.addEvent).toHaveBeenCalledTimes(1);
|
||||||
|
// });
|
||||||
|
});
|
@ -0,0 +1,84 @@
|
|||||||
|
import { BaseTransport } from '@grafana/agent-core';
|
||||||
|
import {
|
||||||
|
initializeAgent,
|
||||||
|
BrowserConfig,
|
||||||
|
ErrorsInstrumentation,
|
||||||
|
ConsoleInstrumentation,
|
||||||
|
WebVitalsInstrumentation,
|
||||||
|
FetchTransport,
|
||||||
|
} from '@grafana/agent-web';
|
||||||
|
import { BuildInfo } from '@grafana/data';
|
||||||
|
import { EchoBackend, EchoEvent, EchoEventType } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { EchoSrvTransport } from './EchoSrvTransport';
|
||||||
|
import { GrafanaJavascriptAgentEchoEvent, User } from './types';
|
||||||
|
|
||||||
|
export interface GrafanaJavascriptAgentBackendOptions extends BrowserConfig {
|
||||||
|
buildInfo: BuildInfo;
|
||||||
|
customEndpoint: string;
|
||||||
|
user: User;
|
||||||
|
errorInstrumentalizationEnabled: boolean;
|
||||||
|
consoleInstrumentalizationEnabled: boolean;
|
||||||
|
webVitalsInstrumentalizationEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GrafanaJavascriptAgentBackend
|
||||||
|
implements EchoBackend<GrafanaJavascriptAgentEchoEvent, GrafanaJavascriptAgentBackendOptions>
|
||||||
|
{
|
||||||
|
supportedEvents = [EchoEventType.GrafanaJavascriptAgent];
|
||||||
|
private agentInstance;
|
||||||
|
transports: BaseTransport[];
|
||||||
|
|
||||||
|
constructor(public options: GrafanaJavascriptAgentBackendOptions) {
|
||||||
|
// configure instrumentalizations
|
||||||
|
const instrumentations = [];
|
||||||
|
this.transports = [];
|
||||||
|
|
||||||
|
if (options.customEndpoint) {
|
||||||
|
this.transports.push(new FetchTransport({ url: options.customEndpoint, apiKey: options.apiKey }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.errorInstrumentalizationEnabled) {
|
||||||
|
instrumentations.push(new ErrorsInstrumentation());
|
||||||
|
}
|
||||||
|
if (options.consoleInstrumentalizationEnabled) {
|
||||||
|
instrumentations.push(new ConsoleInstrumentation());
|
||||||
|
}
|
||||||
|
if (options.webVitalsInstrumentalizationEnabled) {
|
||||||
|
instrumentations.push(new WebVitalsInstrumentation());
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize GrafanaJavascriptAgent so it can set up it's hooks and start collecting errors
|
||||||
|
const grafanaJavaScriptAgentOptions: BrowserConfig = {
|
||||||
|
globalObjectKey: options.globalObjectKey || 'grafanaAgent',
|
||||||
|
preventGlobalExposure: options.preventGlobalExposure || false,
|
||||||
|
app: {
|
||||||
|
version: options.buildInfo.version,
|
||||||
|
environment: options.buildInfo.env,
|
||||||
|
},
|
||||||
|
instrumentations,
|
||||||
|
transports: [new EchoSrvTransport()],
|
||||||
|
};
|
||||||
|
this.agentInstance = initializeAgent(grafanaJavaScriptAgentOptions);
|
||||||
|
|
||||||
|
if (options.user) {
|
||||||
|
this.agentInstance.api.setUser({
|
||||||
|
email: options.user.email,
|
||||||
|
id: options.user.id,
|
||||||
|
attributes: {
|
||||||
|
orgId: String(options.user.orgId) || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEvent = (e: EchoEvent) => {
|
||||||
|
this.transports.forEach((t) => t.send(e.payload));
|
||||||
|
};
|
||||||
|
|
||||||
|
// backend will log events to stdout, and at least in case of hosted grafana they will be
|
||||||
|
// ingested into Loki. Due to Loki limitations logs cannot be backdated,
|
||||||
|
// so not using buffering for this backend to make sure that events are logged as close
|
||||||
|
// to their context as possible
|
||||||
|
flush = () => {};
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { CurrentUserDTO } from '@grafana/data';
|
||||||
|
import { EchoEvent, EchoEventType } from '@grafana/runtime';
|
||||||
|
|
||||||
|
export interface BaseTransport {
|
||||||
|
sendEvent(event: EchoEvent): PromiseLike<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GrafanaJavascriptAgentEchoEvent = EchoEvent<EchoEventType.GrafanaJavascriptAgent>;
|
||||||
|
|
||||||
|
export interface User extends Pick<CurrentUserDTO, 'email'> {
|
||||||
|
id: string;
|
||||||
|
orgId?: number;
|
||||||
|
}
|
118
yarn.lock
118
yarn.lock
@ -4298,6 +4298,29 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@grafana/agent-core@npm:^0.3.0":
|
||||||
|
version: 0.3.0
|
||||||
|
resolution: "@grafana/agent-core@npm:0.3.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/api": ^1.1.0
|
||||||
|
"@opentelemetry/api-metrics": ^0.29.1
|
||||||
|
"@opentelemetry/otlp-transformer": ^0.29.1
|
||||||
|
uuid: ^8.3.2
|
||||||
|
checksum: 664ded4d6bbe466cf6629fc0fb2c751b488662a12c25b620872939472ae51a7360237f3d669ca900ce5157c8575f312a64328ef03311b075b3a3937bc896d9e8
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@grafana/agent-web@npm:^0.3.0":
|
||||||
|
version: 0.3.0
|
||||||
|
resolution: "@grafana/agent-web@npm:0.3.0"
|
||||||
|
dependencies:
|
||||||
|
"@grafana/agent-core": ^0.3.0
|
||||||
|
ua-parser-js: ^1.0.2
|
||||||
|
web-vitals: ^2.1.4
|
||||||
|
checksum: 5abbb1e17afbb8fd2944a7ffd14a10c701fdd11a6c80c986b4039a8abe36dc1647513493acc7898fd373a20c05b02ce3c8cc6c18ad5e3f47980cd4dd50373e6e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@grafana/api-documenter@npm:7.11.2":
|
"@grafana/api-documenter@npm:7.11.2":
|
||||||
version: 7.11.2
|
version: 7.11.2
|
||||||
resolution: "@grafana/api-documenter@npm:7.11.2"
|
resolution: "@grafana/api-documenter@npm:7.11.2"
|
||||||
@ -4493,6 +4516,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@grafana/runtime@workspace:packages/grafana-runtime"
|
resolution: "@grafana/runtime@workspace:packages/grafana-runtime"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@grafana/agent-web": ^0.3.0
|
||||||
"@grafana/data": 9.1.0-pre
|
"@grafana/data": 9.1.0-pre
|
||||||
"@grafana/e2e-selectors": 9.1.0-pre
|
"@grafana/e2e-selectors": 9.1.0-pre
|
||||||
"@grafana/tsconfig": ^1.2.0-rc1
|
"@grafana/tsconfig": ^1.2.0-rc1
|
||||||
@ -7112,7 +7136,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/api@npm:1.1.0":
|
"@opentelemetry/api-metrics@npm:0.29.2, @opentelemetry/api-metrics@npm:^0.29.1":
|
||||||
|
version: 0.29.2
|
||||||
|
resolution: "@opentelemetry/api-metrics@npm:0.29.2"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/api": ^1.0.0
|
||||||
|
checksum: 6197a1f05c8bfc72db7052b65d0612155f675282d796f4566fc1f99228f6c0b21df52bf9d865456992298d1a1720ea58dd79ec4b27b85563ec13f820dcaf2d3a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/api@npm:1.1.0, @opentelemetry/api@npm:^1.0.0, @opentelemetry/api@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "@opentelemetry/api@npm:1.1.0"
|
resolution: "@opentelemetry/api@npm:1.1.0"
|
||||||
checksum: 8be8e8dd20a473639a9bb9b4185b8984f537f86e49829ba1d4c4e909f4480309cb22696b7eb7122882878dac0b5f4ce799d66ed72248568bafed085d6269e1bc
|
checksum: 8be8e8dd20a473639a9bb9b4185b8984f537f86e49829ba1d4c4e909f4480309cb22696b7eb7122882878dac0b5f4ce799d66ed72248568bafed085d6269e1bc
|
||||||
@ -7131,6 +7164,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/core@npm:1.3.1":
|
||||||
|
version: 1.3.1
|
||||||
|
resolution: "@opentelemetry/core@npm:1.3.1"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/semantic-conventions": 1.3.1
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.2.0"
|
||||||
|
checksum: a59d0e8b7af2054d4b741a076abb992fb9bb241b1e7e2563a7d03b8a810155eb5c5c4eab28ebae39ce67d9ae66ae2a8d5d038de3b7ddc6301ac636840ceb876c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/exporter-collector@npm:0.25.0":
|
"@opentelemetry/exporter-collector@npm:0.25.0":
|
||||||
version: 0.25.0
|
version: 0.25.0
|
||||||
resolution: "@opentelemetry/exporter-collector@npm:0.25.0"
|
resolution: "@opentelemetry/exporter-collector@npm:0.25.0"
|
||||||
@ -7146,6 +7190,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/otlp-transformer@npm:^0.29.1":
|
||||||
|
version: 0.29.2
|
||||||
|
resolution: "@opentelemetry/otlp-transformer@npm:0.29.2"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/api-metrics": 0.29.2
|
||||||
|
"@opentelemetry/core": 1.3.1
|
||||||
|
"@opentelemetry/resources": 1.3.1
|
||||||
|
"@opentelemetry/sdk-metrics-base": 0.29.2
|
||||||
|
"@opentelemetry/sdk-trace-base": 1.3.1
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.2.0"
|
||||||
|
checksum: e86dc023c96fcf2faa72d47300bb43d2820a5f3f75446d17b109c4a16accec2257642dbe0856ec65ef4c6a9ba9ac551ace5a429bd7222da64563acd7002f8698
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/resources@npm:0.25.0":
|
"@opentelemetry/resources@npm:0.25.0":
|
||||||
version: 0.25.0
|
version: 0.25.0
|
||||||
resolution: "@opentelemetry/resources@npm:0.25.0"
|
resolution: "@opentelemetry/resources@npm:0.25.0"
|
||||||
@ -7158,6 +7217,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/resources@npm:1.3.1":
|
||||||
|
version: 1.3.1
|
||||||
|
resolution: "@opentelemetry/resources@npm:1.3.1"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/core": 1.3.1
|
||||||
|
"@opentelemetry/semantic-conventions": 1.3.1
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.2.0"
|
||||||
|
checksum: 2aeb76e23364f2ede34d27c912d3489e219dbdb430c7ae33e30c38d4451f05fc58c0ff05332a8eac428d889f456692450417b8477585089cd047165fb25681ea
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/sdk-metrics-base@npm:0.25.0":
|
"@opentelemetry/sdk-metrics-base@npm:0.25.0":
|
||||||
version: 0.25.0
|
version: 0.25.0
|
||||||
resolution: "@opentelemetry/sdk-metrics-base@npm:0.25.0"
|
resolution: "@opentelemetry/sdk-metrics-base@npm:0.25.0"
|
||||||
@ -7172,6 +7243,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/sdk-metrics-base@npm:0.29.2":
|
||||||
|
version: 0.29.2
|
||||||
|
resolution: "@opentelemetry/sdk-metrics-base@npm:0.29.2"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/api-metrics": 0.29.2
|
||||||
|
"@opentelemetry/core": 1.3.1
|
||||||
|
"@opentelemetry/resources": 1.3.1
|
||||||
|
lodash.merge: 4.6.2
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ^1.0.0
|
||||||
|
checksum: 3518b881991ce13bc1a93346889bb4e5e9581528b511151981049b60bf78acf85f44701597e303f23629f2282ba36ac26d55c39740ef288e227d8253658933aa
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/sdk-trace-base@npm:0.25.0":
|
"@opentelemetry/sdk-trace-base@npm:0.25.0":
|
||||||
version: 0.25.0
|
version: 0.25.0
|
||||||
resolution: "@opentelemetry/sdk-trace-base@npm:0.25.0"
|
resolution: "@opentelemetry/sdk-trace-base@npm:0.25.0"
|
||||||
@ -7186,6 +7271,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@opentelemetry/sdk-trace-base@npm:1.3.1":
|
||||||
|
version: 1.3.1
|
||||||
|
resolution: "@opentelemetry/sdk-trace-base@npm:1.3.1"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/core": 1.3.1
|
||||||
|
"@opentelemetry/resources": 1.3.1
|
||||||
|
"@opentelemetry/semantic-conventions": 1.3.1
|
||||||
|
peerDependencies:
|
||||||
|
"@opentelemetry/api": ">=1.0.0 <1.2.0"
|
||||||
|
checksum: 9f3074f226854ff285e15d1f636f6c912b9760306c3617fd731dc2c9cb4816d59b7b74185ce1d3be9131d4852a881599cbba2d721cc02871bdf2827a028f7a62
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/semantic-conventions@npm:0.25.0":
|
"@opentelemetry/semantic-conventions@npm:0.25.0":
|
||||||
version: 0.25.0
|
version: 0.25.0
|
||||||
resolution: "@opentelemetry/semantic-conventions@npm:0.25.0"
|
resolution: "@opentelemetry/semantic-conventions@npm:0.25.0"
|
||||||
@ -20681,6 +20779,8 @@ __metadata:
|
|||||||
"@emotion/css": 11.9.0
|
"@emotion/css": 11.9.0
|
||||||
"@emotion/eslint-plugin": 11.7.0
|
"@emotion/eslint-plugin": 11.7.0
|
||||||
"@emotion/react": 11.9.0
|
"@emotion/react": 11.9.0
|
||||||
|
"@grafana/agent-core": ^0.3.0
|
||||||
|
"@grafana/agent-web": ^0.3.0
|
||||||
"@grafana/api-documenter": 7.11.2
|
"@grafana/api-documenter": 7.11.2
|
||||||
"@grafana/aws-sdk": 0.0.36
|
"@grafana/aws-sdk": 0.0.36
|
||||||
"@grafana/data": "workspace:*"
|
"@grafana/data": "workspace:*"
|
||||||
@ -25261,7 +25361,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lodash.merge@npm:^4.6.2":
|
"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.2":
|
||||||
version: 4.6.2
|
version: 4.6.2
|
||||||
resolution: "lodash.merge@npm:4.6.2"
|
resolution: "lodash.merge@npm:4.6.2"
|
||||||
checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005
|
checksum: ad580b4bdbb7ca1f7abf7e1bce63a9a0b98e370cf40194b03380a46b4ed799c9573029599caebc1b14e3f24b111aef72b96674a56cfa105e0f5ac70546cdc005
|
||||||
@ -35632,6 +35732,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ua-parser-js@npm:^1.0.2":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "ua-parser-js@npm:1.0.2"
|
||||||
|
checksum: ff7f6d79a9c1a38aa85a0e751040fc7e17a0b621bda876838d14ebe55aca4e50e68da0350f181e58801c2d8a35e7db4e12473776e558910c4b7cabcec96aa3bf
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"uglify-js@npm:^3.1.4":
|
"uglify-js@npm:^3.1.4":
|
||||||
version: 3.14.2
|
version: 3.14.2
|
||||||
resolution: "uglify-js@npm:3.14.2"
|
resolution: "uglify-js@npm:3.14.2"
|
||||||
@ -36469,6 +36576,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"web-vitals@npm:^2.1.4":
|
||||||
|
version: 2.1.4
|
||||||
|
resolution: "web-vitals@npm:2.1.4"
|
||||||
|
checksum: 03d3f47dbf55c3dce07beb0ff5de8ddd52e2d0a53a8df5c84e7a16dda93543341d67231fa79b1d9772b091419af4ec0fd395b8bcf451a0e26846e3f76b3d0efc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"web-worker@npm:^1.2.0":
|
"web-worker@npm:^1.2.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "web-worker@npm:1.2.0"
|
resolution: "web-worker@npm:1.2.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user