Add route with access to server-log buffer
This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
146
pkg/logger/rrbuf.go
Normal file
146
pkg/logger/rrbuf.go
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user