feat: docs api design
This commit is contained in:
48
pkg/logger/kylin.go
Normal file
48
pkg/logger/kylin.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"octopus/internal/config"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var (
|
||||
CtxDebug = struct{}{}
|
||||
CtxTimezone = struct{}{}
|
||||
)
|
||||
|
||||
func NewKylinMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
ctxLogger := log.With().Interface(config.LogTagTraceID, c.Locals("trace_id"))
|
||||
|
||||
if authorization := c.Get(fiber.HeaderAuthorization); authorization != "" {
|
||||
ctxLogger.Str(config.LogTagAuthorization, authorization)
|
||||
}
|
||||
if bid := c.Get("Rcrai-Bid"); bid != "" {
|
||||
ctxLogger.Str(config.LogTagBID, bid)
|
||||
}
|
||||
if staffID := c.Get("Rcrai-StaffId"); staffID != "" {
|
||||
ctxLogger.Str(config.LogTagStaffID, staffID)
|
||||
}
|
||||
ctx := c.UserContext()
|
||||
logger := ctxLogger.Logger()
|
||||
logger = logger.Level(zerolog.InfoLevel)
|
||||
if lv := c.Get("Debug_Kylin"); strings.ToLower(lv) == "true" {
|
||||
ctx = context.WithValue(ctx, CtxDebug, struct{}{})
|
||||
logger = logger.Level(zerolog.DebugLevel)
|
||||
}
|
||||
if tz := c.Get("R-Timezone"); tz != "" {
|
||||
if loc, err := time.LoadLocation(tz); err == nil {
|
||||
ctx = context.WithValue(ctx, CtxTimezone, loc)
|
||||
}
|
||||
}
|
||||
c.SetUserContext(logger.WithContext(ctx))
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
269
pkg/logger/sentry_writer.go
Normal file
269
pkg/logger/sentry_writer.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"octopus/internal/config"
|
||||
|
||||
// "github.com/buger/jsonparser"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
var levelsMapping = map[zerolog.Level]sentry.Level{
|
||||
zerolog.DebugLevel: sentry.LevelDebug,
|
||||
zerolog.InfoLevel: sentry.LevelInfo,
|
||||
zerolog.WarnLevel: sentry.LevelWarning,
|
||||
zerolog.ErrorLevel: sentry.LevelError,
|
||||
zerolog.FatalLevel: sentry.LevelFatal,
|
||||
zerolog.PanicLevel: sentry.LevelFatal,
|
||||
}
|
||||
|
||||
var _ = io.WriteCloser(new(SentryWriter))
|
||||
|
||||
var now = time.Now
|
||||
|
||||
// SentryWriter is a sentry events writer with std io.SentryWriter iface.
|
||||
type SentryWriter struct {
|
||||
client *sentry.Client
|
||||
|
||||
levels map[zerolog.Level]struct{}
|
||||
flushTimeout time.Duration
|
||||
}
|
||||
|
||||
// Write handles zerolog's json and sends events to sentry.
|
||||
func (w *SentryWriter) Write(data []byte) (int, error) {
|
||||
event, ok := w.parseLogEvent(data)
|
||||
if ok {
|
||||
w.client.CaptureEvent(event, nil, nil)
|
||||
// should flush before os.Exit
|
||||
if event.Level == sentry.LevelFatal {
|
||||
w.client.Flush(w.flushTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
// Close forces client to flush all pending events.
|
||||
// Can be useful before application exits.
|
||||
func (w *SentryWriter) Close() error {
|
||||
w.client.Flush(w.flushTimeout)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *SentryWriter) parseLogEvent(data []byte) (*sentry.Event, bool) {
|
||||
const logger = "zerolog"
|
||||
lvlStr := gjson.GetBytes(data, zerolog.LevelFieldName)
|
||||
lvl, err := zerolog.ParseLevel(lvlStr.String())
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
_, enabled := w.levels[lvl]
|
||||
if !enabled {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
sentryLvl, ok := levelsMapping[lvl]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
event := sentry.Event{
|
||||
Timestamp: now(),
|
||||
Level: sentryLvl,
|
||||
Logger: logger,
|
||||
Tags: make(map[string]string, 6),
|
||||
Request: &sentry.Request{},
|
||||
}
|
||||
|
||||
gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool {
|
||||
switch key.String() {
|
||||
// case zerolog.LevelFieldName, zerolog.TimestampFieldName:
|
||||
case zerolog.MessageFieldName:
|
||||
event.Message = value.String()
|
||||
case zerolog.ErrorFieldName:
|
||||
event.Exception = append(event.Exception, sentry.Exception{
|
||||
Value: value.String(),
|
||||
Stacktrace: newStacktrace(),
|
||||
})
|
||||
case config.LogTagURL:
|
||||
event.Request.URL = value.String()
|
||||
case config.LogTagMethod:
|
||||
event.Request.Method = value.String()
|
||||
case config.LogTagHeaders:
|
||||
headers := make(map[string]string)
|
||||
value.ForEach(func(key, value gjson.Result) bool {
|
||||
headers[key.String()] = value.String()
|
||||
return true
|
||||
})
|
||||
event.Request.Headers = headers
|
||||
case config.LogTagData:
|
||||
event.Request.Data = value.String()
|
||||
case config.LogTagAuthorization:
|
||||
event.Tags["Authorization"] = value.String()
|
||||
case config.LogTagBID:
|
||||
event.Tags["bid"] = value.String()
|
||||
case config.LogTagStaffID:
|
||||
event.Tags["staff_id"] = value.String()
|
||||
case config.LogTagTraceID:
|
||||
event.Tags["trace_id"] = value.String()
|
||||
default:
|
||||
event.Tags[key.String()] = value.String()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return &event, true
|
||||
}
|
||||
|
||||
func newStacktrace() *sentry.Stacktrace {
|
||||
const (
|
||||
module = "github.com/archdx/zerolog-sentry"
|
||||
loggerModule = "github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
st := sentry.NewStacktrace()
|
||||
|
||||
threshold := len(st.Frames) - 1
|
||||
// drop current module frames
|
||||
for ; threshold > 0 && st.Frames[threshold].Module == module; threshold-- {
|
||||
}
|
||||
|
||||
outer:
|
||||
// try to drop zerolog module frames after logger call point
|
||||
for i := threshold; i > 0; i-- {
|
||||
if st.Frames[i].Module == loggerModule {
|
||||
for j := i - 1; j >= 0; j-- {
|
||||
if st.Frames[j].Module != loggerModule {
|
||||
threshold = j
|
||||
break outer
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
st.Frames = st.Frames[:threshold+1]
|
||||
|
||||
return st
|
||||
}
|
||||
|
||||
// WriterOption configures sentry events writer.
|
||||
type WriterOption interface {
|
||||
apply(*options)
|
||||
}
|
||||
|
||||
type optionFunc func(*options)
|
||||
|
||||
func (fn optionFunc) apply(c *options) { fn(c) }
|
||||
|
||||
type options struct {
|
||||
release string
|
||||
environment string
|
||||
serverName string
|
||||
levels []zerolog.Level
|
||||
ignoreErrors []string
|
||||
sampleRate float64
|
||||
flushTimeout time.Duration
|
||||
debug bool
|
||||
}
|
||||
|
||||
// WithLevels configures zerolog levels that have to be sent to Sentry.
|
||||
// Default levels are: error, fatal, panic.
|
||||
func WithLevels(levels ...zerolog.Level) WriterOption {
|
||||
return optionFunc(func(cfg *options) {
|
||||
cfg.levels = levels
|
||||
})
|
||||
}
|
||||
|
||||
// WithSampleRate configures the sample rate as a percentage of events to be sent in the range of 0.0 to 1.0.
|
||||
func WithSampleRate(rate float64) WriterOption {
|
||||
return optionFunc(func(cfg *options) {
|
||||
cfg.sampleRate = rate
|
||||
})
|
||||
}
|
||||
|
||||
// WithRelease configures the release to be sent with events.
|
||||
func WithRelease(release string) WriterOption {
|
||||
return optionFunc(func(cfg *options) {
|
||||
cfg.release = release
|
||||
})
|
||||
}
|
||||
|
||||
// WithEnvironment configures the environment to be sent with events.
|
||||
func WithEnvironment(environment string) WriterOption {
|
||||
return optionFunc(func(cfg *options) {
|
||||
cfg.environment = environment
|
||||
})
|
||||
}
|
||||
|
||||
// WithServerName configures the server name field for events. Default value is OS hostname.
|
||||
func WithServerName(serverName string) WriterOption {
|
||||
return optionFunc(func(cfg *options) {
|
||||
cfg.serverName = serverName
|
||||
})
|
||||
}
|
||||
|
||||
// WithIgnoreErrors configures the list of regexp strings that will be used to match against event's message
|
||||
// and if applicable, caught errors type and value. If the match is found, then a whole event will be dropped.
|
||||
func WithIgnoreErrors(reList []string) WriterOption {
|
||||
return optionFunc(func(cfg *options) {
|
||||
cfg.ignoreErrors = reList
|
||||
})
|
||||
}
|
||||
|
||||
// WithDebug enables sentry client debug logs.
|
||||
func WithDebug(debug bool) WriterOption {
|
||||
return optionFunc(func(cfg *options) {
|
||||
cfg.debug = debug
|
||||
})
|
||||
}
|
||||
|
||||
// NewSentryWriter creates writer with provided DSN and options.
|
||||
func NewSentryWriter(dsn string, opts ...WriterOption) (*SentryWriter, error) {
|
||||
cfg := newDefaultConfig()
|
||||
for _, opt := range opts {
|
||||
opt.apply(&cfg)
|
||||
}
|
||||
|
||||
client, err := sentry.NewClient(sentry.ClientOptions{
|
||||
Dsn: dsn,
|
||||
SampleRate: cfg.sampleRate,
|
||||
Release: cfg.release,
|
||||
Environment: cfg.environment,
|
||||
ServerName: cfg.serverName,
|
||||
IgnoreErrors: cfg.ignoreErrors,
|
||||
Debug: cfg.debug,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
levels := make(map[zerolog.Level]struct{}, len(cfg.levels))
|
||||
for _, lvl := range cfg.levels {
|
||||
levels[lvl] = struct{}{}
|
||||
}
|
||||
|
||||
return &SentryWriter{
|
||||
client: client,
|
||||
levels: levels,
|
||||
flushTimeout: cfg.flushTimeout,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newDefaultConfig() options {
|
||||
return options{
|
||||
levels: []zerolog.Level{
|
||||
zerolog.ErrorLevel,
|
||||
zerolog.FatalLevel,
|
||||
zerolog.PanicLevel,
|
||||
},
|
||||
sampleRate: 1.0,
|
||||
flushTimeout: 3 * time.Second,
|
||||
}
|
||||
}
|
||||
38
pkg/logger/setup.go
Normal file
38
pkg/logger/setup.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"octopus"
|
||||
"octopus/internal/config"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
func Setup() {
|
||||
cfg := config.Get()
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
writers := []io.Writer{}
|
||||
if config.Get().IsLocal {
|
||||
writers = append(writers, zerolog.NewConsoleWriter())
|
||||
} else {
|
||||
writers = append(writers, os.Stderr)
|
||||
}
|
||||
if dsn := cfg.Sentry.DSN; dsn != "" {
|
||||
sentryWriter, err := NewSentryWriter(
|
||||
cfg.Sentry.DSN,
|
||||
WithDebug(cfg.Debug),
|
||||
WithEnvironment(cfg.Sentry.Environment),
|
||||
WithRelease(octopus.Version),
|
||||
WithServerName("octopus-service"),
|
||||
)
|
||||
if err == nil {
|
||||
writers = append(writers, sentryWriter)
|
||||
}
|
||||
}
|
||||
log.Logger = zerolog.New(io.MultiWriter(writers...)).With().Stack().Timestamp().Logger()
|
||||
}
|
||||
Reference in New Issue
Block a user