feat: Use slog instead of zap

This commit is contained in:
Paramtamtam 2024-06-24 19:28:03 +04:00
parent a52dbde00c
commit ceeb7f9384
No known key found for this signature in database
GPG Key ID: 366371698FAD0A2B
18 changed files with 525 additions and 166 deletions

View File

@ -37,15 +37,9 @@ linters-settings:
ignore-words: [cancelled]
lll:
line-length: 120
forbidigo:
forbid:
- '^(fmt\.Print(|f|ln)|print(|ln))(# it looks like a forgotten debugging printing call)?$'
depguard:
rules:
logger:
deny:
- pkg: log
desc: 'logging is allowed only by zaplog'
# forbidigo:
# forbid:
# - '^(fmt\.Print(|f|ln)|print(|ln))(# it looks like a forgotten debugging printing call)?$'
prealloc:
simple: true
range-loops: true
@ -108,7 +102,6 @@ linters: # All available linters list: <https://golangci-lint.run/usage/linters/
- durationcheck # Check for two durations multiplied together
- errchkjson # Checks types passed to the json encoding functions
- errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
- depguard # Go linter that checks if package imports are in a list of acceptable packages.
issues:
exclude-dirs:

View File

@ -60,8 +60,6 @@ func NewApp(appName string) *cli.Command { //nolint:funlen
Usage: appName,
Suggest: true,
Before: func(ctx context.Context, c *cli.Command) error {
_ = log.Sync() // sync previous logger instance
var (
logLevel, _ = logger.ParseLevel(c.String(logLevelFlag.Name)) // error ignored because the flag validates itself
logFormat, _ = logger.ParseFormat(c.String(logFormatFlag.Name)) // --//--

View File

@ -5,9 +5,9 @@ import (
"fmt"
"github.com/urfave/cli/v3"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/logger"
)
type checker interface {
@ -15,7 +15,7 @@ type checker interface {
}
// NewCommand creates `healthcheck` command.
func NewCommand(_ *zap.Logger, checker checker) *cli.Command {
func NewCommand(_ *logger.Logger, checker checker) *cli.Command {
var portFlag = shared.ListenPortFlag
return &cli.Command{

View File

@ -6,15 +6,15 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
"gh.tarampamp.am/error-pages/internal/logger"
)
func TestNewCommand(t *testing.T) {
t.Parallel()
var cmd = healthcheck.NewCommand(zap.NewNop(), nil)
var cmd = healthcheck.NewCommand(logger.NewNop(), nil)
assert.Equal(t, "healthcheck", cmd.Name)
assert.Equal(t, []string{"chk", "health", "check"}, cmd.Aliases)
@ -35,7 +35,7 @@ func (m *fakeHealthChecker) Check(_ context.Context, addr string) error {
func TestCommand_RunSuccess(t *testing.T) {
t.Parallel()
var cmd = healthcheck.NewCommand(zap.NewNop(), &fakeHealthChecker{
var cmd = healthcheck.NewCommand(logger.NewNop(), &fakeHealthChecker{
t: t,
wantAddress: "http://127.0.0.1:1234",
})
@ -46,7 +46,7 @@ func TestCommand_RunSuccess(t *testing.T) {
func TestCommand_RunFail(t *testing.T) {
t.Parallel()
cmd := healthcheck.NewCommand(zap.NewNop(), &fakeHealthChecker{
cmd := healthcheck.NewCommand(logger.NewNop(), &fakeHealthChecker{
t: t,
wantAddress: "http://127.0.0.1:4321",
giveErr: assert.AnError,

View File

@ -9,11 +9,11 @@ import (
"time"
"github.com/urfave/cli/v3"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/config"
appHttp "gh.tarampamp.am/error-pages/internal/http"
"gh.tarampamp.am/error-pages/internal/logger"
)
type command struct {
@ -29,7 +29,7 @@ type command struct {
}
// NewCommand creates `serve` command.
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
var (
cmd command
cfg = config.New()
@ -182,8 +182,8 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
return fmt.Errorf("cannot add template from file %s: %w", templatePath, err)
} else {
log.Info("Template added",
zap.String("name", addedName),
zap.String("path", templatePath),
logger.String("name", addedName),
logger.String("path", templatePath),
)
}
}
@ -207,9 +207,9 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
cfg.Codes[code] = desc
log.Info("HTTP code added",
zap.String("code", code),
zap.String("message", desc.Message),
zap.String("description", desc.Description),
logger.String("code", code),
logger.String("message", desc.Message),
logger.String("description", desc.Description),
)
}
}
@ -225,16 +225,16 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
}
log.Debug("Configuration",
zap.Strings("loaded templates", cfg.Templates.Names()),
zap.Strings("described HTTP codes", cfg.Codes.Codes()),
zap.String("JSON format", cfg.Formats.JSON),
zap.String("XML format", cfg.Formats.XML),
zap.String("template name", cfg.TemplateName),
zap.Bool("disable localization", cfg.L10n.Disable),
zap.Uint16("default code to render", cfg.DefaultCodeToRender),
zap.Bool("respond with the same HTTP code", cfg.RespondWithSameHTTPCode),
zap.Bool("show details", cfg.ShowDetails),
zap.Strings("proxy HTTP headers", cfg.ProxyHeaders),
logger.Strings("loaded templates", cfg.Templates.Names()...),
logger.Strings("described HTTP codes", cfg.Codes.Codes()...),
logger.String("JSON format", cfg.Formats.JSON),
logger.String("XML format", cfg.Formats.XML),
logger.String("template name", cfg.TemplateName),
logger.Bool("disable localization", cfg.L10n.Disable),
logger.Uint16("default code to render", cfg.DefaultCodeToRender),
logger.Bool("respond with the same HTTP code", cfg.RespondWithSameHTTPCode),
logger.Bool("show details", cfg.ShowDetails),
logger.Strings("proxy HTTP headers", cfg.ProxyHeaders...),
)
return cmd.Run(ctx, log, &cfg)
@ -261,7 +261,7 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
}
// Run current command.
func (cmd *command) Run(ctx context.Context, log *zap.Logger, cfg *config.Config) error {
func (cmd *command) Run(ctx context.Context, log *logger.Logger, cfg *config.Config) error {
var srv = appHttp.NewServer(ctx, log)
if err := srv.Register(cfg); err != nil {
@ -276,12 +276,12 @@ func (cmd *command) Run(ctx context.Context, log *zap.Logger, cfg *config.Config
var now = time.Now()
defer func() {
log.Info("HTTP server stopped", zap.Duration("uptime", time.Since(now).Round(time.Millisecond)))
log.Info("HTTP server stopped", logger.Duration("uptime", time.Since(now).Round(time.Millisecond)))
}()
log.Info("HTTP server starting",
zap.String("addr", cmd.opt.http.addr),
zap.Uint16("port", cmd.opt.http.port),
logger.String("addr", cmd.opt.http.addr),
logger.Uint16("port", cmd.opt.http.port),
)
if err := srv.Start(cmd.opt.http.addr, cmd.opt.http.port); err != nil && !errors.Is(err, http.ErrServerClosed) {
@ -297,7 +297,7 @@ func (cmd *command) Run(ctx context.Context, log *zap.Logger, cfg *config.Config
case <-ctx.Done(): // ..or context cancellation
const shutdownTimeout = 5 * time.Second
log.Info("HTTP server stopping", zap.Duration("with timeout", shutdownTimeout))
log.Info("HTTP server stopping", logger.Duration("with timeout", shutdownTimeout))
if err := srv.Stop(shutdownTimeout); err != nil { //nolint:contextcheck
return err

View File

@ -4,13 +4,13 @@ import (
"net/http"
"time"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/logger"
)
// New creates a middleware for [http.ServeMux] that logs every incoming request.
//
// The skipper function should return true if the request should be skipped. It's ok to pass nil.
func New(log *zap.Logger, skipper func(*http.Request) bool) func(http.Handler) http.Handler {
func New(log *logger.Logger, skipper func(*http.Request) bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if skipper != nil && skipper(r) {
@ -22,21 +22,21 @@ func New(log *zap.Logger, skipper func(*http.Request) bool) func(http.Handler) h
var now = time.Now()
defer func() {
var fields = []zap.Field{
zap.String("useragent", r.UserAgent()),
zap.String("method", r.Method),
zap.String("url", r.URL.String()),
zap.String("referer", r.Referer()),
zap.String("content type", w.Header().Get("Content-Type")),
zap.String("remote addr", r.RemoteAddr),
zap.String("method", r.Method),
zap.Duration("duration", time.Since(now).Round(time.Microsecond)),
var fields = []logger.Attr{
logger.String("useragent", r.UserAgent()),
logger.String("method", r.Method),
logger.String("url", r.URL.String()),
logger.String("referer", r.Referer()),
logger.String("content type", w.Header().Get("Content-Type")),
logger.String("remote addr", r.RemoteAddr),
logger.String("method", r.Method),
logger.Duration("duration", time.Since(now).Round(time.Microsecond)),
}
if log.Level() <= zap.DebugLevel {
if log.Level() <= logger.DebugLevel {
fields = append(fields,
zap.Any("request headers", r.Header.Clone()),
zap.Any("response headers", w.Header().Clone()),
logger.Any("request headers", r.Header.Clone()),
logger.Any("response headers", w.Header().Clone()),
)
}

View File

@ -8,24 +8,23 @@ import (
"strings"
"time"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/appmeta"
"gh.tarampamp.am/error-pages/internal/config"
ep "gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
"gh.tarampamp.am/error-pages/internal/http/handlers/live"
"gh.tarampamp.am/error-pages/internal/http/handlers/version"
"gh.tarampamp.am/error-pages/internal/http/middleware/logreq"
"gh.tarampamp.am/error-pages/internal/logger"
)
// Server is an HTTP server for serving error pages.
type Server struct {
log *zap.Logger
log *logger.Logger
server *http.Server
}
// NewServer creates a new HTTP server.
func NewServer(baseCtx context.Context, log *zap.Logger) Server {
func NewServer(baseCtx context.Context, log *logger.Logger) Server {
const (
readTimeout = 30 * time.Second
writeTimeout = readTimeout + 10*time.Second // should be bigger than the read timeout
@ -39,7 +38,7 @@ func NewServer(baseCtx context.Context, log *zap.Logger) Server {
WriteTimeout: writeTimeout,
ReadHeaderTimeout: readTimeout,
MaxHeaderBytes: maxHeaderBytes,
ErrorLog: zap.NewStdLog(log),
ErrorLog: logger.NewStdLog(log),
BaseContext: func(net.Listener) context.Context { return baseCtx },
},
}

View File

@ -13,15 +13,15 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/config"
appHttp "gh.tarampamp.am/error-pages/internal/http"
"gh.tarampamp.am/error-pages/internal/logger"
)
func TestRouting(t *testing.T) {
var (
srv = appHttp.NewServer(context.Background(), zap.NewNop())
srv = appHttp.NewServer(context.Background(), logger.NewNop())
cfg = config.New()
)

42
internal/logger/attr.go Normal file
View File

@ -0,0 +1,42 @@
package logger
import (
"log/slog"
"time"
)
// An Attr is a key-value pair.
type Attr = slog.Attr
// String returns an Attr for a string value.
func String(key, value string) Attr { return slog.String(key, value) }
// Strings returns an Attr for a slice of strings.
func Strings(key string, value ...string) Attr { return slog.Any(key, value) }
// Int64 returns an Attr for an int64.
func Int64(key string, value int64) Attr { return slog.Int64(key, value) }
// Int converts an int to an int64 and returns an Attr with that value.
func Int(key string, value int) Attr { return slog.Int(key, value) }
// Uint64 returns an Attr for an uint64.
func Uint64(key string, v uint64) Attr { return slog.Uint64(key, v) }
// Uint16 returns an Attr for an uint16.
func Uint16(key string, v uint16) Attr { return slog.Uint64(key, uint64(v)) }
// Float64 returns an Attr for a floating-point number.
func Float64(key string, v float64) Attr { return slog.Float64(key, v) }
// Bool returns an Attr for a bool.
func Bool(key string, v bool) Attr { return slog.Bool(key, v) }
// Time returns an Attr for a [time.Time]. It discards the monotonic portion.
func Time(key string, v time.Time) Attr { return slog.Time(key, v) }
// Duration returns an Attr for a [time.Duration].
func Duration(key string, v time.Duration) Attr { return slog.Duration(key, v) }
// Any returns an Attr for any value.
func Any(key string, v any) Attr { return slog.Any(key, v) }

View File

@ -0,0 +1,44 @@
package logger_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/logger"
)
func TestAttrs(t *testing.T) {
t.Parallel()
var someTime, _ = time.Parse(time.RFC3339, "2021-01-01T00:00:00Z")
for name, _tt := range map[string]struct {
giveAttr logger.Attr
wantKey string
wantValue any
}{
"String": {logger.String("key", "value"), "key", "value"},
"Strings": {logger.Strings("key", "value1", "value2"), "key", []string{"value1", "value2"}},
"Int64": {logger.Int64("key", 42), "key", int64(42)},
"Int": {logger.Int("key", 42), "key", int64(42)},
"Uint64": {logger.Uint64("key", 42), "key", uint64(42)},
"Uint16": {logger.Uint16("key", 42), "key", uint64(42)},
"Float64": {logger.Float64("key", 42.42), "key", 42.42},
"Bool": {logger.Bool("key", true), "key", true},
"Time": {logger.Time("key", someTime), "key", someTime},
"Duration": {logger.Duration("key", time.Second), "key", time.Second},
"Any": {logger.Any("key", "value"), "key", "value"},
} {
tt := _tt
t.Run(name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.wantKey, tt.giveAttr.Key)
assert.Equal(t, tt.wantValue, tt.giveAttr.Value.Any())
})
}
}

View File

@ -60,3 +60,11 @@ func TestParseFormat(t *testing.T) {
})
}
}
func TestFormats(t *testing.T) {
require.Equal(t, []logger.Format{logger.ConsoleFormat, logger.JSONFormat}, logger.Formats())
}
func TestFormatStrings(t *testing.T) {
require.Equal(t, []string{"console", "json"}, logger.FormatStrings())
}

View File

@ -13,7 +13,6 @@ const (
InfoLevel // default level (zero-value)
WarnLevel
ErrorLevel
FatalLevel
)
// String returns a lower-case ASCII representation of the log level.
@ -27,8 +26,6 @@ func (l Level) String() string {
return "warn"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
}
return fmt.Sprintf("level(%d)", l)
@ -36,7 +33,7 @@ func (l Level) String() string {
// Levels returns a slice of all logging levels.
func Levels() []Level {
return []Level{DebugLevel, InfoLevel, WarnLevel, ErrorLevel, FatalLevel}
return []Level{DebugLevel, InfoLevel, WarnLevel, ErrorLevel}
}
// LevelStrings returns a slice of all logging levels as strings.
@ -75,8 +72,6 @@ func ParseLevel[T string | []byte](text T) (Level, error) {
return WarnLevel, nil
case "error":
return ErrorLevel, nil
case "fatal":
return FatalLevel, nil
}
return Level(0), fmt.Errorf("unrecognized logging level: %q", text)

View File

@ -18,7 +18,6 @@ func TestLevel_String(t *testing.T) {
"info": {giveLevel: logger.InfoLevel, wantString: "info"},
"warn": {giveLevel: logger.WarnLevel, wantString: "warn"},
"error": {giveLevel: logger.ErrorLevel, wantString: "error"},
"fatal": {giveLevel: logger.FatalLevel, wantString: "fatal"},
"<unknown>": {giveLevel: logger.Level(127), wantString: "level(127)"},
} {
t.Run(name, func(t *testing.T) {
@ -43,8 +42,6 @@ func TestParseLevel(t *testing.T) {
"info": {giveBytes: []byte("info"), wantLevel: logger.InfoLevel},
"warn": {giveBytes: []byte("warn"), wantLevel: logger.WarnLevel},
"error": {giveBytes: []byte("error"), wantLevel: logger.ErrorLevel},
"fatal": {giveBytes: []byte("fatal"), wantLevel: logger.FatalLevel},
"fatal (string)": {giveString: "fatal", wantLevel: logger.FatalLevel},
"foobar": {giveBytes: []byte("foobar"), wantError: errors.New("unrecognized logging level: \"foobar\"")}, //nolint:lll
} {
t.Run(name, func(t *testing.T) {
@ -75,10 +72,9 @@ func TestLevels(t *testing.T) {
logger.InfoLevel,
logger.WarnLevel,
logger.ErrorLevel,
logger.FatalLevel,
}, logger.Levels())
}
func TestLevelStrings(t *testing.T) {
require.Equal(t, []string{"debug", "info", "warn", "error", "fatal"}, logger.LevelStrings())
require.Equal(t, []string{"debug", "info", "warn", "error"}, logger.LevelStrings())
}

View File

@ -1,60 +1,126 @@
package logger
import (
"context"
"errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"io"
"log/slog"
"os"
"strings"
"time"
)
// New creates new "zap" logger with a small customization.
func New(l Level, f Format) (*zap.Logger, error) {
var config zap.Config
// internalAttrKeyLoggerName is used to store the logger name in the logger context (attributes).
const internalAttrKeyLoggerName = "named_logger"
switch f {
case ConsoleFormat:
config = zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder
config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05")
var (
// consoleFormatAttrReplacer is a replacer for console format. It replaces some attributes with more
// human-readable ones.
consoleFormatAttrReplacer = func(_ []string, a slog.Attr) slog.Attr { //nolint:gochecknoglobals
switch a.Key {
case internalAttrKeyLoggerName:
return slog.String("logger", a.Value.String())
case "level":
return slog.String(a.Key, strings.ToLower(a.Value.String()))
default:
if ts, ok := a.Value.Any().(time.Time); ok && a.Key == "time" {
return slog.String(a.Key, ts.Format("15:04:05"))
}
}
case JSONFormat:
config = zap.NewProductionConfig() // json encoder is used by default
default:
return nil, errors.New("unsupported logging format")
return a
}
// default configuration for all encoders
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
config.Development = false
config.DisableStacktrace = true
config.DisableCaller = true
// jsonFormatAttrReplacer is a replacer for JSON format. It replaces some attributes with more
// machine-readable ones.
jsonFormatAttrReplacer = func(_ []string, a slog.Attr) slog.Attr { //nolint:gochecknoglobals
switch a.Key {
case internalAttrKeyLoggerName:
return slog.String("logger", a.Value.String())
case "level":
return slog.String(a.Key, strings.ToLower(a.Value.String()))
default:
if ts, ok := a.Value.Any().(time.Time); ok && a.Key == "time" {
return slog.Float64("ts", float64(ts.Unix())+float64(ts.Nanosecond())/1e9)
}
}
// enable additional features for debugging
if l <= DebugLevel {
config.Development = true
config.DisableStacktrace = false
config.DisableCaller = false
return a
}
)
var zapLvl zapcore.Level
// Logger is a simple logger that wraps [slog.Logger]. It provides a more convenient API for logging and
// formatting messages.
type Logger struct {
ctx context.Context
slog *slog.Logger
lvl Level
}
switch l { // convert level to zap.Level
// New creates a new logger with the given level and format. Optionally, you can specify the writer to write logs to.
func New(l Level, f Format, writer ...io.Writer) (*Logger, error) {
var options slog.HandlerOptions
switch l {
case DebugLevel:
zapLvl = zap.DebugLevel
options.Level = slog.LevelDebug
case InfoLevel:
zapLvl = zap.InfoLevel
options.Level = slog.LevelInfo
case WarnLevel:
zapLvl = zap.WarnLevel
options.Level = slog.LevelWarn
case ErrorLevel:
zapLvl = zap.ErrorLevel
case FatalLevel:
zapLvl = zap.FatalLevel
options.Level = slog.LevelError
default:
return nil, errors.New("unsupported logging level")
}
config.Level = zap.NewAtomicLevelAt(zapLvl)
var (
handler slog.Handler
target io.Writer
)
return config.Build()
if len(writer) > 0 && writer[0] != nil {
target = writer[0]
} else {
target = os.Stderr
}
switch f {
case ConsoleFormat:
options.ReplaceAttr = consoleFormatAttrReplacer
handler = slog.NewTextHandler(target, &options)
case JSONFormat:
options.ReplaceAttr = jsonFormatAttrReplacer
handler = slog.NewJSONHandler(target, &options)
default:
return nil, errors.New("unsupported logging format")
}
return &Logger{ctx: context.Background(), slog: slog.New(handler), lvl: l}, nil
}
// Level returns the logger level.
func (l *Logger) Level() Level { return l.lvl }
// Named creates a new logger with the same properties as the original logger and the given name.
func (l *Logger) Named(name string) *Logger {
return &Logger{
ctx: l.ctx,
slog: l.slog.With(slog.String(internalAttrKeyLoggerName, name)),
lvl: l.lvl,
}
}
// Debug logs a message at DebugLevel.
func (l *Logger) Debug(msg string, f ...Attr) { l.slog.LogAttrs(l.ctx, slog.LevelDebug, msg, f...) }
// Info logs a message at InfoLevel.
func (l *Logger) Info(msg string, f ...Attr) { l.slog.LogAttrs(l.ctx, slog.LevelInfo, msg, f...) }
// Warn logs a message at WarnLevel.
func (l *Logger) Warn(msg string, f ...Attr) { l.slog.LogAttrs(l.ctx, slog.LevelWarn, msg, f...) }
// Error logs a message at ErrorLevel.
func (l *Logger) Error(msg string, f ...Attr) { l.slog.LogAttrs(l.ctx, slog.LevelError, msg, f...) }

View File

@ -0,0 +1,21 @@
package logger
import (
"context"
"log/slog"
)
// NewNop returns a no-op Logger. It never writes out logs or internal errors. The common use case is to use it
// in tests.
func NewNop() *Logger {
return &Logger{ctx: context.Background(), slog: slog.New(&noopHandler{}), lvl: DebugLevel}
}
type noopHandler struct{}
var _ slog.Handler = (*noopHandler)(nil) // verify interface implementation
func (noopHandler) Enabled(context.Context, slog.Level) bool { return true }
func (noopHandler) Handle(context.Context, slog.Record) error { return nil }
func (noopHandler) WithAttrs([]slog.Attr) slog.Handler { return noopHandler{} }
func (noopHandler) WithGroup(string) slog.Handler { return noopHandler{} }

View File

@ -1,75 +1,235 @@
package logger_test
import (
"regexp"
"strings"
"bytes"
"strconv"
"testing"
"time"
"github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gh.tarampamp.am/error-pages/internal/logger"
)
func TestNewDebugLevelConsoleFormat(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(logger.DebugLevel, logger.ConsoleFormat)
require.NoError(t, err)
log.Debug("dbg msg")
log.Info("inf msg")
log.Error("err msg")
})
assert.Contains(t, output, time.Now().Format("15:04:05"))
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
assert.Regexp(t, `\t.+info.+\t.+logger_test\.go:\d+\tinf msg`, output)
assert.Contains(t, output, "dbg msg")
assert.Contains(t, output, "err msg")
}
func TestNewErrorLevelConsoleFormat(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(logger.ErrorLevel, logger.ConsoleFormat)
require.NoError(t, err)
log.Debug("dbg msg")
log.Info("inf msg")
log.Error("err msg")
})
assert.NotContains(t, output, "inf msg")
assert.NotContains(t, output, "dbg msg")
assert.Contains(t, output, "err msg")
}
func TestNewWarnLevelJSONFormat(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(logger.WarnLevel, logger.JSONFormat)
require.NoError(t, err)
log.Debug("dbg msg")
log.Info("inf msg")
log.Warn("warn msg")
log.Error("err msg")
})
// replace timestamp field with fixed value
fakeTimestamp := regexp.MustCompile(`"ts":\d+\.\d+,`)
output = fakeTimestamp.ReplaceAllString(output, `"ts":0.1,`)
lines := strings.Split(strings.Trim(output, "\n"), "\n")
assert.JSONEq(t, `{"level":"warn","ts":0.1,"msg":"warn msg"}`, lines[0])
assert.JSONEq(t, `{"level":"error","ts":0.1,"msg":"err msg"}`, lines[1])
}
func TestNewErrors(t *testing.T) {
_, err := logger.New(logger.Level(127), logger.ConsoleFormat)
log, err := logger.New(logger.Level(127), logger.ConsoleFormat)
require.Nil(t, log)
require.EqualError(t, err, "unsupported logging level")
_, err = logger.New(logger.WarnLevel, logger.Format(255))
log, err = logger.New(logger.WarnLevel, logger.Format(255))
require.Nil(t, log)
require.EqualError(t, err, "unsupported logging format")
}
func TestLogger_ConsoleFormat(t *testing.T) {
var (
buf bytes.Buffer
log, logErr = logger.New(logger.DebugLevel, logger.ConsoleFormat, &buf)
now = time.Now()
)
require.NoError(t, logErr)
assert.Equal(t, logger.DebugLevel, log.Level())
log.Debug("debug message",
logger.String("String", "value"),
logger.Strings("Strings", "foo", "bar", ""),
logger.Int64("Int64", 0),
logger.Int("Int", 1),
logger.Uint64("Uint64", 2),
logger.Float64("Float64", 3.14),
logger.Bool("Bool", true),
logger.Time("Time", now),
logger.Duration("Duration", time.Millisecond),
)
var output = buf.String()
assert.Contains(t, output, `time=`+now.Format("15:04:")) // match without seconds
assert.Contains(t, output, `level=debug`)
assert.Contains(t, output, `msg="debug message"`)
assert.Contains(t, output, "String=value")
assert.Contains(t, output, `Strings="[foo bar ]"`)
assert.Contains(t, output, "Int64=0")
assert.Contains(t, output, "Int=1")
assert.Contains(t, output, "Uint64=2")
assert.Contains(t, output, "Float64=3.14")
assert.Contains(t, output, "Bool=true")
assert.Contains(t, output, "Time="+now.Format("2006-01-02T15:04:05.000Z07:00"))
assert.Contains(t, output, "Duration=1ms")
}
func TestLogger_JSONFormat(t *testing.T) {
var (
buf bytes.Buffer
log, logErr = logger.New(logger.DebugLevel, logger.JSONFormat, &buf)
now = time.Now()
)
require.NoError(t, logErr)
assert.Equal(t, logger.DebugLevel, log.Level())
log.Debug("debug message",
logger.String("String", "value"),
logger.Strings("Strings", "foo", "bar", ""),
logger.Int64("Int64", 0),
logger.Int("Int", 1),
logger.Uint64("Uint64", 2),
logger.Float64("Float64", 3.14),
logger.Bool("Bool", true),
logger.Time("Time", now),
logger.Duration("Duration", time.Millisecond),
)
var output = buf.String()
assert.Contains(t, output, `"ts":`+strconv.Itoa(int(now.Unix()))+".") // match without nanoseconds
assert.Contains(t, output, `"level":"debug"`)
assert.Contains(t, output, `"msg":"debug message"`)
assert.Contains(t, output, `"String":"value"`)
assert.Contains(t, output, `"Strings":["foo","bar",""]`)
assert.Contains(t, output, `"Int64":0`)
assert.Contains(t, output, `"Int":1`)
assert.Contains(t, output, `"Uint64":2`)
assert.Contains(t, output, `"Float64":3.14`)
assert.Contains(t, output, `"Bool":true`)
assert.Contains(t, output, `"Time":"`+now.Format("2006-01-02T15:04:05.000")) // omit nano seconds
assert.Contains(t, output, `"Duration":1000000`)
}
func TestLogger_Debug(t *testing.T) {
var (
buf bytes.Buffer
log, logErr = logger.New(logger.DebugLevel, logger.JSONFormat, &buf)
)
require.NoError(t, logErr)
assert.Equal(t, logger.DebugLevel, log.Level())
log.Debug("debug message")
log.Info("info message")
log.Warn("warn message")
log.Error("error message")
var output = buf.String()
assert.Contains(t, output, "debug message")
assert.Contains(t, output, "info message")
assert.Contains(t, output, "warn message")
assert.Contains(t, output, "error message")
}
func TestLogger_Info(t *testing.T) {
var (
buf bytes.Buffer
log, logErr = logger.New(logger.InfoLevel, logger.JSONFormat, &buf)
)
require.NoError(t, logErr)
assert.Equal(t, logger.InfoLevel, log.Level())
log.Debug("debug message")
log.Info("info message")
log.Warn("warn message")
log.Error("error message")
var output = buf.String()
assert.NotContains(t, output, "debug message")
assert.Contains(t, output, "info message")
assert.Contains(t, output, "warn message")
assert.Contains(t, output, "error message")
}
func TestLogger_Warn(t *testing.T) {
var (
buf bytes.Buffer
log, logErr = logger.New(logger.WarnLevel, logger.JSONFormat, &buf)
)
require.NoError(t, logErr)
assert.Equal(t, logger.WarnLevel, log.Level())
log.Debug("debug message")
log.Info("info message")
log.Warn("warn message")
log.Error("error message")
var output = buf.String()
assert.NotContains(t, output, "debug message")
assert.NotContains(t, output, "info message")
assert.Contains(t, output, "warn message")
assert.Contains(t, output, "error message")
}
func TestLogger_Error(t *testing.T) {
var (
buf bytes.Buffer
log, logErr = logger.New(logger.ErrorLevel, logger.JSONFormat, &buf)
)
require.NoError(t, logErr)
assert.Equal(t, logger.ErrorLevel, log.Level())
log.Debug("debug message")
log.Info("info message")
log.Warn("warn message")
log.Error("error message")
var output = buf.String()
assert.NotContains(t, output, "debug message")
assert.NotContains(t, output, "info message")
assert.NotContains(t, output, "warn message")
assert.Contains(t, output, "error message")
}
func TestLogger_Named_JSONFormat(t *testing.T) {
var (
buf bytes.Buffer
log, _ = logger.New(logger.DebugLevel, logger.JSONFormat, &buf)
named = log.Named("test_name")
)
log.Debug("debug message")
var output = buf.String()
assert.Contains(t, output, `"msg":"debug message"`)
assert.NotContains(t, output, `"logger":"`)
buf.Reset()
named.Debug("named log message")
output = buf.String()
assert.Contains(t, output, `"msg":"named log message"`)
assert.Contains(t, output, `"logger":"test_name"`)
}
func TestLogger_Named_ConsoleFormat(t *testing.T) {
var (
buf bytes.Buffer
log, _ = logger.New(logger.DebugLevel, logger.ConsoleFormat, &buf)
named = log.Named("test_name")
)
log.Debug("debug message")
var output = buf.String()
assert.Contains(t, output, `msg="debug message"`)
assert.NotContains(t, output, `logger=`)
buf.Reset()
named.Debug("named log message")
output = buf.String()
assert.Contains(t, output, `msg="named log message"`)
assert.Contains(t, output, `logger=test_name`)
}

14
internal/logger/std.go Normal file
View File

@ -0,0 +1,14 @@
package logger
import (
stdLog "log"
)
// NewStdLog returns a *[log.Logger] which writes to the supplied [Logger] at [InfoLevel].
func NewStdLog(log *Logger) *stdLog.Logger {
return stdLog.New(&loggerWriter{log} /* prefix */, "" /* flags */, 0)
}
type loggerWriter struct{ log *Logger }
func (lw *loggerWriter) Write(p []byte) (int, error) { lw.log.Info(string(p)); return len(p), nil } //nolint:nlreturn

View File

@ -0,0 +1,23 @@
package logger_test
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/logger"
)
func TestNewStdLog(t *testing.T) {
var (
buf bytes.Buffer
log, _ = logger.New(logger.InfoLevel, logger.JSONFormat, &buf)
std = logger.NewStdLog(log)
)
std.Print("test")
assert.Contains(t, buf.String(), "test")
}