Skip to content

Commit

Permalink
Merge pull request #143 from wneessen/feature/142_implement-structure…
Browse files Browse the repository at this point in the history
…d-json-logger

#142 Add structured JSON logger and associated tests
  • Loading branch information
wneessen authored Aug 23, 2023
2 parents 0189acf + 77d9e3d commit 8f3e5d3
Show file tree
Hide file tree
Showing 3 changed files with 406 additions and 0 deletions.
82 changes: 82 additions & 0 deletions log/jsonlog.go
Original file line number Diff line number Diff line change
@@ -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 changes: 297 additions & 0 deletions log/jsonlog_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 8f3e5d3

Please sign in to comment.