Initial QSfera import
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package log
|
||||
|
||||
import "github.com/qsfera/server/pkg/shared"
|
||||
|
||||
// Configure initializes a service-specific logger instance.
|
||||
func Configure(name string, commons *shared.Commons, localServiceLogLevel string) Logger {
|
||||
return NewLogger(
|
||||
Name(name),
|
||||
Level(localServiceLogLevel),
|
||||
Pretty(commons.Log.Pretty),
|
||||
Color(commons.Log.Color),
|
||||
File(commons.Log.File),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
mzlog "github.com/go-micro/plugins/v4/logger/zerolog"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go-micro.dev/v4/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
RequestIDString = "request-id"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// this is ugly, but "logger.DefaultLogger" is a global variable, and we need to set it _before_ anybody uses it
|
||||
setMicroLogger()
|
||||
}
|
||||
|
||||
// for logging reasons we don't want the same logging level on both КуСфера and micro. As a framework builder we do not
|
||||
// want to expose to the end user the internal framework logs unless explicitly specified.
|
||||
func setMicroLogger() {
|
||||
if os.Getenv("MICRO_LOG_LEVEL") == "" {
|
||||
_ = os.Setenv("MICRO_LOG_LEVEL", "error")
|
||||
}
|
||||
|
||||
lev, err := zerolog.ParseLevel(os.Getenv("MICRO_LOG_LEVEL"))
|
||||
if err != nil {
|
||||
lev = zerolog.ErrorLevel
|
||||
}
|
||||
logger.DefaultLogger = mzlog.NewLogger(
|
||||
logger.WithLevel(logger.Level(lev)),
|
||||
logger.WithFields(map[string]any{
|
||||
"system": "go-micro",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Logger simply wraps the zerolog logger.
|
||||
type Logger struct {
|
||||
zerolog.Logger
|
||||
}
|
||||
|
||||
// NopLogger initializes a no-operation logger.
|
||||
func NopLogger() Logger {
|
||||
return Logger{zerolog.Nop()}
|
||||
}
|
||||
|
||||
type LineInfoHook struct{}
|
||||
|
||||
// Run is a hook to add line info to log messages.
|
||||
// I found the zerolog example for this here:
|
||||
// https://github.com/rs/zerolog/issues/22#issuecomment-1127295489
|
||||
func (h LineInfoHook) Run(e *zerolog.Event, _ zerolog.Level, _ string) {
|
||||
_, file, line, ok := runtime.Caller(3)
|
||||
if ok {
|
||||
e.Str("line", fmt.Sprintf("%s:%d", file, line))
|
||||
}
|
||||
}
|
||||
|
||||
// NewLogger initializes a new logger instance.
|
||||
func NewLogger(opts ...Option) Logger {
|
||||
options := newOptions(opts...)
|
||||
|
||||
// set GlobalLevel() to the minimum value -1 = TraceLevel, so that only the services' log level matter
|
||||
zerolog.SetGlobalLevel(zerolog.TraceLevel)
|
||||
|
||||
var logLevel zerolog.Level
|
||||
switch strings.ToLower(options.Level) {
|
||||
case "panic":
|
||||
logLevel = zerolog.PanicLevel
|
||||
case "fatal":
|
||||
logLevel = zerolog.FatalLevel
|
||||
case "error":
|
||||
logLevel = zerolog.ErrorLevel
|
||||
case "warn":
|
||||
logLevel = zerolog.WarnLevel
|
||||
case "info":
|
||||
logLevel = zerolog.InfoLevel
|
||||
case "debug":
|
||||
logLevel = zerolog.DebugLevel
|
||||
case "trace":
|
||||
logLevel = zerolog.TraceLevel
|
||||
default:
|
||||
logLevel = zerolog.ErrorLevel
|
||||
}
|
||||
|
||||
var l zerolog.Logger
|
||||
|
||||
if options.Pretty {
|
||||
l = log.Output(
|
||||
zerolog.NewConsoleWriter(
|
||||
func(w *zerolog.ConsoleWriter) {
|
||||
w.TimeFormat = time.RFC3339
|
||||
w.Out = os.Stderr
|
||||
w.NoColor = !options.Color
|
||||
},
|
||||
),
|
||||
)
|
||||
} else if options.File != "" {
|
||||
f, err := os.OpenFile(options.File, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
print(fmt.Sprintf("file could not be opened for writing: %s. error: %v", options.File, err))
|
||||
os.Exit(1)
|
||||
}
|
||||
l = l.Output(f)
|
||||
} else {
|
||||
l = zerolog.New(os.Stderr)
|
||||
}
|
||||
|
||||
l = l.With().
|
||||
Str("service", options.Name).
|
||||
Timestamp().
|
||||
Logger().Level(logLevel)
|
||||
|
||||
if logLevel <= zerolog.InfoLevel {
|
||||
var lineInfoHook LineInfoHook
|
||||
l = l.Hook(lineInfoHook)
|
||||
}
|
||||
|
||||
return Logger{
|
||||
l,
|
||||
}
|
||||
}
|
||||
|
||||
// SubloggerWithRequestID returns a sub-logger with the x-request-id added to all events
|
||||
func (l Logger) SubloggerWithRequestID(c context.Context) Logger {
|
||||
return Logger{
|
||||
l.With().Str(RequestIDString, chimiddleware.GetReqID(c)).Logger(),
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecation logs a deprecation message,
|
||||
// it is used to inform the user that a certain feature is deprecated and will be removed in the future.
|
||||
// Do not use a logger here because the message MUST be visible independent of the log level.
|
||||
func Deprecation(a ...any) {
|
||||
fmt.Printf("\033[1;31mDEPRECATION: %s\033[0m\n", a...)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package log_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
"github.com/qsfera/server/internal/testenv"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
)
|
||||
|
||||
func TestDeprecation(t *testing.T) {
|
||||
cmdTest := testenv.NewCMDTest(t.Name())
|
||||
if cmdTest.ShouldRun() {
|
||||
log.Deprecation("this is a deprecation")
|
||||
return
|
||||
}
|
||||
|
||||
out, err := cmdTest.Run()
|
||||
|
||||
g := gomega.NewWithT(t)
|
||||
g.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
g.Expect(string(out)).To(gomega.HavePrefix("\033[1;31mDEPRECATION: this is a deprecation"))
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type levelMap map[logrus.Level]zerolog.Level
|
||||
|
||||
var levelMapping = levelMap{
|
||||
logrus.PanicLevel: zerolog.PanicLevel,
|
||||
logrus.ErrorLevel: zerolog.ErrorLevel,
|
||||
logrus.TraceLevel: zerolog.TraceLevel,
|
||||
logrus.DebugLevel: zerolog.DebugLevel,
|
||||
logrus.WarnLevel: zerolog.WarnLevel,
|
||||
logrus.InfoLevel: zerolog.InfoLevel,
|
||||
}
|
||||
|
||||
// LogrusWrapper around zerolog. Required because idp uses logrus internally.
|
||||
type LogrusWrapper struct {
|
||||
zeroLog *zerolog.Logger
|
||||
levelMap levelMap
|
||||
}
|
||||
|
||||
// LogrusWrap returns a logrus logger which internally logs to /dev/null. Messages are passed to the
|
||||
// underlying zerolog via hooks.
|
||||
func LogrusWrap(zr zerolog.Logger) *logrus.Logger {
|
||||
lr := logrus.New()
|
||||
lr.SetOutput(io.Discard)
|
||||
lr.SetLevel(logrusLevel(zr.GetLevel()))
|
||||
lr.AddHook(&LogrusWrapper{
|
||||
zeroLog: &zr,
|
||||
levelMap: levelMapping,
|
||||
})
|
||||
|
||||
return lr
|
||||
}
|
||||
|
||||
// Levels on which logrus hooks should be triggered
|
||||
func (h *LogrusWrapper) Levels() []logrus.Level {
|
||||
return logrus.AllLevels
|
||||
}
|
||||
|
||||
// Fire called by logrus on new message
|
||||
func (h *LogrusWrapper) Fire(entry *logrus.Entry) error {
|
||||
h.zeroLog.WithLevel(h.levelMap[entry.Level]).
|
||||
Fields(zeroLogFields(entry.Data)).
|
||||
Msg(entry.Message)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert logrus fields to zerolog
|
||||
func zeroLogFields(fields logrus.Fields) map[string]any {
|
||||
fm := make(map[string]any)
|
||||
for k, v := range fields {
|
||||
fm[k] = v
|
||||
}
|
||||
|
||||
return fm
|
||||
}
|
||||
|
||||
// Convert logrus level to zerolog
|
||||
func logrusLevel(level zerolog.Level) logrus.Level {
|
||||
for lrLvl, zrLvl := range levelMapping {
|
||||
if zrLvl == level {
|
||||
return lrLvl
|
||||
}
|
||||
}
|
||||
|
||||
panic("Unexpected loglevel")
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package log
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Name string
|
||||
Level string
|
||||
Pretty bool
|
||||
Color bool
|
||||
File string
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{
|
||||
Name: "qsfera",
|
||||
Level: "info",
|
||||
Pretty: true,
|
||||
Color: true,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Name provides a function to set the name option.
|
||||
func Name(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Name = val
|
||||
}
|
||||
}
|
||||
|
||||
// Level provides a function to set the level option.
|
||||
func Level(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Level = val
|
||||
}
|
||||
}
|
||||
|
||||
// Pretty provides a function to set the pretty option.
|
||||
func Pretty(val bool) Option {
|
||||
return func(o *Options) {
|
||||
o.Pretty = val
|
||||
}
|
||||
}
|
||||
|
||||
// Color provides a function to set the color option.
|
||||
func Color(val bool) Option {
|
||||
return func(o *Options) {
|
||||
o.Color = val
|
||||
}
|
||||
}
|
||||
|
||||
// File provides a function to set the file option.
|
||||
func File(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.File = val
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user