mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-09 15:32:54 +01:00
#142 Add structured JSON logger and associated tests
This commit introduces a new type 'JSONlog' that satisfies the Logger interface for structured JSON logging. This includes new methods 'Debugf', 'Infof', 'Warnf' and 'Errorf' to log messages at different levels and an associated test 'jsonlog_test.go' to ensure correct functionality. This enhances the logging functionality by providing clarity in logs and eases debugging process.
This commit is contained in:
parent
0189acf1e4
commit
77d9e3d02a
3 changed files with 406 additions and 0 deletions
82
log/jsonlog.go
Normal file
82
log/jsonlog.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//go:build go1.21
|
||||||
|
// +build go1.21
|
||||||
|
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSONlog is the default structured JSON logger that satisfies the Logger interface
|
||||||
|
type JSONlog struct {
|
||||||
|
l Level
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSON returns a new JSONlog type that satisfies the Logger interface
|
||||||
|
func NewJSON(o io.Writer, l Level) *JSONlog {
|
||||||
|
lo := slog.HandlerOptions{}
|
||||||
|
switch l {
|
||||||
|
case LevelDebug:
|
||||||
|
lo.Level = slog.LevelDebug
|
||||||
|
case LevelInfo:
|
||||||
|
lo.Level = slog.LevelInfo
|
||||||
|
case LevelWarn:
|
||||||
|
lo.Level = slog.LevelWarn
|
||||||
|
case LevelError:
|
||||||
|
lo.Level = slog.LevelError
|
||||||
|
default:
|
||||||
|
lo.Level = slog.LevelDebug
|
||||||
|
}
|
||||||
|
lh := slog.NewJSONHandler(o, &lo)
|
||||||
|
return &JSONlog{
|
||||||
|
l: l,
|
||||||
|
log: slog.New(lh),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs a debug message via the structured JSON logger
|
||||||
|
func (l *JSONlog) Debugf(lo Log) {
|
||||||
|
if l.l >= LevelDebug {
|
||||||
|
l.log.WithGroup(DirString).With(
|
||||||
|
slog.String(DirFromString, lo.directionFrom()),
|
||||||
|
slog.String(DirToString, lo.directionTo()),
|
||||||
|
).Debug(fmt.Sprintf(lo.Format, lo.Messages...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof logs a info message via the structured JSON logger
|
||||||
|
func (l *JSONlog) Infof(lo Log) {
|
||||||
|
if l.l >= LevelInfo {
|
||||||
|
l.log.WithGroup(DirString).With(
|
||||||
|
slog.String(DirFromString, lo.directionFrom()),
|
||||||
|
slog.String(DirToString, lo.directionTo()),
|
||||||
|
).Info(fmt.Sprintf(lo.Format, lo.Messages...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf logs a warn message via the structured JSON logger
|
||||||
|
func (l *JSONlog) Warnf(lo Log) {
|
||||||
|
if l.l >= LevelWarn {
|
||||||
|
l.log.WithGroup(DirString).With(
|
||||||
|
slog.String(DirFromString, lo.directionFrom()),
|
||||||
|
slog.String(DirToString, lo.directionTo()),
|
||||||
|
).Warn(fmt.Sprintf(lo.Format, lo.Messages...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs a warn message via the structured JSON logger
|
||||||
|
func (l *JSONlog) Errorf(lo Log) {
|
||||||
|
if l.l >= LevelError {
|
||||||
|
l.log.WithGroup(DirString).With(
|
||||||
|
slog.String(DirFromString, lo.directionFrom()),
|
||||||
|
slog.String(DirToString, lo.directionTo()),
|
||||||
|
).Error(fmt.Sprintf(lo.Format, lo.Messages...))
|
||||||
|
}
|
||||||
|
}
|
297
log/jsonlog_test.go
Normal file
297
log/jsonlog_test.go
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//go:build go1.21
|
||||||
|
// +build go1.21
|
||||||
|
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsonLog struct {
|
||||||
|
Direction jsonDir `json:"direction"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Message string `json:"msg"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonDir struct {
|
||||||
|
From string `json:"from"`
|
||||||
|
To string `json:"to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewJSON(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
l := NewJSON(&b, LevelDebug)
|
||||||
|
if l.l != LevelDebug {
|
||||||
|
t.Error("Expected level to be LevelDebug, got ", l.l)
|
||||||
|
}
|
||||||
|
if l.log == nil {
|
||||||
|
t.Error("logger not initialized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONDebugf(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
l := NewJSON(&b, LevelDebug)
|
||||||
|
f := "test %s"
|
||||||
|
msg := "foo"
|
||||||
|
msg2 := "bar"
|
||||||
|
|
||||||
|
l.Debugf(Log{Direction: DirServerToClient, Format: f, Messages: []interface{}{msg}})
|
||||||
|
exFrom := "server"
|
||||||
|
exTo := "client"
|
||||||
|
jl, err := unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.Debugf(Log{Direction: DirClientToServer, Format: f, Messages: []interface{}{msg2}})
|
||||||
|
exFrom = "client"
|
||||||
|
exTo = "server"
|
||||||
|
jl, err = unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg2) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg2, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.l = LevelInfo
|
||||||
|
l.Debugf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
|
||||||
|
if b.String() != "" {
|
||||||
|
t.Error("Debug message was not expected to be logged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONDebugf_WithDefault(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
l := NewJSON(&b, 999)
|
||||||
|
f := "test %s"
|
||||||
|
msg := "foo"
|
||||||
|
msg2 := "bar"
|
||||||
|
|
||||||
|
l.Debugf(Log{Direction: DirServerToClient, Format: f, Messages: []interface{}{msg}})
|
||||||
|
exFrom := "server"
|
||||||
|
exTo := "client"
|
||||||
|
jl, err := unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.Debugf(Log{Direction: DirClientToServer, Format: f, Messages: []interface{}{msg2}})
|
||||||
|
exFrom = "client"
|
||||||
|
exTo = "server"
|
||||||
|
jl, err = unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg2) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg2, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.l = LevelInfo
|
||||||
|
l.Debugf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
|
||||||
|
if b.String() != "" {
|
||||||
|
t.Error("Debug message was not expected to be logged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONInfof(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
l := NewJSON(&b, LevelInfo)
|
||||||
|
f := "test %s"
|
||||||
|
msg := "foo"
|
||||||
|
msg2 := "bar"
|
||||||
|
|
||||||
|
l.Infof(Log{Direction: DirServerToClient, Format: f, Messages: []interface{}{msg}})
|
||||||
|
exFrom := "server"
|
||||||
|
exTo := "client"
|
||||||
|
jl, err := unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.Infof(Log{Direction: DirClientToServer, Format: f, Messages: []interface{}{msg2}})
|
||||||
|
exFrom = "client"
|
||||||
|
exTo = "server"
|
||||||
|
jl, err = unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg2) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg2, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.l = LevelWarn
|
||||||
|
l.Infof(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
|
||||||
|
if b.String() != "" {
|
||||||
|
t.Error("Info message was not expected to be logged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONWarnf(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
l := NewJSON(&b, LevelWarn)
|
||||||
|
f := "test %s"
|
||||||
|
msg := "foo"
|
||||||
|
msg2 := "bar"
|
||||||
|
|
||||||
|
l.Warnf(Log{Direction: DirServerToClient, Format: f, Messages: []interface{}{msg}})
|
||||||
|
exFrom := "server"
|
||||||
|
exTo := "client"
|
||||||
|
jl, err := unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.Warnf(Log{Direction: DirClientToServer, Format: f, Messages: []interface{}{msg2}})
|
||||||
|
exFrom = "client"
|
||||||
|
exTo = "server"
|
||||||
|
jl, err = unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg2) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg2, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.l = LevelError
|
||||||
|
l.Warnf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
|
||||||
|
if b.String() != "" {
|
||||||
|
t.Error("Warn message was not expected to be logged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONErrorf(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
l := NewJSON(&b, LevelError)
|
||||||
|
f := "test %s"
|
||||||
|
msg := "foo"
|
||||||
|
msg2 := "bar"
|
||||||
|
|
||||||
|
l.Errorf(Log{Direction: DirServerToClient, Format: f, Messages: []interface{}{msg}})
|
||||||
|
exFrom := "server"
|
||||||
|
exTo := "client"
|
||||||
|
jl, err := unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.Errorf(Log{Direction: DirClientToServer, Format: f, Messages: []interface{}{msg2}})
|
||||||
|
exFrom = "client"
|
||||||
|
exTo = "server"
|
||||||
|
jl, err = unmarshalLog(b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Debugf() failed, unmarshal json log message failed: %s", err)
|
||||||
|
}
|
||||||
|
if jl.Direction.To != exTo {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exTo, jl.Direction.To)
|
||||||
|
}
|
||||||
|
if jl.Direction.From != exFrom {
|
||||||
|
t.Errorf("Debugf() failed, expected message to: %s, got: %s", exFrom, jl.Direction.From)
|
||||||
|
}
|
||||||
|
if jl.Message != fmt.Sprintf(f, msg2) {
|
||||||
|
t.Errorf("Debugf() failed, expected message: %s, got %s", msg2, jl.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
l.l = -99
|
||||||
|
l.Errorf(Log{Direction: DirServerToClient, Format: "test %s", Messages: []interface{}{"foo"}})
|
||||||
|
if b.String() != "" {
|
||||||
|
t.Error("Error message was not expected to be logged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalLog(j []byte) (jsonLog, error) {
|
||||||
|
var jl jsonLog
|
||||||
|
if err := json.Unmarshal(j, &jl); err != nil {
|
||||||
|
return jl, err
|
||||||
|
}
|
||||||
|
return jl, nil
|
||||||
|
}
|
27
log/log.go
27
log/log.go
|
@ -21,6 +21,15 @@ const (
|
||||||
LevelDebug
|
LevelDebug
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DirString is a constant used for the structured logger
|
||||||
|
DirString = "direction"
|
||||||
|
// DirFromString is a constant used for the structured logger
|
||||||
|
DirFromString = "from"
|
||||||
|
// DirToString is a constant used for the structured logger
|
||||||
|
DirToString = "to"
|
||||||
|
)
|
||||||
|
|
||||||
// Direction is a type wrapper for the direction a debug log message goes
|
// Direction is a type wrapper for the direction a debug log message goes
|
||||||
type Direction int
|
type Direction int
|
||||||
|
|
||||||
|
@ -51,3 +60,21 @@ func (l Log) directionPrefix() string {
|
||||||
}
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// directionFrom will return a from direction string depending on the Direction.
|
||||||
|
func (l Log) directionFrom() string {
|
||||||
|
p := "server"
|
||||||
|
if l.Direction == DirClientToServer {
|
||||||
|
p = "client"
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// directionTo will return a to direction string depending on the Direction.
|
||||||
|
func (l Log) directionTo() string {
|
||||||
|
p := "client"
|
||||||
|
if l.Direction == DirClientToServer {
|
||||||
|
p = "server"
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue