README
¶
Logkit: Structured JSON Log Testing for Go
logkit is a lightweight, powerful Go library for testing structured JSON logs.
It simplifies capturing, filtering, waiting for, and asserting log entries,
ensuring your logging logic is robust and reliable. Designed for flexibility, it
integrates seamlessly with any logging library that supports JSON output and
custom io.Writer destinations.
Why Logkit?
Logging is critical for debugging and monitoring applications, but testing logs
is often overlooked or cumbersome. The logkit module addresses this by
providing:
- Simple Assertions: Validate log levels, messages, and fields with clear, type-safe assert methods.
- Flexible Matching: Define custom matchers to test complex log structures.
- Asynchronous Support: Wait for logs in concurrent applications with configurable timeouts.
- File Integration: Load and test logs directly from files.
- Type Safety: Ensure fields match expected types (e.g., string, number, time) with precise error reporting.
Installation
go get github.com/ctx42/logkit
Usage
With Zerolog
The zerolog log message format is supported out of the box without any additional configuration.
func Test_Zerolog(t *testing.T) {
// --- Given ---
tst := logkit.New(t) // Initialize logkit.
// Configure zerolog with Tester as the writer.
log := zerolog.New(tst)
// --- When ---
// Use the log instance in your application.
log.Info().Int("A", 0).Str("B", "x").Msg("msg 0")
log.Warn().Int("A", 1).Str("B", "y").Msg("msg 1")
log.Error().Int("A", 2).Str("B", "z").Msg("msg 2")
// --- Then ---
ets := tst.Entries()
ets.AssertNumber("A", 2) // Success.
ets.AssertStr("B", "z") // Success.
t.Log(tst.Entries().Summary())
}
Slog
The log/slog log message format is supported
by providing logkit.SlogConfig() configuration to the logkit.Tester.
func Test_Slog(t *testing.T) {
// --- Given ---
opt := logkit.WithConfig(logkit.SlogConfig()) // Configure logkit.
tst := logkit.New(t, opt) // Initialize logkit.
// Configure slog.
log := slog.New(slog.NewJSONHandler(tst, nil))
// --- When ---
// Use the log instance in your application.
log.Info("msg 0", "A", 0, "B", "x")
log.Warn("msg 1", "A", 1, "B", "y")
log.Error("msg 2", "A", 2, "B", "z")
// --- Then ---
ets := tst.Entries()
ets.AssertNumber("A", 2) // Success.
ets.AssertStr("B", "z") // Success.
t.Log(tst.Entries().Summary())
}
With Zap
The zap log message format is supported
by providing logkit.ZapConfig() configuration to the logkit.Tester.
func Test_zap(t *testing.T) {
// --- Given ---
opt := logkit.WithConfig(logkit.ZapConfig()) // Configure logkit.
tst := logkit.New(t, opt) // Initialize logkit.
// Configure Zap.
writer := zapcore.AddSync(tst) // Set the Tester as the destination.
encCfg := zap.NewProductionEncoderConfig()
encCfg.EncodeTime = zapcore.RFC3339TimeEncoder
enc := zapcore.NewJSONEncoder(encCfg)
log := zap.New(zapcore.NewCore(enc, writer, zapcore.InfoLevel))
// --- When ---
// Use the log instance in your application.
log.Info("msg 0", zap.Int("A", 0), zap.String("B", "x"))
log.Warn("msg 1", zap.Int("A", 1), zap.String("B", "y"))
log.Error("msg 2", zap.Int("A", 2), zap.String("B", "z"))
// --- Then ---
ets := tst.Entries()
ets.AssertNumber("A", 2) // Success.
ets.AssertStr("B", "z") // Success.
t.Log(tst.Entries().Summary())
}
With Logrus
The logrus log message format is
supported by providing logkit.LogrusConfig() configuration to the
logkit.Tester.
func Test_Logrus(t *testing.T) {
// --- Given ---
opt := logkit.WithConfig(logkit.LogrusConfig()) // Configure logkit.
tst := logkit.New(t, opt) // Initialize logkit.
// Configure Logrus.
log := logrus.New()
log.SetOutput(tst) // Set the Tester as the destination.
log.SetFormatter(&logrus.JSONFormatter{})
// --- When ---
// Use the log instance in your application.
log.WithField("A", 0).WithField("B", "x").Info("msg 0")
log.WithField("A", 1).WithField("B", "y").Warn("msg 1")
log.WithField("A", 2).WithField("B", "z").Error("msg 2")
// --- Then ---
ets := tst.Entries()
ets.AssertNumber("A", 2) // Success.
ets.AssertStr("B", "z") // Success.
t.Log(tst.Entries().Summary())
}
Assertions
The logkit library provides two primary types for working with log entries:
Entries: A collection of log entries, offering methods to assert fields across all entries.Entry: A single log entry, providing methods to assert individual fields.
Both types offer a suite of assertion methods for testing log entries. Below is a summary of the available assertions:
Entrues.AssertRaw(want ...string) bool
Entrues.AssertLen(want int) bool
Entrues.AssertMsg(want string) bool
Entrues.AssertNoMsg(want string) bool
Entrues.AssertMsgContain(want string) bool
Entrues.AssertNoMsgContain(want string) bool
Entrues.AssertError(want string) bool
Entrues.AssertErrorContain(want string) bool
Entrues.AssertNoError(want string) bool
Entrues.AssertErr(want error) bool
Entrues.AssertNoErr(want error) bool
Entrues.AssertContain(field, want string) bool
Entrues.AssertStr(field, want string) bool
Entrues.AssertNoStr(field, want string) bool
Entrues.AssertNumber(field string, want float64) bool
Entrues.AssertNoNumber(field string, want float64) bool
Entrues.AssertBool(field string, want bool) bool
Entrues.AssertTime(field string, want time.Time) bool
Entrues.AssertNoTime(field string, want time.Time) bool
Entrues.AssertDuration(field string, want time.Duration) bool
Entrues.AssertNoDuration(field string, want time.Duration) bool
Entry.AssertRaw(want string) bool
Entry.AssertExist(field string) bool
Entry.AssertNotExist(field string) bool
Entry.AssertFieldCount(want int) bool
Entry.AssertFieldType(field string, want FieldType) bool
Entry.AssertLevel(want string) bool
Entry.AssertMsg(want string) bool
Entry.AssertMsgErr(want error) bool
Entry.AssertError(want string) bool
Entry.AssertErr(want error) bool
Entry.AssertStr(field, want string) bool
Entry.AssertContain(field, want string) bool
Entry.AssertNumber(field string, want float64) bool
Entry.AssertBool(field string, want bool) bool
Entry.AssertTime(key string, want time.Time) bool
Entry.AssertWithin(field string, want time.Time, diff string) bool
Entry.AssertLoggedWithin(want time.Time, diff string) bool
Entry.AssertDuration(field string, want time.Duration) bool
Entry.AssertMap(field string, want map[string]any) bool
Custom Matchers for Complex Tests
Use Matcher to find a specific log entry with a set of assertions on multiple
fields. Ideal for complex log entries.
func Test_Match(t *testing.T) {
mcr := logkit.NewMatcher(
t,
logkit.DefaultConfig(),
logkit.CheckNumber("A", 1),
logkit.CheckNumber("B", 2),
)
tst := logkit.New(t)
// Example logs.
_, _ = tst.Write([]byte(`{"level": "info", "A": 1, "B": 44, "message": "msg 0"}`))
_, _ = tst.Write([]byte(`{"level": "info", "A": 1, "B": 42, "message": "msg 1"}`))
_, _ = tst.Write([]byte(`{"level": "info", "A": 1, "B": 2, "message": "msg 2"}`))
// Find the first matching entry.
ent := tst.Match(mcr)
fmt.Printf("found: %v\n", ent.String())
// Output:
// found: {"level": "info", "A": 1, "B": 2, "message": "msg 2"}
}
Waiting for Asynchronous Logs
Test logs from goroutines or async processes with WaitFor, which supports
configurable timeouts.
func Test_WaitFor(t *testing.T) {
tst := logkit.New(t)
go func() {
_, _ = tst.Write([]byte(`{"level": "debug", "A": 0}`))
time.Sleep(500 * time.Millisecond)
_, _ = tst.Write([]byte(`{"level": "error", "A": 1}`))
}()
ent := tst.WaitFor("1s", logkit.CheckNumber("A", 1))
fmt.Printf("found: %v\n", ent.String())
// Output:
// found: {"level": "error", "A": 1}
}
Loading Logs from Files
Load and test logs from a file, useful for debugging or validating production logs.
func Test_Load(t *testing.T) {
tst := logkit.Load(t, "testdata/log.log")
fmt.Println(tst.String())
// Output:
// {"level":"info", "str":"abc", "message":"msg0"}
// {"level":"info", "str":"def", "message":"msg1"}
}