diff --git a/loggers/noop/context.go b/loggers/noop/context.go new file mode 100644 index 0000000..6a67695 --- /dev/null +++ b/loggers/noop/context.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 + +package noop + +import ( + "net" + + "github.com/loopholelabs/logging/types" +) + +var _ types.Context = (*Context)(nil) + +type Context struct { + l *Logger +} + +func (c *Context) Logger() types.SubLogger { + return c.l +} + +func (c *Context) Str(key string, val string) types.Context { + return c +} + +func (c *Context) Bool(key string, val bool) types.Context { + return c +} + +func (c *Context) Int(key string, val int) types.Context { + return c +} + +func (c *Context) Int8(key string, val int8) types.Context { + return c +} + +func (c *Context) Int16(key string, val int16) types.Context { + return c +} + +func (c *Context) Int32(key string, val int32) types.Context { + return c +} + +func (c *Context) Int64(key string, val int64) types.Context { + return c +} + +func (c *Context) Uint(key string, val uint) types.Context { + return c +} + +func (c *Context) Uint8(key string, val uint8) types.Context { + return c +} + +func (c *Context) Uint16(key string, val uint16) types.Context { + return c +} + +func (c *Context) Uint32(key string, val uint32) types.Context { + return c +} + +func (c *Context) Uint64(key string, val uint64) types.Context { + return c +} + +func (c *Context) Float32(key string, val float32) types.Context { + return c +} + +func (c *Context) Float64(key string, val float64) types.Context { + return c +} + +func (c *Context) IPAddr(key string, ipAddr net.IP) types.Context { + return c +} + +func (c *Context) MACAddr(key string, macAddr net.HardwareAddr) types.Context { + return c +} + +func (c *Context) Err(err error) types.Context { + return c +} diff --git a/loggers/noop/noop.go b/loggers/noop/noop.go index 66b4787..ba0b45e 100644 --- a/loggers/noop/noop.go +++ b/loggers/noop/noop.go @@ -24,6 +24,10 @@ func (s *Logger) Level() types.Level { func (s *Logger) SubLogger(string) types.SubLogger { return s } +func (s *Logger) With() types.Context { + return &Context{l: s} +} + func (s *Logger) Fatal() types.Event { return new(Event) } diff --git a/loggers/slog/context.go b/loggers/slog/context.go new file mode 100644 index 0000000..8eb62cb --- /dev/null +++ b/loggers/slog/context.go @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 + +package slog + +import ( + "log/slog" + "net" + + "github.com/loopholelabs/logging/types" +) + +var _ types.Context = (*Context)(nil) + +type Context struct { + l *Logger + attrs []any +} + +func (c *Context) Logger() types.SubLogger { + l := New(c.l.source, c.l.level, c.l.output) + l.logger = c.l.logger.With(c.attrs...) + return l +} + +func (c *Context) Str(key string, val string) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.StringValue(val), + }) + return c +} + +func (c *Context) Bool(key string, val bool) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.BoolValue(val), + }) + return c +} + +func (c *Context) Int(key string, val int) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.IntValue(val), + }) + return c +} + +func (c *Context) Int8(key string, val int8) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.IntValue(int(val)), + }) + return c +} + +func (c *Context) Int16(key string, val int16) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.IntValue(int(val)), + }) + return c +} + +func (c *Context) Int32(key string, val int32) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.IntValue(int(val)), + }) + return c +} + +func (c *Context) Int64(key string, val int64) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.Int64Value(val), + }) + return c +} + +func (c *Context) Uint(key string, val uint) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.Uint64Value(uint64(val)), + }) + return c +} + +func (c *Context) Uint8(key string, val uint8) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.Uint64Value(uint64(val)), + }) + return c +} + +func (c *Context) Uint16(key string, val uint16) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.Uint64Value(uint64(val)), + }) + return c +} + +func (c *Context) Uint32(key string, val uint32) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.Uint64Value(uint64(val)), + }) + return c +} + +func (c *Context) Uint64(key string, val uint64) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.Uint64Value(val), + }) + return c +} + +func (c *Context) Float32(key string, val float32) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.Float64Value(float64(val)), + }) + return c +} + +func (c *Context) Float64(key string, val float64) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.Float64Value(float64(val)), + }) + return c +} + +func (c *Context) IPAddr(key string, ipAddr net.IP) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.StringValue(ipAddr.String()), + }) + return c +} + +func (c *Context) MACAddr(key string, macAddr net.HardwareAddr) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: key, + Value: slog.StringValue(macAddr.String()), + }) + return c +} + +func (c *Context) Err(err error) types.Context { + c.attrs = append(c.attrs, slog.Attr{ + Key: "error", + Value: slog.StringValue(err.Error()), + }) + return c +} diff --git a/loggers/slog/slog.go b/loggers/slog/slog.go index c2cd538..ba9b3f1 100644 --- a/loggers/slog/slog.go +++ b/loggers/slog/slog.go @@ -83,6 +83,10 @@ func (s *Logger) SubLogger(source string) types.SubLogger { return newSlog(fmt.Sprintf("%s:%s", s.source, source), sloglevel, s.output) } +func (s *Logger) With() types.Context { + return &Context{l: s} +} + func (s *Logger) Fatal() types.Event { return &Event{ level: slog.LevelError + 1, diff --git a/loggers/zerolog/context.go b/loggers/zerolog/context.go new file mode 100644 index 0000000..6b51f25 --- /dev/null +++ b/loggers/zerolog/context.go @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 + +package zerolog + +import ( + "net" + + "github.com/loopholelabs/logging/types" + "github.com/rs/zerolog" +) + +var _ types.Context = (*Context)(nil) + +type Context struct { + l *Logger + zeroCtx zerolog.Context +} + +func (c *Context) Logger() types.SubLogger { + return &Logger{ + logger: c.zeroCtx.Logger(), + source: c.l.source, + level: c.l.level, + } +} + +func (c *Context) Str(key string, val string) types.Context { + c.zeroCtx = c.zeroCtx.Str(key, val) + return c +} + +func (c *Context) Bool(key string, val bool) types.Context { + c.zeroCtx = c.zeroCtx.Bool(key, val) + return c +} + +func (c *Context) Int(key string, val int) types.Context { + c.zeroCtx = c.zeroCtx.Int(key, val) + return c +} + +func (c *Context) Int8(key string, val int8) types.Context { + c.zeroCtx = c.zeroCtx.Int8(key, val) + return c +} + +func (c *Context) Int16(key string, val int16) types.Context { + c.zeroCtx = c.zeroCtx.Int16(key, val) + return c +} + +func (c *Context) Int32(key string, val int32) types.Context { + c.zeroCtx = c.zeroCtx.Int32(key, val) + return c +} + +func (c *Context) Int64(key string, val int64) types.Context { + c.zeroCtx = c.zeroCtx.Int64(key, val) + return c +} + +func (c *Context) Uint(key string, val uint) types.Context { + c.zeroCtx = c.zeroCtx.Uint(key, val) + return c +} + +func (c *Context) Uint8(key string, val uint8) types.Context { + c.zeroCtx = c.zeroCtx.Uint8(key, val) + return c +} + +func (c *Context) Uint16(key string, val uint16) types.Context { + c.zeroCtx = c.zeroCtx.Uint16(key, val) + return c +} + +func (c *Context) Uint32(key string, val uint32) types.Context { + c.zeroCtx = c.zeroCtx.Uint32(key, val) + return c +} + +func (c *Context) Uint64(key string, val uint64) types.Context { + c.zeroCtx = c.zeroCtx.Uint64(key, val) + return c +} + +func (c *Context) Float32(key string, val float32) types.Context { + c.zeroCtx = c.zeroCtx.Float32(key, val) + return c +} + +func (c *Context) Float64(key string, val float64) types.Context { + c.zeroCtx = c.zeroCtx.Float64(key, val) + return c +} + +func (c *Context) IPAddr(key string, val net.IP) types.Context { + c.zeroCtx = c.zeroCtx.IPAddr(key, val) + return c +} + +func (c *Context) MACAddr(key string, val net.HardwareAddr) types.Context { + c.zeroCtx = c.zeroCtx.MACAddr(key, val) + return c +} + +func (c *Context) Err(err error) types.Context { + c.zeroCtx = c.zeroCtx.Err(err) + return c +} diff --git a/loggers/zerolog/zerolog.go b/loggers/zerolog/zerolog.go index c8cd2de..a9ddcb7 100644 --- a/loggers/zerolog/zerolog.go +++ b/loggers/zerolog/zerolog.go @@ -65,6 +65,13 @@ func (z *Logger) SubLogger(source string) types.SubLogger { } } +func (z *Logger) With() types.Context { + return &Context{ + l: z, + zeroCtx: z.logger.With(), + } +} + func (z *Logger) Fatal() types.Event { return (*Event)(z.logger.Fatal().Timestamp().Str(types.SourceKey, z.source)) } diff --git a/logging_test.go b/logging_test.go index fb8c392..c01fe64 100644 --- a/logging_test.go +++ b/logging_test.go @@ -5,11 +5,12 @@ package logging import ( "bytes" "fmt" - "github.com/rs/zerolog" slogLogger "log/slog" "testing" "time" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" "github.com/loopholelabs/logging/loggers/slog" @@ -119,4 +120,82 @@ func TestInfo(t *testing.T) { assert.Equal(t, fillSlogTestFields(t, "level=INFO msg=\"\" source=%s foo=bar n=123\n"), out.String()) }) }) + + t.Run("with", func(t *testing.T) { + t.Run("noop", func(t *testing.T) { + out := &bytes.Buffer{} + log := New(Noop, t.Name(), out) + logger := log.With(). + Str("foo", "bar"). + Logger() + logger.Info().Msg("") + assert.Equal(t, "", out.String()) + }) + + t.Run("zerolog", func(t *testing.T) { + out := &bytes.Buffer{} + log := New(Zerolog, t.Name(), out) + + logger := log.With(). + Str("foo", "bar"). + Logger() + logger.Info().Msg("") + assert.Equal(t, fillZerologTestFields(t, "{\"level\":\"info\",\"foo\":\"bar\",\"time\":\"%s\",\"source\":\"%s\"}\n"), out.String()) + + // Log with per-message attribute. + out.Reset() + logger.Info().Int("n", 123).Msg("") + assert.Equal(t, fillZerologTestFields(t, "{\"level\":\"info\",\"foo\":\"bar\",\"time\":\"%s\",\"source\":\"%s\",\"n\":123}\n"), out.String()) + + // Retain attributes on sublogger. + out.Reset() + logger2 := logger.With(). + Str("foo2", "bar2"). + Logger() + logger2.Info().Msg("") + assert.Equal(t, fillZerologTestFields(t, "{\"level\":\"info\",\"foo\":\"bar\",\"foo2\":\"bar2\",\"time\":\"%s\",\"source\":\"%s\"}\n"), out.String()) + + // Ensure original loggers were not modified. + out.Reset() + log.Info().Msg("") + assert.Equal(t, fillZerologTestFields(t, "{\"level\":\"info\",\"time\":\"%s\",\"source\":\"%s\"}\n"), out.String()) + + out.Reset() + logger.Info().Msg("") + assert.Equal(t, fillZerologTestFields(t, "{\"level\":\"info\",\"foo\":\"bar\",\"time\":\"%s\",\"source\":\"%s\"}\n"), out.String()) + }) + + t.Run("slog", func(t *testing.T) { + out := &bytes.Buffer{} + log := New(Slog, t.Name(), out) + + logger := log.With(). + Str("foo", "bar"). + Logger() + logger.Info().Msg("") + assert.Equal(t, fillSlogTestFields(t, "level=INFO msg=\"\" source=%s foo=bar\n"), out.String()) + + // Log with per-message attribute. + out.Reset() + logger.Info().Int("n", 123).Msg("") + assert.Equal(t, fillSlogTestFields(t, "level=INFO msg=\"\" source=%s foo=bar n=123\n"), out.String()) + + // Retain attributes on sublogger. + out.Reset() + logger2 := logger.With(). + Str("foo2", "bar2"). + Logger() + logger2.Info().Msg("") + assert.Equal(t, fillSlogTestFields(t, "level=INFO msg=\"\" source=%s foo=bar foo2=bar2\n"), out.String()) + + // Ensure original loggers were not modified. + out.Reset() + log.Info().Msg("") + assert.Equal(t, fillSlogTestFields(t, "level=INFO msg=\"\" source=%s\n"), out.String()) + + out.Reset() + logger.Info().Msg("") + assert.Equal(t, fillSlogTestFields(t, "level=INFO msg=\"\" source=%s foo=bar\n"), out.String()) + }) + }) } diff --git a/types/types.go b/types/types.go index e2709f0..3acb465 100644 --- a/types/types.go +++ b/types/types.go @@ -29,6 +29,7 @@ type Logger interface { type SubLogger interface { Level() Level SubLogger(source string) SubLogger + With() Context Fatal() Event Error() Event @@ -39,29 +40,40 @@ type SubLogger interface { } type Event interface { - Str(key string, val string) Event - Bool(key string, val bool) Event + taggable[Event] - Int(key string, val int) Event - Int8(key string, val int8) Event - Int16(key string, val int16) Event - Int32(key string, val int32) Event - Int64(key string, val int64) Event + Msg(msg string) + Msgf(format string, args ...interface{}) +} - Uint(key string, val uint) Event - Uint8(key string, val uint8) Event - Uint16(key string, val uint16) Event - Uint32(key string, val uint32) Event - Uint64(key string, val uint64) Event +type Context interface { + taggable[Context] - Float32(key string, val float32) Event - Float64(key string, val float64) Event + Logger() SubLogger +} - IPAddr(key string, ipAddr net.IP) Event - MACAddr(key string, macAddr net.HardwareAddr) Event +// taggable represents values that can receive structured fields. +type taggable[T any] interface { + Str(key string, val string) T + Bool(key string, val bool) T - Err(err error) Event + Int(key string, val int) T + Int8(key string, val int8) T + Int16(key string, val int16) T + Int32(key string, val int32) T + Int64(key string, val int64) T - Msg(msg string) - Msgf(format string, args ...interface{}) + Uint(key string, val uint) T + Uint8(key string, val uint8) T + Uint16(key string, val uint16) T + Uint32(key string, val uint32) T + Uint64(key string, val uint64) T + + Float32(key string, val float32) T + Float64(key string, val float64) T + + IPAddr(key string, ipAddr net.IP) T + MACAddr(key string, macAddr net.HardwareAddr) T + + Err(err error) T }