Frontend Logging: Integrate grafana javascript agent (#50801)
Add Grafana Javascript Agent integration to Grafana
This commit is contained in:
@@ -616,4 +616,6 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
sourceMapStore := frontendlogging.NewSourceMapStore(hs.Cfg, hs.pluginStaticRouteResolver, frontendlogging.ReadSourceMapFromFS)
|
||||
r.Post("/log", middleware.RateLimit(hs.Cfg.Sentry.EndpointRPS, hs.Cfg.Sentry.EndpointBurst, time.Now),
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
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{}) {
|
||||
assert.Contains(t, logRecord, label)
|
||||
assert.Equal(t, value, logRecord[label])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
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 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
|
||||
@@ -124,7 +124,7 @@ func (store *SourceMapStore) getSourceMap(sourceURL string) (*sourceMap, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
consumer, err := sourcemap.Parse(sourceURL+".map", b)
|
||||
consumer, err := sourcemap.Parse(sourceURL+".map", content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"rendererVersion": hs.RenderService.Version(),
|
||||
"http2Enabled": hs.Cfg.Protocol == setting.HTTP2Scheme,
|
||||
"sentry": hs.Cfg.Sentry,
|
||||
"grafanaJavascriptAgent": hs.Cfg.GrafanaJavascriptAgent,
|
||||
"pluginCatalogURL": hs.Cfg.PluginCatalogURL,
|
||||
"pluginAdminEnabled": hs.Cfg.PluginAdminEnabled,
|
||||
"pluginAdminExternalManageEnabled": hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled,
|
||||
|
||||
Reference in New Issue
Block a user