From e0bcaf86620a9e82d267fe34ede3256da9d8385b Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Sun, 13 Feb 2022 18:23:27 +0100 Subject: [PATCH] Add route with access to server-log buffer --- pkg/api/server/debug.go | 7 +- pkg/api/server/handlers.go | 40 ++++++++++ pkg/logger/logger.go | 21 +++++- pkg/logger/rrbuf.go | 146 +++++++++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 pkg/logger/rrbuf.go diff --git a/pkg/api/server/debug.go b/pkg/api/server/debug.go index 81a407efc..37ca5bf53 100644 --- a/pkg/api/server/debug.go +++ b/pkg/api/server/debug.go @@ -2,13 +2,14 @@ package server import ( "fmt" - "github.com/cortezaproject/corteza-server/pkg/corredor" - "github.com/cortezaproject/corteza-server/pkg/eventbus" - "github.com/davecgh/go-spew/spew" "net/http" "reflect" "runtime" + "github.com/cortezaproject/corteza-server/pkg/corredor" + "github.com/cortezaproject/corteza-server/pkg/eventbus" + "github.com/davecgh/go-spew/spew" + "github.com/go-chi/chi/v5" ) diff --git a/pkg/api/server/handlers.go b/pkg/api/server/handlers.go index 99db4acff..1ad392822 100644 --- a/pkg/api/server/handlers.go +++ b/pkg/api/server/handlers.go @@ -4,12 +4,14 @@ import ( "fmt" "net/http" "path" + "strconv" "strings" "github.com/cortezaproject/corteza-server/pkg/api" "github.com/cortezaproject/corteza-server/pkg/auth" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/healthcheck" + "github.com/cortezaproject/corteza-server/pkg/logger" "github.com/cortezaproject/corteza-server/pkg/options" "github.com/cortezaproject/corteza-server/pkg/version" "github.com/go-chi/chi/v5" @@ -68,6 +70,7 @@ func shutdownRoutes() (r chi.Router) { // routes used when in active mode func activeRoutes(log *zap.Logger, mountable []func(r chi.Router), envOpt options.EnvironmentOpt, httpOpt options.HttpServerOpt) (r chi.Router) { r = chi.NewRouter() + r.Use(handleCORS) r.Route("/"+strings.TrimPrefix(httpOpt.BaseUrl, "/"), func(r chi.Router) { // Reports error to Sentry if enabled @@ -130,6 +133,8 @@ func mountServiceHandlers(r chi.Router, log *zap.Logger, opt options.HttpServerO if opt.EnableHealthcheckRoute { mountHealthCheckHandler(r, log, opt.BaseUrl) } + + mountDebugLogViewer(r, log) } func mountDebugHandler(r chi.Router, log *zap.Logger) { @@ -184,3 +189,38 @@ func mountHealthCheckHandler(r chi.Router, log *zap.Logger, basePath string) { } } + +func mountDebugLogViewer(r chi.Router, log *zap.Logger) { + var ( + path = "/view-log" + ) + + r.Get(path+".json", func(w http.ResponseWriter, r *http.Request) { + var ( + after int = 0 + limit int = 100 + err error + q = r.URL.Query() + ) + + if aux := q.Get("after"); len(aux) > 0 { + after, err = strconv.Atoi(aux) + if err != nil { + errors.ProperlyServeHTTP(w, r, errors.InvalidData("invalid value format for after: %w", err), false) + return + } + } + + if aux := q.Get("limit"); len(aux) > 0 { + limit, err = strconv.Atoi(aux) + if err != nil { + errors.ProperlyServeHTTP(w, r, errors.InvalidData("invalid value format for limit: %w", err), false) + return + } + } + + _, _ = logger.WriteLogBuffer(w, after, limit) + }) + + log.Debug("log viewer route enabled: " + path) +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 8c3b1d983..9392cb0bc 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -37,16 +37,18 @@ func Init() { } var ( - err error - conf = applyOptions(zap.NewProductionConfig(), opt) + conf = applyOptions(zap.NewProductionConfig(), opt) + logger, err = conf.Build() ) - defaultLogger, err = conf.Build() if err != nil { panic(err) } - defaultLogger = applySpecials(defaultLogger, opt) + logger = applySpecials(defaultLogger, opt) + logger = withDebugBuffer(logger) + + defaultLogger = logger } func MakeDebugLogger() *zap.Logger { @@ -71,6 +73,8 @@ func MakeDebugLogger() *zap.Logger { panic(err) } + logger = withDebugBuffer(logger) + return applySpecials(logger, &dbgOpt) } @@ -98,6 +102,15 @@ func applySpecials(l *zap.Logger, opt *options.LogOpt) *zap.Logger { return l.WithOptions(zap.AddStacktrace(mustParseLevel(opt.StacktraceLevel))) } +// adds Tee logger that copies all log messages to debug buffer +func withDebugBuffer(in *zap.Logger) *zap.Logger { + return zap.New(zapcore.NewTee( + in.Core(), + + DebugBufferedLogger(debugLogRR), + )) +} + func mustParseLevel(l string) (o zapcore.Level) { if err := o.Set(l); err != nil { panic(err) diff --git a/pkg/logger/rrbuf.go b/pkg/logger/rrbuf.go new file mode 100644 index 000000000..2eac2973a --- /dev/null +++ b/pkg/logger/rrbuf.go @@ -0,0 +1,146 @@ +package logger + +import ( + "fmt" + "io" + "sync" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type ( + rrBufEntry struct { + num int + rec []byte + } + + // simple round-robin struct that holds our buffer with log entries + rr struct { + mux sync.RWMutex + num int + buf []*rrBufEntry + } + + debugBufferingLogger struct { + zapcore.LevelEnabler + out *rr + enc zapcore.Encoder + } +) + +const ( + debugLogCap = 10240 +) + +var ( + // initialize debug logger round robin db + debugLogRR = &rr{ + buf: make([]*rrBufEntry, 0), + } +) + +func WriteLogBuffer(w io.Writer, after, limit int) (_ int, err error) { + var ( + // was at least one entry outputted? + has bool + ) + + debugLogRR.mux.RLock() + defer debugLogRR.mux.RUnlock() + if _, err = w.Write([]byte{'['}); err != nil { + return + } + + for _, e := range debugLogRR.buf { + if after >= e.num { + continue + } + + // count back to zero + limit-- + + if has { + if _, err = w.Write([]byte{','}); err != nil { + return + } + } + + if _, err = w.Write(e.rec); err != nil { + return + } + + if limit == 0 { + break + } + + has = true + } + + if _, err = w.Write([]byte{']'}); err != nil { + return + } + + return +} + +func (r *rr) append(ent []byte) { + r.mux.Lock() + defer r.mux.Unlock() + + r.num++ + + // modify serialized json + var bufEnt = &rrBufEntry{r.num, append(ent[:len(ent)-2], []byte(fmt.Sprintf(`,"index":%d}`, r.num))...)} + + if len(r.buf) >= debugLogCap { + r.buf = append(r.buf[1:], bufEnt) + } else { + r.buf = append(r.buf, bufEnt) + } +} + +func DebugBufferedLogger(out *rr) *debugBufferingLogger { + var encConf = zap.NewProductionEncoderConfig() + encConf.EncodeTime = zapcore.RFC3339NanoTimeEncoder + + return &debugBufferingLogger{ + LevelEnabler: zapcore.DebugLevel, + out: out, + enc: zapcore.NewJSONEncoder(encConf), + } +} + +func (c *debugBufferingLogger) With(fields []zapcore.Field) zapcore.Core { + clone := c.clone() + for i := range fields { + fields[i].AddTo(clone.enc) + } + return clone +} + +func (c *debugBufferingLogger) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if c.Enabled(ent.Level) { + return ce.AddCore(ent, c) + } + return ce +} + +func (c *debugBufferingLogger) Write(ent zapcore.Entry, fields []zapcore.Field) error { + encbuf, err := c.enc.EncodeEntry(ent, fields) + if err != nil { + return err + } + + c.out.append(encbuf.Bytes()) + return nil +} + +func (c *debugBufferingLogger) Sync() error { return nil } +func (c *debugBufferingLogger) clone() *debugBufferingLogger { + return &debugBufferingLogger{ + LevelEnabler: c.LevelEnabler, + enc: c.enc.Clone(), + out: c.out, + } +}