mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 17:43:35 -06:00
* add core plugin flow * add instrumentation * move func * remove cruft * support external backend plugins * refactor + clean up * remove comments * refactor loader * simplify core plugin path arg * cleanup loggers * move signature validator to plugins package * fix sig packaging * cleanup plugin model * remove unnecessary plugin field * add start+stop for pm * fix failures * add decommissioned state * export fields just to get things flowing * fix comments * set static routes * make image loading idempotent * merge with backend plugin manager * re-use funcs * reorder imports + remove unnecessary interface * add some TODOs + remove unused func * remove unused instrumentation func * simplify client usage * remove import alias * re-use backendplugin.Plugin interface * re order funcs * improve var name * fix log statements * refactor data model * add logic for dupe check during loading * cleanup state setting * refactor loader * cleanup manager interface * add rendering flow * refactor loading + init * add renderer support * fix renderer plugin * reformat imports * track errors * fix plugin signature inheritance * name param in interface * update func comment * fix func arg name * introduce class concept * remove func * fix external plugin check * apply changes from pm-experiment * fix core plugins * fix imports * rename interface * comment API interface * add support for testdata plugin * enable alerting + use correct core plugin contracts * slim manager API * fix param name * fix filter * support static routes * fix rendering * tidy rendering * get tests compiling * fix install+uninstall * start finder test * add finder test coverage * start loader tests * add test for core plugins * load core + bundled test * add test for nested plugin loading * add test files * clean interface + fix registering some core plugins * refactoring * reformat and create sub packages * simplify core plugin init * fix ctx cancel scenario * migrate initializer * remove Init() funcs * add test starter * new logger * flesh out initializer tests * refactoring * remove unused svc * refactor rendering flow * fixup loader tests * add enabled helper func * fix logger name * fix data fetchers * fix case where plugin dir doesn't exist * improve coverage + move dupe checking to loader * remove noisy debug logs * register core plugins automagically * add support for renderer in catalog * make private func + fix req validation * use interface * re-add check for renderer in catalog * tidy up from moving to auto reg core plugins * core plugin registrar * guards * copy over core plugins for test infra * all tests green * renames * propagate new interfaces * kill old manager * get compiling * tidy up * update naming * refactor manager test + cleanup * add more cases to finder test * migrate validator to field * more coverage * refactor dupe checking * add test for plugin class * add coverage for initializer * split out rendering * move * fixup tests * fix uss test * fix frontend settings * fix grafanads test * add check when checking sig errors * fix enabled map * fixup * allow manual setup of CM * rename to cloud-monitoring * remove TODO * add installer interface for testing * loader interface returns * tests passing * refactor + add more coverage * support 'stackdriver' * fix frontend settings loading * improve naming based on package name * small tidy * refactor test * fix renderer start * make cloud-monitoring plugin ID clearer * add plugin update test * add integration tests * don't break all if sig can't be calculated * add root URL check test * add more signature verification tests * update DTO name * update enabled plugins comment * update comments * fix linter * revert fe naming change * fix errors endpoint * reset error code field name * re-order test to help verify * assert -> require * pm check * add missing entry + re-order * re-check * dump icon log * verify manager contents first * reformat * apply PR feedback * apply style changes * fix one vs all loading err * improve log output * only start when no signature error * move log * rework plugin update check * fix test * fix multi loading from cfg.PluginSettings * improve log output #2 * add error abstraction to capture errors without registering a plugin * add debug log * add unsigned warning * e2e test attempt * fix logger * set home path * prevent panic * alternate * ugh.. fix home path * return renderer even if not started * make renderer plugin managed * add fallback renderer icon, update renderer badge + prevent changes when renderer is installed * fix icon loading * rollback renderer changes * use correct field * remove unneccessary block * remove newline * remove unused func * fix bundled plugins base + module fields * remove unused field since refactor * add authorizer abstraction * loader only returns plugins expected to run * fix multi log output
323 lines
10 KiB
Go
323 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/grafana/grafana/pkg/api/frontendlogging"
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
"github.com/grafana/grafana/pkg/api/routing"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
log "github.com/inconshreveable/log15"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type SourceMapReadRecord struct {
|
|
dir string
|
|
path string
|
|
}
|
|
|
|
type logScenarioFunc func(c *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord)
|
|
|
|
func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.FrontendSentryEvent, fn logScenarioFunc) {
|
|
t.Run(desc, func(t *testing.T) {
|
|
logs := []*log.Record{}
|
|
sourceMapReads := []SourceMapReadRecord{}
|
|
|
|
origHandler := frontendLogger.GetHandler()
|
|
frontendLogger.SetHandler(log.FuncHandler(func(r *log.Record) error {
|
|
logs = append(logs, r)
|
|
return nil
|
|
}))
|
|
t.Cleanup(func() {
|
|
frontendLogger.SetHandler(origHandler)
|
|
})
|
|
|
|
sc := setupScenarioContext(t, "/log")
|
|
|
|
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 := NewFrontendLogMessageHandler(sourceMapStore)
|
|
|
|
handler := routing.Wrap(func(w http.ResponseWriter, c *models.ReqContext) response.Response {
|
|
sc.context = c
|
|
return loggingHandler(c, event)
|
|
})
|
|
|
|
sc.m.Post(sc.url, handler)
|
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
|
fn(sc, logs, sourceMapReads)
|
|
})
|
|
}
|
|
|
|
func TestFrontendLoggingEndpoint(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("FrontendLoggingEndpoint", func(t *testing.T) {
|
|
request := sentry.Request{
|
|
URL: "http://localhost:3000/",
|
|
Headers: map[string]string{
|
|
"User-Agent": "Chrome",
|
|
},
|
|
}
|
|
|
|
user := sentry.User{
|
|
Email: "geralt@kaermorhen.com",
|
|
ID: "45",
|
|
}
|
|
|
|
event := sentry.Event{
|
|
EventID: "123",
|
|
Level: sentry.LevelError,
|
|
Request: &request,
|
|
Timestamp: ts,
|
|
}
|
|
|
|
errorEvent := frontendlogging.FrontendSentryEvent{
|
|
Event: &event,
|
|
Exception: &frontendlogging.FrontendSentryException{
|
|
Values: []frontendlogging.FrontendSentryExceptionValue{
|
|
{
|
|
Type: "UserError",
|
|
Value: "Please replace user and try again",
|
|
Stacktrace: sentry.Stacktrace{
|
|
Frames: []sentry.Frame{
|
|
{
|
|
Function: "foofn",
|
|
Filename: "foo.js",
|
|
Lineno: 123,
|
|
Colno: 23,
|
|
},
|
|
{
|
|
Function: "barfn",
|
|
Filename: "bar.js",
|
|
Lineno: 113,
|
|
Colno: 231,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
logSentryEventScenario(t, "Should log received error event", errorEvent,
|
|
func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
|
|
assert.Equal(t, 200, sc.resp.Code)
|
|
assert.Len(t, logs, 1)
|
|
assertContextContains(t, logs[0], "logger", "frontend")
|
|
assertContextContains(t, logs[0], "url", errorEvent.Request.URL)
|
|
assertContextContains(t, logs[0], "user_agent", errorEvent.Request.Headers["User-Agent"])
|
|
assertContextContains(t, logs[0], "event_id", errorEvent.EventID)
|
|
assertContextContains(t, logs[0], "original_timestamp", errorEvent.Timestamp)
|
|
assertContextContains(t, logs[0], "stacktrace", `UserError: Please replace user and try again
|
|
at foofn (foo.js:123:23)
|
|
at barfn (bar.js:113:231)`)
|
|
assert.NotContains(t, logs[0].Ctx, "context")
|
|
})
|
|
|
|
messageEvent := frontendlogging.FrontendSentryEvent{
|
|
Event: &sentry.Event{
|
|
EventID: "123",
|
|
Level: sentry.LevelInfo,
|
|
Request: &request,
|
|
Timestamp: ts,
|
|
Message: "hello world",
|
|
User: user,
|
|
},
|
|
Exception: nil,
|
|
}
|
|
|
|
logSentryEventScenario(t, "Should log received message event", messageEvent,
|
|
func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
|
|
assert.Equal(t, 200, sc.resp.Code)
|
|
assert.Len(t, logs, 1)
|
|
assert.Equal(t, "hello world", logs[0].Msg)
|
|
assert.Equal(t, log.LvlInfo, logs[0].Lvl)
|
|
assertContextContains(t, logs[0], "logger", "frontend")
|
|
assertContextContains(t, logs[0], "url", messageEvent.Request.URL)
|
|
assertContextContains(t, logs[0], "user_agent", messageEvent.Request.Headers["User-Agent"])
|
|
assertContextContains(t, logs[0], "event_id", messageEvent.EventID)
|
|
assertContextContains(t, logs[0], "original_timestamp", messageEvent.Timestamp)
|
|
assert.NotContains(t, logs[0].Ctx, "stacktrace")
|
|
assert.NotContains(t, logs[0].Ctx, "context")
|
|
assertContextContains(t, logs[0], "user_email", user.Email)
|
|
assertContextContains(t, logs[0], "user_id", user.ID)
|
|
})
|
|
|
|
eventWithContext := frontendlogging.FrontendSentryEvent{
|
|
Event: &sentry.Event{
|
|
EventID: "123",
|
|
Level: sentry.LevelInfo,
|
|
Request: &request,
|
|
Timestamp: ts,
|
|
Message: "hello world",
|
|
User: user,
|
|
Contexts: map[string]interface{}{
|
|
"foo": map[string]interface{}{
|
|
"one": "two",
|
|
"three": 4,
|
|
},
|
|
"bar": "baz",
|
|
},
|
|
},
|
|
Exception: nil,
|
|
}
|
|
|
|
logSentryEventScenario(t, "Should log event context", eventWithContext,
|
|
func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
|
|
assert.Equal(t, 200, sc.resp.Code)
|
|
assert.Len(t, logs, 1)
|
|
assertContextContains(t, logs[0], "context_foo_one", "two")
|
|
assertContextContains(t, logs[0], "context_foo_three", "4")
|
|
assertContextContains(t, logs[0], "context_bar", "baz")
|
|
})
|
|
|
|
errorEventForSourceMapping := frontendlogging.FrontendSentryEvent{
|
|
Event: &event,
|
|
Exception: &frontendlogging.FrontendSentryException{
|
|
Values: []frontendlogging.FrontendSentryExceptionValue{
|
|
{
|
|
Type: "UserError",
|
|
Value: "Please replace user and try again",
|
|
Stacktrace: sentry.Stacktrace{
|
|
Frames: []sentry.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,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
logSentryEventScenario(t, "Should load sourcemap and transform stacktrace line when possible",
|
|
errorEventForSourceMapping, func(sc *scenarioContext, logs []*log.Record, sourceMapReads []SourceMapReadRecord) {
|
|
assert.Equal(t, 200, sc.resp.Code)
|
|
assert.Len(t, logs, 1)
|
|
assertContextContains(t, logs[0], "stacktrace", `UserError: Please replace user and try again
|
|
at ? (core|webpack:///./some_source.ts:2:2)
|
|
at ? (telepathic|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 ? (core|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)
|
|
})
|
|
})
|
|
}
|
|
|
|
func indexOf(arr []interface{}, item string) int {
|
|
for i, elem := range arr {
|
|
if elem == item {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func assertContextContains(t *testing.T, logRecord *log.Record, label string, value interface{}) {
|
|
assert.Contains(t, logRecord.Ctx, label)
|
|
labelIdx := indexOf(logRecord.Ctx, label)
|
|
assert.Equal(t, value, logRecord.Ctx[labelIdx+1])
|
|
}
|