Repository: corazawaf/coraza
Pull request #686 (2023-03-03 13:17:58)

chore(readme): remove roadmap from readme
There is no specific roadmap right now


Patch:
diff --git a/README.md b/README.md
index 507c659..2866904 100644
--- a/README.md
+++ b/README.md
@@ -56,14 +56,6 @@ The Coraza Project maintains implementations and plugins for the following serve
 
 * [Coraza GeoIP](https://github.com/corazawaf/coraza-geoip) (preview)
 
-## Roadmap
-
-* WASM scripts support
-* New rule language
-* GraphQL body processor
-* TinyGo support
-* libcoraza C exports
-
 ## Prerequisites
 
 * Golang compiler v1.18+
diff --git a/actions/ctl.go b/actions/ctl.go
index cad033d..2d89f0d 100644
--- a/actions/ctl.go
+++ b/actions/ctl.go
@@ -10,6 +10,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/corazawaf/coraza/v3/debuglogger"
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
 	"github.com/corazawaf/coraza/v3/rules"
@@ -80,7 +81,10 @@ func (a *ctlFn) Evaluate(_ rules.RuleMetadata, txS rules.TransactionState) {
 	case ctlRuleRemoveTargetByID:
 		ran, err := rangeToInts(tx.WAF.Rules.GetRules(), a.value)
 		if err != nil {
-			tx.WAF.Logger.Error("[ctl:RuleRemoveTargetByID] invalid range: %s", err.Error())
+			tx.DebugLogger().Error().
+				Str("ctl", "RuleRemoveTargetByID").
+				Err(err).
+				Msg("Invalid range")
 			return
 		}
 		for _, id := range ran {
@@ -103,7 +107,11 @@ func (a *ctlFn) Evaluate(_ rules.RuleMetadata, txS rules.TransactionState) {
 	case ctlAuditEngine:
 		ae, err := types.ParseAuditEngineStatus(a.value)
 		if err != nil {
-			tx.WAF.Logger.Error("[ctl:AuditEngine] %s", err.Error())
+			tx.DebugLogger().Error().
+				Str("ctl", "AuditEngine").
+				Str("value", a.value).
+				Err(err).
+				Msg("Invalid status")
 			return
 		}
 		tx.AuditEngine = ae
@@ -113,36 +121,57 @@ func (a *ctlFn) Evaluate(_ rules.RuleMetadata, txS rules.TransactionState) {
 	case ctlForceRequestBodyVariable:
 		val, ok := parseOnOff(a.value)
 		if !ok {
-			tx.WAF.Logger.Error("[ctl:ForceRequestBodyVariable] unknown value %q", a.value)
+			tx.DebugLogger().Error().
+				Str("ctl", "ForceRequestBodyVariable").
+				Str("value", a.value).
+				Msg("Unknown toggle")
 			return
 		}
 		tx.ForceRequestBodyVariable = val
-		tx.WAF.Logger.Debug("[ctl:ForceRequestBodyVariable] Forcing request body var with CTL to %s", val)
+		tx.DebugLogger().Debug().
+			Str("ctl", "ForceRequestBodyVariable").
+			Bool("value", val).
+			Msg("Forcing request body var")
 	case ctlRequestBodyAccess:
 		val, ok := parseOnOff(a.value)
 		if !ok {
-			tx.WAF.Logger.Error("[ctl:RequestBodyAccess] unknown value %q", a.value)
+			tx.DebugLogger().Error().
+				Str("ctl", "RequestBodyAccess").
+				Str("value", a.value).
+				Msg("Unknown toggle")
 			return
 		}
 		tx.RequestBodyAccess = val
 	case ctlRequestBodyLimit:
 		limit, err := strconv.ParseInt(a.value, 10, 64)
 		if err != nil {
-			tx.WAF.Logger.Error("[ctl:RequestBodyLimit] Incorrect integer CTL value %q", a.value)
+			tx.DebugLogger().Error().
+				Str("ctl", "RequestBodyLimit").
+				Str("value", a.value).
+				Err(err).
+				Msg("Invalid limit")
 			return
 		}
 		tx.RequestBodyLimit = limit
 	case ctlRuleEngine:
 		re, err := types.ParseRuleEngineStatus(a.value)
 		if err != nil {
-			tx.WAF.Logger.Error("[ctl:RuleEngine] %s", err.Error())
+			tx.DebugLogger().Error().
+				Str("ctl", "RuleEngine").
+				Str("value", a.value).
+				Err(err).
+				Msg("Invalid status")
 			return
 		}
 		tx.RuleEngine = re
 	case ctlRuleRemoveByID:
 		id, err := strconv.Atoi(a.value)
 		if err != nil {
-			tx.WAF.Logger.Error("[ctl:RuleRemoveByID] %s", err.Error())
+			tx.DebugLogger().Error().
+				Str("ctl", "RuleRemoveByID").
+				Str("value", a.value).
+				Err(err).
+				Msg("Invalid rule ID")
 			return
 		}
 		tx.RemoveRuleByID(id)
@@ -170,12 +199,18 @@ func (a *ctlFn) Evaluate(_ rules.RuleMetadata, txS rules.TransactionState) {
 	case ctlHashEnforcement:
 		// Not supported yet
 	case ctlDebugLogLevel:
-		// lvl, _ := strconv.Atoi(a.value)
-		// TODO
-		// We cannot update the log level, it would affect the whole waf instance...
-		// tx.WAF.SetLogLevel(lvl)
-	}
+		lvl, err := strconv.ParseInt(a.value, 10, 8)
+		if err != nil {
+			tx.DebugLogger().Error().
+				Str("ctl", "DebugLogLevel").
+				Str("value", a.value).
+				Err(err).
+				Msg("Invalid log level")
+			return
+		}
 
+		tx.SetDebugLogLevel(debuglogger.LogLevel(lvl))
+	}
 }
 
 func (a *ctlFn) Type() rules.ActionType {
@@ -230,6 +265,8 @@ func parseCtl(data string) (ctlFunctionType, string, variables.RuleVariable, str
 		act = ctlHashEngine
 	case "hashEnforcement":
 		act = ctlHashEnforcement
+	case "debugLogLevel":
+		act = ctlDebugLogLevel
 	default:
 		return ctlUnknown, "", 0x00, "", fmt.Errorf("unknown ctl action %q", action)
 	}
diff --git a/actions/expirevar.go b/actions/expirevar.go
index 8ffd60c..9e8db67 100644
--- a/actions/expirevar.go
+++ b/actions/expirevar.go
@@ -47,7 +47,7 @@ func (a *expirevarFn) Init(_ rules.RuleMetadata, data string) error {
 func (a *expirevarFn) Evaluate(_ rules.RuleMetadata, _ rules.TransactionState) {
 	// Not supported
 	// TODO(jcchavezs): Shall we log a message?
-	// tx.WAF.Logger.Error("Expirevar was used but it's not supported", zap.Int("rule", r.Id))
+	// tx.DebugLogger().Error().Msg("Expirevar was used but it's not supported", zap.Int("rule", r.Id))
 }
 
 func (a *expirevarFn) Type() rules.ActionType {
diff --git a/actions/initcol.go b/actions/initcol.go
index a55d180..a50c547 100644
--- a/actions/initcol.go
+++ b/actions/initcol.go
@@ -29,7 +29,7 @@ func (a *initcolFn) Init(_ rules.RuleMetadata, data string) error {
 }
 
 func (a *initcolFn) Evaluate(_ rules.RuleMetadata, _ rules.TransactionState) {
-	// tx.WAF.Logger.Error("initcol was used but it's not supported", zap.Int("rule", r.Id))
+	// tx.DebugLogger().Error().Msg("initcol was used but it's not supported", zap.Int("rule", r.Id))
 	/*
 		key := tx.MacroExpansion(a.key)
 		data := tx.WAF.Persistence.Get(a.variable, key)
diff --git a/actions/setenv.go b/actions/setenv.go
index 0f9145d..68e3e50 100644
--- a/actions/setenv.go
+++ b/actions/setenv.go
@@ -48,7 +48,11 @@ func (a *setenvFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	v := a.value.Expand(tx)
 	// set env variable
 	if err := os.Setenv(a.key, v); err != nil {
-		tx.DebugLogger().Error("[%s] Error setting env variable for rule %d: %s", tx.ID(), r.ID(), err.Error())
+		tx.DebugLogger().
+			Error().
+			Int("rule_id", r.ID()).
+			Err(err).
+			Msg("Failed to set the env variable for rule")
 	}
 	// TODO is this ok?
 	tx.Variables().Env().Set(a.key, []string{v})
diff --git a/actions/setvar.go b/actions/setvar.go
index 20d6bb6..3d63bad 100644
--- a/actions/setvar.go
+++ b/actions/setvar.go
@@ -58,7 +58,11 @@ func (a *setvarFn) Init(_ rules.RuleMetadata, data string) error {
 func (a *setvarFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	key := a.key.Expand(tx)
 	value := a.value.Expand(tx)
-	tx.DebugLogger().Debug("[%s] Setting var %q to %q by rule %d", tx.ID(), key, value, r.ID())
+	tx.DebugLogger().Debug().
+		Str("var_key", key).
+		Str("var_value", value).
+		Int("rule_id", r.ID()).
+		Msg("Action evaluated")
 	a.evaluateTxCollection(r, tx, strings.ToLower(key), value)
 }
 
@@ -92,7 +96,11 @@ func (a *setvarFn) evaluateTxCollection(r rules.RuleMetadata, tx rules.Transacti
 		if len(value) > 1 {
 			sum, err = strconv.Atoi(value[1:])
 			if err != nil {
-				tx.DebugLogger().Error("[%s] Invalid value for setvar %q on rule %d", tx.ID(), value, r.ID())
+				tx.DebugLogger().Error().
+					Str("var_value", value).
+					Int("rule_id", r.ID()).
+					Err(err).
+					Msg("Invalid value")
 				return
 			}
 		}
@@ -100,7 +108,11 @@ func (a *setvarFn) evaluateTxCollection(r rules.RuleMetadata, tx rules.Transacti
 		if res != "" {
 			val, err = strconv.Atoi(res)
 			if err != nil {
-				tx.DebugLogger().Error("[%s] Invalid value for setvar %q on rule %d", tx.ID(), res, r.ID())
+				tx.DebugLogger().Error().
+					Str("var_key", res).
+					Int("rule_id", r.ID()).
+					Err(err).
+					Msg("Invalid value")
 				return
 			}
 		}
diff --git a/actions/skipafter.go b/actions/skipafter.go
index 613c443..5092cf5 100644
--- a/actions/skipafter.go
+++ b/actions/skipafter.go
@@ -23,7 +23,9 @@ func (a *skipafterFn) Init(_ rules.RuleMetadata, data string) error {
 }
 
 func (a *skipafterFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
-	tx.DebugLogger().Debug("[%s] Starting secmarker %q", tx.ID(), a.data)
+	tx.DebugLogger().Debug().
+		Str("value", a.data).
+		Msg("Starting secmarker")
 	tx.(*corazawaf.Transaction).SkipAfter = a.data
 }
 
diff --git a/config.go b/config.go
index a7e6a7a..9e3a8c1 100644
--- a/config.go
+++ b/config.go
@@ -6,6 +6,7 @@ package coraza
 import (
 	"io/fs"
 
+	"github.com/corazawaf/coraza/v3/debuglogger"
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/loggers"
 	"github.com/corazawaf/coraza/v3/types"
@@ -56,7 +57,7 @@ type WAFConfig interface {
 	WithResponseBodyMimeTypes(mimeTypes []string) WAFConfig
 
 	// WithDebugLogger configures a debug logger.
-	WithDebugLogger(logger loggers.DebugLogger) WAFConfig
+	WithDebugLogger(logger debuglogger.Logger) WAFConfig
 
 	// WithErrorCallback configures an error callback that can be used
 	// to log errors triggered by the WAF.
@@ -107,7 +108,7 @@ type wafConfig struct {
 	responseBodyAccess       bool
 	responseBodyLimit        *int
 	responseBodyMimeTypes    []string
-	debugLogger              loggers.DebugLogger
+	debugLogger              debuglogger.Logger
 	errorCallback            func(rule types.MatchedRule)
 	fsRoot                   fs.FS
 }
@@ -154,7 +155,7 @@ func (c *wafConfig) WithResponseBodyAccess() WAFConfig {
 	return ret
 }
 
-func (c *wafConfig) WithDebugLogger(logger loggers.DebugLogger) WAFConfig {
+func (c *wafConfig) WithDebugLogger(logger debuglogger.Logger) WAFConfig {
 	ret := c.clone()
 	ret.debugLogger = logger
 	return ret
diff --git a/debuglogger/default.go b/debuglogger/default.go
new file mode 100644
index 0000000..18c9628
--- /dev/null
+++ b/debuglogger/default.go
@@ -0,0 +1,192 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package debuglogger
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"strconv"
+)
+
+type defaultEvent struct {
+	level   LogLevel
+	printer Printer
+	fields  []byte
+}
+
+func (e *defaultEvent) Msg(msg string) {
+	if len(msg) == 0 {
+		return
+	}
+
+	if len(e.fields) == 0 {
+		e.printer(e.level, msg, "")
+	} else {
+		// if event has fields, there serialization starts with a
+		// trailing space.
+		e.printer(e.level, msg, string(e.fields[1:]))
+	}
+}
+
+func (e *defaultEvent) Str(key, val string) Event {
+	e.fields = append(e.fields, ' ')
+	e.fields = append(e.fields, key...)
+	e.fields = append(e.fields, '=')
+	e.fields = append(e.fields, strconv.Quote(val)...)
+
+	return e
+}
+
+func (e *defaultEvent) Err(err error) Event {
+	if err == nil {
+		return e
+	}
+
+	e.fields = append(e.fields, " error=\""...)
+	e.fields = append(e.fields, err.Error()...)
+	e.fields = append(e.fields, '"')
+	return e
+}
+
+func (e *defaultEvent) Bool(key string, b bool) Event {
+	e.fields = append(e.fields, ' ')
+	e.fields = append(e.fields, key...)
+	e.fields = append(e.fields, '=')
+	if b {
+		e.fields = append(e.fields, "true"...)
+	} else {
+		e.fields = append(e.fields, "false"...)
+	}
+	return e
+}
+
+func (e *defaultEvent) Int(key string, i int) Event {
+	e.fields = append(e.fields, ' ')
+	e.fields = append(e.fields, key...)
+	e.fields = append(e.fields, '=')
+	e.fields = append(e.fields, strconv.Itoa(i)...)
+	return e
+}
+
+func (e *defaultEvent) Uint(key string, i uint) Event {
+	e.fields = append(e.fields, ' ')
+	e.fields = append(e.fields, key...)
+	e.fields = append(e.fields, '=')
+	e.fields = append(e.fields, strconv.Itoa(int(i))...)
+	return e
+}
+
+func (e *defaultEvent) Stringer(key string, val fmt.Stringer) Event {
+	return e.Str(key, val.String())
+}
+
+func (defaultEvent) IsEnabled() bool {
+	return true
+}
+
+type defaultLogger struct {
+	printer       Printer
+	factory       PrinterFactory
+	level         LogLevel
+	defaultFields []byte
+}
+
+func (l defaultLogger) WithOutput(w io.Writer) Logger {
+	return defaultLogger{
+		printer:       l.factory(w),
+		factory:       l.factory,
+		level:         l.level,
+		defaultFields: l.defaultFields,
+	}
+}
+
+func (l defaultLogger) WithLevel(lvl LogLevel) Logger {
+	return defaultLogger{
+		printer:       l.printer,
+		factory:       l.factory,
+		level:         lvl,
+		defaultFields: l.defaultFields,
+	}
+}
+
+func (l defaultLogger) With(fs ...ContextField) Logger {
+	var e Event = &defaultEvent{}
+	for _, f := range fs {
+		e = f(e)
+	}
+	return defaultLogger{
+		printer:       l.printer,
+		factory:       l.factory,
+		level:         l.level,
+		defaultFields: append(l.defaultFields, e.(*defaultEvent).fields...),
+	}
+}
+
+func (l defaultLogger) Trace() Event {
+	if l.level < LogLevelTrace {
+		return NopEvent{}
+	}
+
+	return &defaultEvent{printer: l.printer, level: LogLevelError, fields: l.defaultFields}
+}
+
+func (l defaultLogger) Debug() Event {
+	if l.level < LogLevelDebug {
+		return NopEvent{}
+	}
+
+	return &defaultEvent{printer: l.printer, level: LogLevelError, fields: l.defaultFields}
+}
+
+func (l defaultLogger) Info() Event {
+	if l.level < LogLevelInfo {
+		return NopEvent{}
+	}
+
+	return &defaultEvent{printer: l.printer, level: LogLevelError, fields: l.defaultFields}
+}
+
+func (l defaultLogger) Warn() Event {
+	if l.level < LogLevelWarn {
+		return NopEvent{}
+	}
+
+	return &defaultEvent{printer: l.printer, level: LogLevelWarn, fields: l.defaultFields}
+}
+
+func (l defaultLogger) Error() Event {
+	if l.level < LogLevelError {
+		return NopEvent{}
+	}
+
+	return &defaultEvent{printer: l.printer, level: LogLevelError, fields: l.defaultFields}
+}
+
+// Default returns a default logger that writes to stderr.
+func Default() Logger {
+	return DefaultWithPrinterFactory(defaultPrinterFactory)
+}
+
+type Printer func(lvl LogLevel, message, fields string)
+
+type PrinterFactory func(w io.Writer) Printer
+
+var defaultPrinterFactory = func(w io.Writer) Printer {
+	l := log.New(w, "", log.LstdFlags)
+	return func(lvl LogLevel, message, fields string) {
+		l.Printf("[%s] %s %s", lvl.String(), message, fields)
+	}
+}
+
+// DefaultWithPrinterFactory returns a default logger that writes to stderr with a given
+// printer factory. It is useful when you need to abstract the printer.
+func DefaultWithPrinterFactory(f PrinterFactory) Logger {
+	return defaultLogger{
+		printer: f(os.Stderr),
+		factory: f,
+		level:   LogLevelInfo,
+	}
+}
diff --git a/loggers/debug.go b/debuglogger/level.go
similarity index 59%
rename from loggers/debug.go
rename to debuglogger/level.go
index b316200..357642f 100644
--- a/loggers/debug.go
+++ b/debuglogger/level.go
@@ -1,33 +1,10 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package loggers
-
-import (
-	"io"
-)
-
-// DebugLogger is used to log SecDebugLog messages
-type DebugLogger interface {
-	// Error logs an error message
-	Error(message string, args ...interface{})
-	// Warn logs a warning message
-	Warn(message string, args ...interface{})
-	// Info logs an info message
-	Info(message string, args ...interface{})
-	// Debug logs a debug message
-	Debug(message string, args ...interface{})
-	// Trace logs a trace message
-	Trace(message string, args ...interface{})
-	// SetLevel sets the log level
-	SetLevel(level LogLevel)
-	// SetOutput sets the output for the logger and closes
-	// the former output if any.
-	SetOutput(w io.WriteCloser)
-}
+package debuglogger
 
 // LogLevel is the type of log level
-type LogLevel int
+type LogLevel int8
 
 const (
 	// LogLevelUnknown is a default value for unknown log level
@@ -67,7 +44,7 @@ func (level LogLevel) String() string {
 	return "UNKNOWN"
 }
 
-// Invalid returns true if the log level is invalid
-func (level LogLevel) Invalid() bool {
-	return level < LogLevelNoLog || level > LogLevelTrace
+// Valid returns true if the log level is valid
+func (level LogLevel) Valid() bool {
+	return level >= LogLevelNoLog && level <= LogLevelTrace
 }
diff --git a/debuglogger/logger.go b/debuglogger/logger.go
new file mode 100644
index 0000000..8863da8
--- /dev/null
+++ b/debuglogger/logger.go
@@ -0,0 +1,100 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package debuglogger
+
+import (
+	"fmt"
+	"io"
+)
+
+// Event represents a log event. It is instanced by one of the level method of
+// Logger and finalized by the Msg  method.
+type Event interface {
+	// Msg sends the Event with msg added as the message field if not empty.
+	Msg(msg string)
+	// Str adds the field key with val as a string to the Event.
+	Str(key, val string) Event
+	// Err adds the field "error" with serialized err to the Event.
+	// If err is nil, no field is added.
+	Err(err error) Event
+	// Bool adds the field key with val as a bool to the Event.
+	Bool(key string, b bool) Event
+	// Int adds the field key with i as a int to the Event.
+	Int(key string, i int) Event
+	// Uint adds the field key with i as a uint to the Event.
+	Uint(key string, i uint) Event
+	// Stringer adds the field key with val.String() (or null if val is nil)
+	// to the Event.
+	Stringer(key string, val fmt.Stringer) Event
+	// IsEnabled returns true if the Event is enabled for the given log level
+	// It is helpful when you want to avoid expensive operations on formatting
+	// the log fields.
+	IsEnabled() bool
+}
+
+type ContextField func(Event) Event
+
+func Str(key, val string) ContextField {
+	return func(e Event) Event {
+		return e.Str(key, val)
+	}
+}
+
+func Bool(key string, b bool) ContextField {
+	return func(e Event) Event {
+		return e.Bool(key, b)
+	}
+}
+
+func Int(key string, i int) ContextField {
+	return func(e Event) Event {
+		return e.Int(key, i)
+	}
+}
+
+func Uint(key string, i uint) ContextField {
+	return func(e Event) Event {
+		return e.Uint(key, i)
+	}
+}
+
+func Stringer(key string, val fmt.Stringer) ContextField {
+	return func(e Event) Event {
+		return e.Stringer(key, val)
+	}
+}
+
+// Logger is used to log SecDebugLog messages
+// This interface is highly inspired in github.com/rs/zerolog logger and the aim
+// is to avoid allocations while logging.
+type Logger interface {
+	// WithOutput duplicates the current logger and sets w as its output.
+	WithOutput(w io.Writer) Logger
+
+	// Level creates a child logger with the minimum accepted level set to level.
+	WithLevel(lvl LogLevel) Logger
+
+	// WithOutput duplicates the current logger and adds context fields to it.
+	With(...ContextField) Logger
+
+	// Trace starts a new message with trace level.
+	// You must call Msg on the returned event in order to send the event.
+	Trace() Event
+
+	// Debug starts a new message with debug level.
+	// You must call Msg on the returned event in order to send the event.
+	Debug() Event
+
+	// Info starts a new message with info level.
+	// You must call Msg on the returned event in order to send the event.
+	Info() Event
+
+	// Warn starts a new message with warn level.
+	// You must call Msg on the returned event in order to send the event.
+	Warn() Event
+
+	// Error starts a new message with error level.
+	// You must call Msg on the returned event in order to send the event.
+	Error() Event
+}
diff --git a/debuglogger/nop.go b/debuglogger/nop.go
new file mode 100644
index 0000000..16f49a7
--- /dev/null
+++ b/debuglogger/nop.go
@@ -0,0 +1,27 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package debuglogger
+
+import (
+	"fmt"
+)
+
+type NopEvent struct{}
+
+func (NopEvent) Msg(string)                            {}
+func (e NopEvent) Str(string, string) Event            { return e }
+func (e NopEvent) Err(error) Event                     { return e }
+func (e NopEvent) Bool(string, bool) Event             { return e }
+func (e NopEvent) Int(string, int) Event               { return e }
+func (e NopEvent) Uint(string, uint) Event             { return e }
+func (e NopEvent) Stringer(string, fmt.Stringer) Event { return e }
+func (e NopEvent) IsEnabled() bool                     { return false }
+
+func Nop() Logger {
+	return defaultLogger{
+		printer: func(LogLevel, string, string) {},
+		factory: defaultPrinterFactory,
+		level:   LogLevelNoLog,
+	}
+}
diff --git a/internal/corazawaf/logger.go b/internal/corazawaf/logger.go
deleted file mode 100644
index e83aca4..0000000
--- a/internal/corazawaf/logger.go
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package corazawaf
-
-import (
-	"fmt"
-	"io"
-	"log"
-
-	"github.com/corazawaf/coraza/v3/loggers"
-)
-
-// DebugLogger is a logger that logs to the standard logger
-type stdDebugLogger struct {
-	io.Closer
-	logger *log.Logger
-	Level  loggers.LogLevel
-}
-
-var _ loggers.DebugLogger = (*stdDebugLogger)(nil)
-
-func (l *stdDebugLogger) formatLog(level loggers.LogLevel, message string, args ...interface{}) {
-	if l.Level >= level {
-		l.logger.Printf("[%s] %s", level.String(), fmt.Sprintf(message, args...))
-	}
-}
-
-// Info logs an info message
-func (l *stdDebugLogger) Info(message string, args ...interface{}) {
-	l.formatLog(loggers.LogLevelInfo, message, args...)
-}
-
-// Warn logs a warning message
-func (l *stdDebugLogger) Warn(message string, args ...interface{}) {
-	l.formatLog(loggers.LogLevelWarn, message, args...)
-}
-
-// Error logs an error message
-func (l *stdDebugLogger) Error(message string, args ...interface{}) {
-	l.formatLog(loggers.LogLevelError, message, args...)
-}
-
-// Debug logs a debug message
-func (l *stdDebugLogger) Debug(message string, args ...interface{}) {
-	l.formatLog(loggers.LogLevelDebug, message, args...)
-}
-
-// Trace logs a trace message
-func (l *stdDebugLogger) Trace(message string, args ...interface{}) {
-	l.formatLog(loggers.LogLevelTrace, message, args...)
-}
-
-// SetLevel sets the log level
-func (l *stdDebugLogger) SetLevel(level loggers.LogLevel) {
-	if level.Invalid() {
-		l.Info("Invalid log level, defaulting to INFO")
-		level = loggers.LogLevelInfo
-	}
-	l.Level = level
-}
-
-// SetOutput sets the output for the logger
-func (l *stdDebugLogger) SetOutput(w io.WriteCloser) {
-	if l.Closer != nil {
-		l.Closer.Close()
-	}
-	l.logger.SetOutput(w)
-	l.Closer = w
-}
diff --git a/internal/corazawaf/rule.go b/internal/corazawaf/rule.go
index efc3e88..3596991 100644
--- a/internal/corazawaf/rule.go
+++ b/internal/corazawaf/rule.go
@@ -176,8 +176,8 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[tran
 
 	var matchedValues []types.MatchData
 	// we log if we are the parent rule
-	tx.WAF.Logger.Debug("[%s] [%d] Evaluating rule %d", tx.id, rid, r.ID_)
-	defer tx.WAF.Logger.Debug("[%s] [%d] Finish evaluating rule %d", tx.id, rid, r.ID_)
+	tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Evaluating rule")
+	defer tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Finish evaluating rule")
 	ruleCol := tx.variables.rule
 	ruleCol.SetIndex("id", 0, strconv.Itoa(rid))
 	if r.Msg != nil {
@@ -190,7 +190,7 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[tran
 	ruleCol.SetIndex("severity", 0, r.Severity_.String())
 	// SecMark and SecAction uses nil operator
 	if r.operator == nil {
-		tx.WAF.Logger.Debug("[%s] [%d] Forcing rule %d to match", tx.id, rid, r.ID_)
+		tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Forcing rule to match")
 		md := &corazarules.MatchData{}
 		matchedValues = append(matchedValues, md)
 		r.matchVariable(tx, md)
@@ -216,14 +216,20 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[tran
 			}
 
 			values = tx.GetField(v)
-			tx.WAF.Logger.Debug("[%s] [%d] Expanding %d arguments for rule %d", tx.id, rid, len(values), r.ID_)
+			tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Expanding arguments for rule")
 			for i, arg := range values {
-				tx.WAF.Logger.Debug("[%s] [%d] Transforming argument %q for rule %d", tx.id, rid, arg.Value(), r.ID_)
+				tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Transforming argument for rule")
 				args, errs := r.transformArg(arg, i, cache)
 				if len(errs) > 0 {
-					tx.WAF.Logger.Debug("[%s] [%d] Error transforming argument %q for rule %d: %v", tx.id, rid, arg.Value(), r.ID_, errs)
+					log := tx.DebugLogger().Debug().Int("rule_id", rid)
+					if log.IsEnabled() {
+						for i, err := range errs {
+							log = log.Str(fmt.Sprintf("errors[%d]", i), err.Error())
+						}
+						log.Msg("Error transforming argument for rule")
+					}
 				}
-				tx.WAF.Logger.Debug("[%s] [%d] Arguments transformed for rule %d: %v", tx.id, rid, r.ID_, args)
+				tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Arguments transformed for rule")
 
 				// args represents the transformed variables
 				for _, carg := range args {
@@ -244,22 +250,19 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[tran
 							mr.Data_ = r.LogData.Expand(tx)
 						}
 						matchedValues = append(matchedValues, mr)
-
-						tx.WAF.Logger.Debug("[%s] [%d] Evaluating operator \"%s %s\" against %q: MATCH",
-							tx.id,
-							rid,
-							r.operator.Function,
-							r.operator.Data,
-							carg,
-						)
+						tx.DebugLogger().Debug().
+							Int("rule_id", rid).
+							Str("operator_function", r.operator.Function).
+							Str("operator_data", r.operator.Data).
+							Str("arg", carg).
+							Msg("Evaluating operator: MATCH")
 					} else {
-						tx.WAF.Logger.Debug("[%s] [%d] Evaluating operator \"%s %s\" against %q: NO MATCH",
-							tx.id,
-							rid,
-							r.operator.Function,
-							r.operator.Data,
-							carg,
-						)
+						tx.DebugLogger().Debug().
+							Int("rule_id", rid).
+							Str("operator_function", r.operator.Function).
+							Str("operator_data", r.operator.Data).
+							Str("arg", carg).
+							Msg("Evaluating operator: NO MATCH")
 					}
 				}
 			}
@@ -274,7 +277,7 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[tran
 	if r.ParentID_ == 0 {
 		// we only run the chains for the parent rule
 		for nr := r.Chain; nr != nil; {
-			tx.WAF.Logger.Debug("[%s] [%d] Evaluating rule chain for %d", tx.id, rid, r.ID_)
+			tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Evaluating rule chain")
 			matchedChainValues := nr.doEvaluate(phase, tx, cache)
 			if len(matchedChainValues) == 0 {
 				return matchedChainValues
@@ -284,10 +287,10 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[tran
 		}
 		// we need to add disruptive actions in the end, otherwise they would be triggered without their chains.
 		if tx.RuleEngine != types.RuleEngineDetectionOnly {
-			tx.WAF.Logger.Debug("[%s] [%d] Disrupting transaction by rule %d", tx.id, rid, r.ID_)
+			tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Disrupting transaction by rule")
 			for _, a := range r.actions {
 				if a.Function.Type() == rules.ActionTypeDisruptive || a.Function.Type() == rules.ActionTypeFlow {
-					tx.WAF.Logger.Debug("[%s] [%d] Evaluating action %s for rule %d", tx.id, rid, a.Name, r.ID_)
+					tx.DebugLogger().Debug().Int("rule_id", rid).Str("action", a.Name).Msg("Evaluating action for rule")
 					a.Function.Evaluate(r, tx)
 				}
 			}
@@ -346,7 +349,11 @@ func (r *Rule) matchVariable(tx *Transaction, m *corazarules.MatchData) {
 		rid = r.ParentID_
 	}
 	if m.Variable() != variables.Unknown {
-		tx.WAF.Logger.Debug("[%s] [%d] Matching rule %d %s:%s", tx.id, rid, r.ID_, m.Variable().Name(), m.Key())
+		tx.DebugLogger().Debug().
+			Int("rule_id", rid).
+			Str("variable_name", m.Variable().Name()).
+			Str("key", m.Key()).
+			Msg("Matching rule")
 	}
 	// we must match the vars before running the chains
 
@@ -354,7 +361,7 @@ func (r *Rule) matchVariable(tx *Transaction, m *corazarules.MatchData) {
 	tx.matchVariable(m)
 	for _, a := range r.actions {
 		if a.Function.Type() == rules.ActionTypeNondisruptive {
-			tx.WAF.Logger.Debug("[%s] [%d] Evaluating action %s for rule %d", tx.id, rid, a.Name, r.ID_)
+			tx.DebugLogger().Debug().Str("action", a.Name).Msg("Evaluating action")
 			a.Function.Evaluate(r, tx)
 		}
 	}
diff --git a/internal/corazawaf/rulegroup.go b/internal/corazawaf/rulegroup.go
index 7439bd6..09a8e64 100644
--- a/internal/corazawaf/rulegroup.go
+++ b/internal/corazawaf/rulegroup.go
@@ -112,7 +112,10 @@ func (rg *RuleGroup) Clear() {
 // as soon as an interruption has been triggered.
 // Returns true if transaction is disrupted
 func (rg *RuleGroup) Eval(phase types.RulePhase, tx *Transaction) bool {
-	tx.WAF.Logger.Debug("[%s] Evaluating phase %d", tx.id, int(phase))
+	tx.DebugLogger().Debug().
+		Int("phase", int(phase)).
+		Msg("Evaluating phase")
+
 	tx.LastPhase = phase
 	usedRules := 0
 	ts := time.Now().UnixNano()
@@ -138,7 +141,10 @@ RulesLoop:
 		// we skip the rule in case it's in the excluded list
 		for _, trb := range tx.ruleRemoveByID {
 			if trb == r.ID_ {
-				tx.WAF.Logger.Debug("[%s] Skipping rule %d", tx.id, r.ID_)
+				tx.DebugLogger().Debug().
+					Int("rule_id", r.ID_).
+					Msg("Skipping rule")
+
 				continue RulesLoop
 			}
 		}
@@ -148,7 +154,11 @@ RulesLoop:
 			if r.SecMark_ == tx.SkipAfter {
 				tx.SkipAfter = ""
 			} else {
-				tx.WAF.Logger.Debug("[%s] Skipping rule %d because of SkipAfter, expecting %s and got: %q", tx.id, r.ID_, tx.SkipAfter, r.SecMark_)
+				tx.DebugLogger().Debug().
+					Int("rule_id", r.ID_).
+					Str("skip_after", tx.SkipAfter).
+					Str("secmarker", r.SecMark_).
+					Msg("Skipping rule because of SkipAfter")
 			}
 			continue
 		}
@@ -177,7 +187,10 @@ RulesLoop:
 		tx.Capture = false // we reset captures
 		usedRules++
 	}
-	tx.WAF.Logger.Debug("[%s] Finished phase %d", tx.id, int(phase))
+	tx.DebugLogger().Debug().
+		Int("phase", int(phase)).
+		Msg("Finished phase")
+
 	tx.stopWatches[phase] = time.Now().UnixNano() - ts
 	return tx.interruption != nil
 }
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index 104ec2e..1cb10c1 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -18,6 +18,7 @@ import (
 
 	"github.com/corazawaf/coraza/v3/bodyprocessors"
 	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/debuglogger"
 	"github.com/corazawaf/coraza/v3/internal/collections"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	corazatypes "github.com/corazawaf/coraza/v3/internal/corazatypes"
@@ -103,6 +104,8 @@ type Transaction struct {
 	// Contains a WAF instance for the current transaction
 	WAF *WAF
 
+	debugLogger debuglogger.Logger
+
 	// Timestamp of the request
 	Timestamp int64
 
@@ -282,8 +285,12 @@ func (tx *Transaction) Interrupt(interruption *types.Interruption) {
 	}
 }
 
-func (tx *Transaction) DebugLogger() loggers.DebugLogger {
-	return tx.WAF.Logger
+func (tx *Transaction) DebugLogger() debuglogger.Logger {
+	return tx.debugLogger
+}
+
+func (tx *Transaction) SetDebugLogLevel(lvl debuglogger.LogLevel) {
+	tx.debugLogger = tx.debugLogger.WithLevel(lvl)
 }
 
 func (tx *Transaction) ResponseBodyReader() (io.Reader, error) {
@@ -350,7 +357,10 @@ func (tx *Transaction) Capturing() bool {
 // that supports capture, like @rx
 func (tx *Transaction) CaptureField(index int, value string) {
 	if tx.Capture {
-		tx.WAF.Logger.Debug("[%s] Capturing field %d with value %q", tx.id, index, value)
+		tx.debugLogger.Debug().
+			Int("field", index).
+			Str("value", value).
+			Msg("Capturing field")
 		i := strconv.Itoa(index)
 		tx.variables.tx.SetIndex(i, 0, value)
 	}
@@ -358,7 +368,8 @@ func (tx *Transaction) CaptureField(index int, value string) {
 
 // this function is used to control which variables are reset after a new rule is evaluated
 func (tx *Transaction) resetCaptures() {
-	tx.WAF.Logger.Debug("[%s] Reseting captured variables", tx.id)
+	tx.debugLogger.Debug().
+		Msg("Reseting captured variables")
 	// We reset capture 0-9
 	ctx := tx.variables.tx
 	// RUNE 48 = 0
@@ -455,7 +466,7 @@ func (tx *Transaction) matchVariable(match *corazarules.MatchData) {
 
 // MatchRule Matches a rule to be logged
 func (tx *Transaction) MatchRule(r *Rule, mds []types.MatchData) {
-	tx.WAF.Logger.Debug("[%s] rule %d matched", tx.id, r.ID_)
+	tx.debugLogger.Debug().Int("rule_id", r.ID_).Msg("Rule matched")
 	// tx.MatchedRules = append(tx.MatchedRules, mr)
 
 	// If the rule is set to audit, we log the transaction to the audit log
@@ -709,7 +720,7 @@ func (tx *Transaction) ProcessURI(uri string, method string, httpVersion string)
 // It is expected to be executed before calling ProcessRequestHeaders.
 func (tx *Transaction) SetServerName(serverName string) {
 	if tx.LastPhase >= types.PhaseRequestHeaders {
-		tx.WAF.Logger.Warn("SetServerName has been called after ProcessRequestHeaders")
+		tx.debugLogger.Warn().Msg("SetServerName has been called after ProcessRequestHeaders")
 	}
 	tx.variables.serverName.Set(serverName)
 }
@@ -727,12 +738,12 @@ func (tx *Transaction) ProcessRequestHeaders() *types.Interruption {
 	}
 	if tx.LastPhase >= types.PhaseRequestHeaders {
 		// Phase already evaluated
-		tx.WAF.Logger.Error("ProcessRequestHeaders has already been called")
+		tx.debugLogger.Error().Msg("ProcessRequestHeaders has already been called")
 		return tx.interruption
 	}
 
 	if tx.interruption != nil {
-		tx.WAF.Logger.Error("Calling ProcessRequestHeaders but there is a preexisting interruption")
+		tx.debugLogger.Error().Msg("Calling ProcessRequestHeaders but there is a preexisting interruption")
 		return tx.interruption
 	}
 
@@ -741,7 +752,7 @@ func (tx *Transaction) ProcessRequestHeaders() *types.Interruption {
 }
 
 func setAndReturnBodyLimitInterruption(tx *Transaction) (*types.Interruption, int, error) {
-	tx.DebugLogger().Warn("Disrupting transaction with body size above the configured limit (Action Reject)")
+	tx.debugLogger.Warn().Msg("Disrupting transaction with body size above the configured limit (Action Reject)")
 	tx.interruption = &types.Interruption{
 		Status: 413,
 		Action: "deny",
@@ -803,7 +814,7 @@ func (tx *Transaction) WriteRequestBody(b []byte) (*types.Interruption, int, err
 	}
 
 	if runProcessRequestBody {
-		tx.DebugLogger().Warn("Processing request body whose size reached the configured limit (Action ProcessPartial)")
+		tx.debugLogger.Warn().Msg("Processing request body whose size reached the configured limit (Action ProcessPartial)")
 		_, err = tx.ProcessRequestBody()
 	}
 	return tx.interruption, int(w), err
@@ -848,7 +859,7 @@ func (tx *Transaction) ReadRequestBodyFrom(r io.Reader) (*types.Interruption, in
 		if tx.requestBodyBuffer.length >= (math.MaxInt64 - writingBytes) {
 			// Overflow, failing. MaxInt64 is not a realistic payload size. Furthermore, it has been tested that
 			// bytes.Buffer does not work with this kind of sizes. See comments in BodyBuffer Write(data []byte)
-			return nil, 0, errors.New("Overflow reached while writing request body")
+			return nil, 0, errors.New("overflow reached while writing request body")
 		}
 		if tx.requestBodyBuffer.length+writingBytes >= tx.RequestBodyLimit {
 			tx.variables.inboundErrorData.Set("1")
@@ -882,7 +893,7 @@ func (tx *Transaction) ReadRequestBodyFrom(r io.Reader) (*types.Interruption, in
 
 	err = nil
 	if runProcessRequestBody {
-		tx.DebugLogger().Warn("Processing request body whose size reached the configured limit (Action ProcessPartial)")
+		tx.debugLogger.Warn().Msg("Processing request body whose size reached the configured limit (Action ProcessPartial)")
 		_, err = tx.ProcessRequestBody()
 	}
 	return tx.interruption, int(w), err
@@ -902,12 +913,12 @@ func (tx *Transaction) ProcessRequestBody() (*types.Interruption, error) {
 
 	if tx.LastPhase >= types.PhaseRequestBody {
 		// Phase already evaluated
-		tx.WAF.Logger.Warn("ProcessRequestBody has already been called")
+		tx.debugLogger.Warn().Msg("ProcessRequestBody has already been called")
 		return tx.interruption, nil
 	}
 
 	if tx.interruption != nil {
-		tx.WAF.Logger.Error("Calling ProcessRequestBody but there is a preexisting interruption")
+		tx.debugLogger.Error().Msg("Calling ProcessRequestBody but there is a preexisting interruption")
 		return tx.interruption, nil
 	}
 
@@ -935,7 +946,9 @@ func (tx *Transaction) ProcessRequestBody() (*types.Interruption, error) {
 		rbp = "URLENCODED"
 		tx.variables.reqbodyProcessor.Set(rbp)
 	}
-	tx.WAF.Logger.Debug("[%s] Attempting to process request body using %q", tx.id, rbp)
+	tx.debugLogger.Debug().
+		Str("body_processor", rbp).
+		Msg("Attempting to process request body using")
 	rbp = strings.ToLower(rbp)
 	if rbp == "" {
 		// so there is no bodyprocessor, we don't want to generate an error
@@ -974,12 +987,12 @@ func (tx *Transaction) ProcessResponseHeaders(code int, proto string) *types.Int
 
 	if tx.LastPhase >= types.PhaseResponseHeaders {
 		// Phase already evaluated
-		tx.WAF.Logger.Error("ProcessResponseHeaders has already been called")
+		tx.debugLogger.Error().Msg("ProcessResponseHeaders has already been called")
 		return tx.interruption
 	}
 
 	if tx.interruption != nil {
-		tx.WAF.Logger.Error("Calling ProcessResponseHeaders but there is a preexisting interruption")
+		tx.debugLogger.Error().Msg("Calling ProcessResponseHeaders but there is a preexisting interruption")
 		return tx.interruption
 	}
 
@@ -1134,21 +1147,24 @@ func (tx *Transaction) ProcessResponseBody() (*types.Interruption, error) {
 
 	if tx.LastPhase >= types.PhaseResponseBody {
 		// Phase already evaluated
-		tx.WAF.Logger.Warn("ProcessResponseBody has already been called")
+		tx.debugLogger.Warn().Msg("ProcessResponseBody has already been called")
 		return tx.interruption, nil
 	}
 
 	if tx.interruption != nil {
-		tx.WAF.Logger.Error("Calling ProcessResponseBody but there is a preexisting interruption")
+		tx.debugLogger.Error().Msg("Calling ProcessResponseBody but there is a preexisting interruption")
 		return tx.interruption, nil
 	}
 
 	if !tx.ResponseBodyAccess || !tx.IsResponseBodyProcessable() {
-		tx.WAF.Logger.Debug("[%s] Skipping response body processing (Access: %t)", tx.id, tx.ResponseBodyAccess)
+		tx.debugLogger.Debug().
+			Bool("response_body_access", tx.ResponseBodyAccess).
+			Msg("Skipping response body processing")
 		tx.WAF.Rules.Eval(types.PhaseResponseBody, tx)
 		return tx.interruption, nil
 	}
-	tx.WAF.Logger.Debug("[%s] Attempting to process response body", tx.id)
+	tx.debugLogger.Debug().
+		Msg("Attempting to process response body")
 	reader, err := tx.responseBodyBuffer.Reader()
 	if err != nil {
 		return tx.interruption, err
@@ -1180,13 +1196,15 @@ func (tx *Transaction) ProcessLogging() {
 
 	if tx.AuditEngine == types.AuditEngineOff {
 		// Audit engine disabled
-		tx.WAF.Logger.Debug("[%s] Transaction not marked for audit logging, AuditEngine is disabled", tx.id)
+		tx.debugLogger.Debug().
+			Msg("Transaction not marked for audit logging, AuditEngine is disabled")
 		return
 	}
 
 	if tx.AuditEngine == types.AuditEngineRelevantOnly && !tx.audit {
 		// Transaction marked not for audit logging
-		tx.WAF.Logger.Debug("[%s] Transaction not marked for audit logging, AuditEngine is RelevantOnly and we got noauditlog", tx.id)
+		tx.debugLogger.Debug().
+			Msg("Transaction not marked for audit logging, AuditEngine is RelevantOnly and we got noauditlog")
 		return
 	}
 
@@ -1195,16 +1213,21 @@ func (tx *Transaction) ProcessLogging() {
 		status := tx.variables.responseStatus.Get()
 		if re != nil && !re.Match([]byte(status)) {
 			// Not relevant status
-			tx.WAF.Logger.Debug("[%s] Transaction status not marked for audit logging", tx.id)
+			tx.debugLogger.Debug().
+				Msg("Transaction status not marked for audit logging")
 			return
 		}
 	}
 
-	tx.WAF.Logger.Debug("[%s] Transaction marked for audit logging", tx.id)
+	tx.debugLogger.Debug().
+		Msg("Transaction marked for audit logging")
+
 	if writer := tx.WAF.AuditLogWriter; writer != nil {
 		// We don't log if there is an empty audit logger
 		if err := writer.Write(tx.AuditLog()); err != nil {
-			tx.WAF.Logger.Error(err.Error())
+			tx.debugLogger.Error().
+				Err(err).
+				Msg("Failed to write audit log")
 		}
 	}
 }
@@ -1352,7 +1375,9 @@ func (tx *Transaction) Close() error {
 		errs = append(errs, err)
 	}
 
-	tx.WAF.Logger.Debug("[%s] Transaction finished, disrupted: %t", tx.id, tx.IsInterrupted())
+	tx.debugLogger.Debug().
+		Bool("is_interrupted", tx.IsInterrupted()).
+		Msg("Transaction finished")
 
 	switch {
 	case len(errs) == 0:
diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go
index 637f6d9..31eb955 100644
--- a/internal/corazawaf/waf.go
+++ b/internal/corazawaf/waf.go
@@ -8,15 +8,15 @@ import (
 	"fmt"
 	"io"
 	"io/fs"
-	"log"
 	"os"
 	"regexp"
 	"strconv"
 	"strings"
 	"time"
 
+	"github.com/corazawaf/coraza/v3/debuglogger"
 	"github.com/corazawaf/coraza/v3/internal/environment"
-	ioutils "github.com/corazawaf/coraza/v3/internal/io"
+
 	stringutils "github.com/corazawaf/coraza/v3/internal/strings"
 	"github.com/corazawaf/coraza/v3/internal/sync"
 	"github.com/corazawaf/coraza/v3/loggers"
@@ -117,7 +117,7 @@ type WAF struct {
 	ProducerConnectorVersion string
 
 	// Used for the debug logger
-	Logger loggers.DebugLogger
+	Logger debuglogger.Logger
 
 	ErrorLogCb func(rule types.MatchedRule)
 
@@ -133,7 +133,7 @@ func (w *WAF) NewTransaction() *Transaction {
 func (w *WAF) NewTransactionWithID(id string) *Transaction {
 	if len(strings.TrimSpace(id)) == 0 {
 		id = stringutils.RandomString(19)
-		w.Logger.Warn("Empty ID passed for new transaction")
+		w.Logger.Warn().Msg("Empty ID passed for new transaction")
 	}
 	return w.newTransactionWithID(id)
 }
@@ -166,6 +166,7 @@ func (w *WAF) newTransactionWithID(id string) *Transaction {
 	tx.Capture = false
 	tx.stopWatches = map[types.RulePhase]int64{}
 	tx.WAF = w
+	tx.debugLogger = w.Logger.With(debuglogger.Str("tx_id", tx.id))
 	tx.Timestamp = time.Now().UnixNano()
 	tx.audit = false
 
@@ -214,37 +215,37 @@ func (w *WAF) newTransactionWithID(id string) *Transaction {
 	tx.variables.highestSeverity.Set("0")
 	tx.variables.uniqueID.Set(tx.id)
 
-	w.Logger.Debug("New transaction created with id %q", tx.id)
+	w.Logger.Debug().Msg("New transaction created")
 
 	return tx
 }
 
-// SetDebugLogPath sets the path for the debug log
-// If the path is empty, the debug log will be disabled
-// note: this is not thread safe
-func (w *WAF) SetDebugLogPath(path string) error {
+func resolveLogPath(path string) (io.Writer, error) {
 	if path == "" {
-		w.Logger.SetOutput(ioutils.NopCloser(io.Discard))
-		return nil
+		return io.Discard, nil
 	}
 
 	if path == "/dev/stdout" {
-		w.Logger.SetOutput(os.Stdout)
-		return nil
+		return os.Stdout, nil
 	}
 
 	if path == "/dev/stderr" {
-		w.Logger.SetOutput(os.Stderr)
-		return nil
+		return os.Stderr, nil
 	}
 
-	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+	return os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+}
+
+// SetDebugLogPath sets the path for the debug log
+// If the path is empty, the debug log will be disabled
+// note: this is not thread safe
+func (w *WAF) SetDebugLogPath(path string) error {
+	o, err := resolveLogPath(path)
 	if err != nil {
-		w.Logger.Error("failed to open the debug log file: %s", err.Error())
+		return err
 	}
 
-	w.Logger.SetOutput(f)
-
+	w.SetDebugLogOutput(o)
 	return nil
 }
 
@@ -252,14 +253,13 @@ const _1gb = 1073741824
 
 // NewWAF creates a new WAF instance with default variables
 func NewWAF() *WAF {
-	logger := &stdDebugLogger{
-		logger: &log.Logger{},
-		Level:  loggers.LogLevelInfo,
-	}
+	logger := debuglogger.Nop()
 
 	logWriter, err := loggers.GetLogWriter("serial")
 	if err != nil {
-		logger.Error("error creating serial log writer: %s", err.Error())
+		logger.Error().
+			Err(err).
+			Msg("error creating serial log writer")
 	}
 
 	waf := &WAF{
@@ -279,17 +279,21 @@ func NewWAF() *WAF {
 		waf.TmpDir = os.TempDir()
 	}
 
-	if err := waf.SetDebugLogPath(""); err != nil {
-		fmt.Println(err)
-	}
-	waf.Logger.Debug("a new WAF instance was created")
+	waf.Logger.Debug().Msg("A new WAF instance was created")
 	return waf
 }
 
+func (w *WAF) SetDebugLogOutput(wr io.Writer) {
+	w.Logger = w.Logger.WithOutput(wr)
+}
+
 // SetDebugLogLevel changes the debug level of the WAF instance
-func (w *WAF) SetDebugLogLevel(lvl int) error {
-	// setLevel is concurrent safe
-	w.Logger.SetLevel(loggers.LogLevel(lvl))
+func (w *WAF) SetDebugLogLevel(lvl debuglogger.LogLevel) error {
+	if !lvl.Valid() {
+		return errors.New("invalid log level")
+	}
+
+	w.Logger = w.Logger.WithLevel(lvl)
 	return nil
 }
 
diff --git a/internal/seclang/directives.go b/internal/seclang/directives.go
index 5ea310b..95c2950 100644
--- a/internal/seclang/directives.go
+++ b/internal/seclang/directives.go
@@ -13,6 +13,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/corazawaf/coraza/v3/debuglogger"
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
 	"github.com/corazawaf/coraza/v3/loggers"
@@ -107,7 +108,7 @@ func directiveSecMarker(options *DirectiveOptions) error {
 	if err := options.WAF.Rules.Add(rule); err != nil {
 		return err
 	}
-	options.WAF.Logger.Debug("added secmark rule")
+	options.WAF.Logger.Debug().Msg("Added secmark rule")
 	return nil
 }
 
@@ -139,7 +140,9 @@ func directiveSecAction(options *DirectiveOptions) error {
 	if err := options.WAF.Rules.Add(rule); err != nil {
 		return err
 	}
-	options.WAF.Logger.Debug("Added SecAction: %s", options.Opts)
+	options.WAF.Logger.Debug().
+		Str("actions", options.Opts).
+		Msg("Added SecAction")
 	return nil
 }
 
@@ -174,14 +177,20 @@ func directiveSecRule(options *DirectiveOptions) error {
 	if err != nil && !ignoreErrors {
 		return err
 	} else if err != nil && ignoreErrors {
-		options.WAF.Logger.Debug("Ignoring rule compilation error for rule %s: %s", options.Opts, err.Error())
+		options.WAF.Logger.Debug().
+			Str("rule_id", options.Opts).
+			Err(err).
+			Msg("Ignoring rule compilation error")
 		return nil
 	}
 	err = options.WAF.Rules.Add(rule)
 	if err != nil && !ignoreErrors {
 		return err
 	} else if err != nil && ignoreErrors {
-		options.WAF.Logger.Debug("Ignoring rule compilation error for rule %s: %s", options.Opts, err.Error())
+		options.WAF.Logger.Debug().
+			Str("rule_id", options.Opts).
+			Err(err).
+			Msg("Ignoring rule compilation error")
 		return nil
 	}
 	return nil
@@ -896,11 +905,11 @@ func directiveSecDebugLog(options *DirectiveOptions) error {
 //
 // Levels outside the 0-9 range will default to level 3 (Info)
 func directiveSecDebugLogLevel(options *DirectiveOptions) error {
-	lvl, err := strconv.Atoi(options.Opts)
+	lvl, err := strconv.ParseInt(options.Opts, 10, 8)
 	if err != nil {
 		return err
 	}
-	return options.WAF.SetDebugLogLevel(lvl)
+	return options.WAF.SetDebugLogLevel(debuglogger.LogLevel(lvl))
 }
 
 func directiveSecRuleUpdateTargetByID(options *DirectiveOptions) error {
@@ -927,8 +936,9 @@ func directiveSecIgnoreRuleCompilationErrors(options *DirectiveOptions) error {
 		return err
 	}
 	if b {
-		options.WAF.Logger.Warn(`Coraza is running in Compatibility Mode (SecIgnoreRuleCompilationErrors On)
-		, which may cause unexpected behavior on faulty rules.`)
+		options.WAF.Logger.Warn().
+			Msg(`Running in Compatibility Mode (SecIgnoreRuleCompilationErrors On), 
+			which may cause unexpected behavior on faulty rules.`)
 	}
 	options.Config.Set("ignore_rule_compilation_errors", b)
 	return nil
@@ -944,7 +954,9 @@ func directiveSecDataset(options *DirectiveOptions) error {
 		return errors.New("syntax error: SecDataset name `\n...\n`")
 	}
 	if _, ok := options.Datasets[name]; ok {
-		options.WAF.Logger.Warn("Dataset %s already exists, overwriting", name)
+		options.WAF.Logger.Warn().
+			Str("dataset_name", name).
+			Msg("Dataset already exists, overwriting")
 	}
 	var arr []string
 	data := strings.Trim(d, "`")
diff --git a/internal/seclang/parser.go b/internal/seclang/parser.go
index 32a210f..dcd664b 100644
--- a/internal/seclang/parser.go
+++ b/internal/seclang/parser.go
@@ -62,7 +62,6 @@ func (p *Parser) FromFile(profilePath string) error {
 			// we don't use defer for this as tinygo does not seem to like it
 			p.currentDir = originalDir
 			p.currentFile = ""
-			p.options.WAF.Logger.Error(err.Error())
 			return fmt.Errorf("failed to readfile: %s", err.Error())
 		}
 
@@ -71,7 +70,6 @@ func (p *Parser) FromFile(profilePath string) error {
 			// we don't use defer for this as tinygo does not seem to like it
 			p.currentDir = originalDir
 			p.currentFile = ""
-			p.options.WAF.Logger.Error(err.Error())
 			return fmt.Errorf("failed to parse string: %s", err.Error())
 		}
 		// restore the lastDir post processing all includes
@@ -135,15 +133,15 @@ func (p *Parser) FromString(data string) error {
 	return nil
 }
 
-func (p *Parser) evaluateLine(data string) error {
-	if data == "" || data[0] == '#' {
+func (p *Parser) evaluateLine(l string) error {
+	if l == "" || l[0] == '#' {
 		// at this point we should not receive an empty line or a commented line
 		return errors.New("invalid line")
 	}
 	// first we get the directive
-	dir, opts, _ := strings.Cut(data, " ")
+	dir, opts, _ := strings.Cut(l, " ")
 
-	p.options.WAF.Logger.Debug("parsing directive %q", data)
+	p.options.WAF.Logger.Debug().Str("line", l).Msg("Parsing directive")
 	directive := strings.ToLower(dir)
 
 	if len(opts) >= 3 && opts[0] == '"' && opts[len(opts)-1] == '"' {
@@ -186,8 +184,7 @@ func (p *Parser) evaluateLine(data string) error {
 }
 
 func (p *Parser) log(msg string) error {
-	msg = fmt.Sprintf("[Parser] [Line %d] %s", p.currentLine, msg)
-	p.options.WAF.Logger.Error("[%d] %s", p.currentLine, msg)
+	p.options.WAF.Logger.Error().Int("line", p.currentLine).Msg(msg)
 	return errors.New(msg)
 }
 
diff --git a/rules/transaction.go b/rules/transaction.go
index f8cecd1..04c470e 100644
--- a/rules/transaction.go
+++ b/rules/transaction.go
@@ -7,7 +7,7 @@ import (
 	"io"
 
 	"github.com/corazawaf/coraza/v3/collection"
-	"github.com/corazawaf/coraza/v3/loggers"
+	"github.com/corazawaf/coraza/v3/debuglogger"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
@@ -35,7 +35,7 @@ type TransactionState interface {
 	ReadResponseBodyFrom(io.Reader) (*types.Interruption, int, error)
 
 	// DebugLogger returns the logger for this transaction.
-	DebugLogger() loggers.DebugLogger
+	DebugLogger() debuglogger.Logger
 
 	// Capturing returns whether the transaction is capturing. CaptureField only works if capturing, this can be used
 	// as an optimization to avoid processing specific to capturing fields.
diff --git a/types/transaction.go b/types/transaction.go
index 11c634a..e927884 100644
--- a/types/transaction.go
+++ b/types/transaction.go
@@ -5,6 +5,8 @@ package types
 
 import (
 	"io"
+
+	"github.com/corazawaf/coraza/v3/debuglogger"
 )
 
 // Transaction is created from a WAF instance to handle web requests and responses,
@@ -186,6 +188,9 @@ type Transaction interface {
 	// MatchedRules returns the rules that have matched the requests with associated information.
 	MatchedRules() []MatchedRule
 
+	// DebugLogger returns the debug logger for this transaction.
+	DebugLogger() debuglogger.Logger
+
 	// ID returns the transaction ID.
 	ID() string
 


Test patch:
diff --git a/actions/ctl_test.go b/actions/ctl_test.go
index f699977..c2d711c 100644
--- a/actions/ctl_test.go
+++ b/actions/ctl_test.go
@@ -4,9 +4,11 @@
 package actions
 
 import (
+	"bytes"
 	"strings"
 	"testing"
 
+	"github.com/corazawaf/coraza/v3/debuglogger"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/types"
@@ -16,11 +18,19 @@ import (
 func TestCtl(t *testing.T) {
 	tests := []struct {
 		input string
-		check func(t *testing.T, tx *corazawaf.Transaction)
+		check func(t *testing.T, logEntry string, tx *corazawaf.Transaction)
 	}{
 		{
 			input: "ruleRemoveTargetById=123",
 		},
+		{
+			input: "ruleRemoveTargetById=A",
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
+				if wantToContain, have := "Invalid range", logEntry; !strings.Contains(have, wantToContain) {
+					t.Errorf("unexpected log entry: want to contain %q, have %q", wantToContain, logEntry)
+				}
+			},
+		},
 		{
 			input: "ruleRemoveTargetByTag=tag1",
 		},
@@ -29,39 +39,75 @@ func TestCtl(t *testing.T) {
 		},
 		{
 			input: "auditEngine=Off",
-			check: func(t *testing.T, tx *corazawaf.Transaction) {
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
 				if tx.AuditEngine != types.AuditEngineOff {
-					t.Error("Failed to disable audit log")
+					t.Error("Failed to set auditEngine")
+				}
+			},
+		},
+		{
+			input: "auditEngine=In",
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
+				if tx.AuditEngine != types.AuditEngineOn {
+					t.Error("Failed to set auditEngine")
+				}
+
+				if wantToContain, have := "invalid audit engine status: In", logEntry; !strings.Contains(have, wantToContain) {
+					t.Errorf("unexpected log entry: want to contain %q, have %q", wantToContain, logEntry)
 				}
 			},
 		},
 		{
 			input: "auditLogParts=A",
-			check: func(t *testing.T, tx *corazawaf.Transaction) {
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
 				if want, have := types.AuditLogPartAuditLogHeader, tx.AuditLogParts[0]; want != have {
-					t.Errorf("Failed to set audit log parts, want %s, have %s", string(want), string(have))
+					t.Errorf("Failed to set auditLogParts, want %s, have %s", string(want), string(have))
 				}
 			},
 		},
 		{
 			input: "forceRequestBodyVariable=On",
-			check: func(t *testing.T, tx *corazawaf.Transaction) {
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
 				if want, have := true, tx.ForceRequestBodyVariable; want != have {
 					t.Errorf("Failed to set forceRequestBodyVariable, want %t, have %t", want, have)
 				}
 			},
 		},
+		{
+			input: "forceRequestBodyVariable=In",
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
+				if tx.ForceRequestBodyVariable != false {
+					t.Error("Failed to set forceRequestBodyVariable")
+				}
+
+				if wantToContain, have := "Unknown toggle", logEntry; !strings.Contains(have, wantToContain) {
+					t.Errorf("unexpected log entry: want to contain %q, have %q", wantToContain, logEntry)
+				}
+			},
+		},
 		{
 			input: "requestBodyAccess=Off",
-			check: func(t *testing.T, tx *corazawaf.Transaction) {
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
 				if want, have := false, tx.RequestBodyAccess; want != have {
 					t.Errorf("Failed to set requestBodyAccess, want %t, have %t", want, have)
 				}
 			},
 		},
+		{
+			input: "requestBodyAccess=In",
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
+				if tx.RequestBodyAccess != false {
+					t.Error("Failed to set requestBodyAccess")
+				}
+
+				if wantToContain, have := "Unknown toggle", logEntry; !strings.Contains(have, wantToContain) {
+					t.Errorf("unexpected log entry: want to contain %q, have %q", wantToContain, logEntry)
+				}
+			},
+		},
 		{
 			input: "requestBodyLimit=12345",
-			check: func(t *testing.T, tx *corazawaf.Transaction) {
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
 				if tx.RequestBodyLimit != 12345 {
 					t.Error("Failed to set request body limit")
 				}
@@ -69,7 +115,7 @@ func TestCtl(t *testing.T) {
 		},
 		{
 			input: "ruleEngine=Off",
-			check: func(t *testing.T, tx *corazawaf.Transaction) {
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
 				if tx.RuleEngine != types.RuleEngineOff {
 					t.Errorf("Failed to disable rule engine, got %s", tx.RuleEngine.String())
 				}
@@ -86,18 +132,31 @@ func TestCtl(t *testing.T) {
 		},
 		{
 			input: "requestBodyProcessor=XML",
-			check: func(t *testing.T, tx *corazawaf.Transaction) {
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
 				if want, have := tx.Variables().RequestBodyProcessor().Get(), "XML"; want != have {
 					t.Errorf("failed to set requestBodyProcessor, want %s, have %s", want, have)
 				}
 			},
 		},
+		{
+			input: "debugLogLevel=1",
+		},
+		{
+			input: "debugLogLevel=A",
+			check: func(t *testing.T, logEntry string, tx *corazawaf.Transaction) {
+				if wantToContain, have := "Invalid log level", logEntry; !strings.Contains(have, wantToContain) {
+					t.Errorf("unexpected log entry: want to contain %q, have %q", wantToContain, logEntry)
+				}
+			},
+		},
 	}
 
 	for _, test := range tests {
 		testName, _, _ := strings.Cut(test.input, "=")
 		t.Run(testName, func(t *testing.T) {
+			logs := &bytes.Buffer{}
 			waf := corazawaf.NewWAF()
+			waf.Logger = debuglogger.Default().WithOutput(logs)
 			r := corazawaf.NewRule()
 			err := waf.Rules.Add(r)
 			if err != nil {
@@ -119,7 +178,7 @@ func TestCtl(t *testing.T) {
 				// https://github.com/tinygo-org/tinygo/blob/release/src/testing/testing.go#L246
 				return
 			} else {
-				test.check(t, tx)
+				test.check(t, logs.String(), tx)
 			}
 		})
 	}
diff --git a/debuglogger/default_test.go b/debuglogger/default_test.go
new file mode 100644
index 0000000..acc259f
--- /dev/null
+++ b/debuglogger/default_test.go
@@ -0,0 +1,159 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package debuglogger
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"testing"
+)
+
+func TestLoggerLogLevels(t *testing.T) {
+	testCases := map[string]struct {
+		logFunction                func(Logger) func() Event
+		expectedLowestPrintedLevel int
+	}{
+		"Trace": {
+			logFunction:                func(l Logger) func() Event { return l.Trace },
+			expectedLowestPrintedLevel: 9,
+		},
+		"Debug": {
+			logFunction:                func(l Logger) func() Event { return l.Debug },
+			expectedLowestPrintedLevel: 4,
+		},
+		"Info": {
+			logFunction:                func(l Logger) func() Event { return l.Info },
+			expectedLowestPrintedLevel: 3,
+		},
+		"Warn": {
+			logFunction:                func(l Logger) func() Event { return l.Warn },
+			expectedLowestPrintedLevel: 2,
+		},
+		"Error": {
+			logFunction:                func(l Logger) func() Event { return l.Error },
+			expectedLowestPrintedLevel: 1,
+		},
+	}
+
+	for name, tCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			buf := bytes.Buffer{}
+			Default().WithOutput(&buf)
+			for settedLevel := 0; settedLevel <= 9; settedLevel++ {
+				l := Default().WithOutput(io.Discard).WithLevel(LogLevel(settedLevel))
+				event := tCase.logFunction(l)()
+				if settedLevel >= tCase.expectedLowestPrintedLevel {
+					if _, ok := event.(NopEvent); ok {
+						t.Fatalf("Missing expected log. Level: %s, Function: %s", LogLevel(settedLevel).String(), name)
+					}
+
+					if !event.IsEnabled() {
+						t.Fatalf("Unexpected event, wanted to be enabled")
+					}
+				}
+				if settedLevel < tCase.expectedLowestPrintedLevel {
+					if _, ok := event.(NopEvent); !ok {
+						t.Fatalf("Unexpected log. Level: %d, Function: %s", settedLevel, name)
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestMsg(t *testing.T) {
+	t.Run("empty error", func(t *testing.T) {
+		l := Default().WithOutput(io.Discard).WithLevel(LogLevelInfo)
+		fields := l.Info().Err(nil).(*defaultEvent).fields
+		if want, have := 0, len(fields); want != have {
+			t.Fatalf("unexpected number of fields, want %d, have %d", want, have)
+		}
+	})
+
+	t.Run("empty message", func(t *testing.T) {
+		buf := bytes.Buffer{}
+		l := Default().WithOutput(&buf).WithLevel(LogLevelInfo)
+		l.Info().Msg("")
+		if want, have := 0, buf.Len(); want != have {
+			t.Fatalf("unexpected message length, want %d, have %d", want, have)
+		}
+	})
+
+	t.Run("message", func(t *testing.T) {
+		buf := bytes.Buffer{}
+		l := Default().WithOutput(&buf).WithLevel(LogLevelInfo)
+		l.Info().
+			Bool("a", true).
+			Int("b", -1).
+			Uint("c", 1).
+			Str("d", "x").
+			Stringer("e", bytes.NewBufferString("y & z")).
+			Bool("f", false).
+			Err(errors.New("my error")).
+			Msg("my message")
+
+		expected := "[ERROR] my message a=true b=-1 c=1 d=\"x\" e=\"y & z\" f=false error=\"my error\"\n"
+
+		// [20:] Skips the timestamp.
+		if want, have := expected, buf.String()[20:]; want != have {
+			t.Fatalf("unexpected message, want %q, have %q", want, have)
+		}
+	})
+}
+
+func TestWithLogger(t *testing.T) {
+	buf := bytes.Buffer{}
+	l := Default().WithOutput(&buf).WithLevel(LogLevelInfo)
+	l2 := l.
+		With(
+			Bool("a", true),
+			Int("b", -1),
+			Uint("c", 1),
+			Str("d", "x"),
+			Stringer("e", bytes.NewBufferString("y & z")),
+			Bool("f", false),
+		)
+	l2.Info().
+		Str("g", "w").
+		Msg("my message")
+
+	expected := "[ERROR] my message a=true b=-1 c=1 d=\"x\" e=\"y & z\" f=false g=\"w\"\n"
+
+	// [20:] Skips the timestamp.
+	if want, have := expected, buf.String()[20:]; want != have {
+		t.Fatalf("unexpected message, want %q, have %q", want, have)
+	}
+
+	// Check that the original log hasn't been modified.
+	buf2 := bytes.Buffer{}
+	l = l.WithOutput(&buf2)
+	l.Info().
+		Str("g", "w").
+		Msg("my message")
+
+	expected = "[ERROR] my message g=\"w\"\n"
+
+	// [20:] Skips the timestamp.
+	if want, have := expected, buf2.String()[20:]; want != have {
+		t.Fatalf("unexpected message, want %q, have %q", want, have)
+	}
+}
+
+func TestWithLoggerAccumulative(t *testing.T) {
+	buf := bytes.Buffer{}
+	l := Default().WithOutput(&buf).WithLevel(LogLevelInfo)
+	l2 := l.With(Bool("a", true))
+	l3 := l2.With(Int("b", -1))
+	l3.Info().
+		Str("g", "w").
+		Msg("my message")
+
+	expected := "[ERROR] my message a=true b=-1 g=\"w\"\n"
+
+	// [20:] Skips the timestamp.
+	if want, have := expected, buf.String()[20:]; want != have {
+		t.Fatalf("unexpected message, want %q, have %q", want, have)
+	}
+}
diff --git a/debuglogger/level_test.go b/debuglogger/level_test.go
new file mode 100644
index 0000000..2b259ad
--- /dev/null
+++ b/debuglogger/level_test.go
@@ -0,0 +1,57 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package debuglogger
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestLevelString(t *testing.T) {
+	var tests = []struct {
+		level LogLevel
+		want  string
+	}{
+		{LogLevelNoLog, "NOLOG"},
+		{LogLevelError, "ERROR"},
+		{LogLevelWarn, "WARN"},
+		{LogLevelInfo, "INFO"},
+		{LogLevelDebug, "DEBUG"},
+		{LogLevelTrace, "TRACE"},
+		{LogLevelUnknown, "UNKNOWN"},
+		{LogLevel(11), "UNKNOWN"},
+	}
+
+	for _, test := range tests {
+		t.Run(fmt.Sprintf("level %d", test.level), func(t *testing.T) {
+			if want, have := test.want, test.level.String(); want != have {
+				t.Errorf("unexpected error string, want %q, have %q", want, have)
+			}
+		})
+	}
+}
+
+func TestLevelValid(t *testing.T) {
+	var tests = []struct {
+		level   LogLevel
+		isValid bool
+	}{
+		{LogLevelUnknown, false},
+		{LogLevelNoLog, true},
+		{LogLevelError, true},
+		{LogLevelWarn, true},
+		{LogLevelInfo, true},
+		{LogLevelDebug, true},
+		{LogLevelTrace, true},
+		{LogLevel(11), false},
+	}
+
+	for _, test := range tests {
+		t.Run(fmt.Sprintf("level %d", test.level), func(t *testing.T) {
+			if want, have := test.isValid, test.level.Valid(); want != have {
+				t.Errorf("unexpected validity, want %t, have %t", want, have)
+			}
+		})
+	}
+}
diff --git a/debuglogger/nop_test.go b/debuglogger/nop_test.go
new file mode 100644
index 0000000..0a32770
--- /dev/null
+++ b/debuglogger/nop_test.go
@@ -0,0 +1,21 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package debuglogger
+
+import (
+	"io"
+	"testing"
+)
+
+func TestNop(t *testing.T) {
+	l := Nop().(defaultLogger)
+	if want, have := l.level, LogLevelNoLog; want != have {
+		t.Fatalf("unexpected log level when nop")
+	}
+
+	lwo := l.WithOutput(io.Discard).(defaultLogger)
+	if lwo.factory == nil {
+		t.Fatalf("unexpected logger factory")
+	}
+}
diff --git a/http/middleware_test.go b/http/middleware_test.go
index 03e6216..ed1bb8a 100644
--- a/http/middleware_test.go
+++ b/http/middleware_test.go
@@ -21,9 +21,9 @@ import (
 	"testing"
 
 	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/debuglogger"
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/internal/seclang"
-	"github.com/corazawaf/coraza/v3/loggers"
 	"github.com/corazawaf/coraza/v3/macro"
 	"github.com/corazawaf/coraza/v3/types"
 )
@@ -194,26 +194,13 @@ func errLogger(t *testing.T) func(rule types.MatchedRule) {
 	}
 }
 
-type debugLogger struct {
+type testLogOutput struct {
 	t *testing.T
 }
 
-func (l debugLogger) Info(message string, args ...interface{}) { l.t.Logf(message, args...) }
-
-func (l debugLogger) Warn(message string, args ...interface{}) { l.t.Logf(message, args...) }
-
-func (l debugLogger) Error(message string, args ...interface{}) { l.t.Logf(message, args...) }
-
-func (l debugLogger) Debug(message string, args ...interface{}) { l.t.Logf(message, args...) }
-
-func (l debugLogger) Trace(message string, args ...interface{}) { l.t.Logf(message, args...) }
-
-func (l debugLogger) SetLevel(level loggers.LogLevel) {
-	l.t.Logf("Setting level to %q", level.String())
-}
-
-func (l debugLogger) SetOutput(w io.WriteCloser) {
-	l.t.Log("ignoring SecDebugLog directive, debug logs are always routed to proxy logs")
+func (l testLogOutput) Write(p []byte) (int, error) {
+	l.t.Log(string(p))
+	return len(p), nil
 }
 
 type httpTest struct {
@@ -306,6 +293,10 @@ func TestHttpServer(t *testing.T) {
 		},
 	}
 
+	logger := debuglogger.Default().
+		WithOutput(testLogOutput{t}).
+		WithLevel(debuglogger.LogLevelInfo)
+
 	// Perform tests
 	for name, tCase := range tests {
 		t.Run(name, func(t *testing.T) {
@@ -326,7 +317,7 @@ func TestHttpServer(t *testing.T) {
 	SecRule RESPONSE_HEADERS:Foo "@pm bar" "id:199,phase:3,deny,t:lowercase,deny, status:401,msg:'Invalid response header',log,auditlog"
 	SecRule RESPONSE_BODY "@contains password" "id:200, phase:4,deny, status:403,msg:'Invalid response body',log,auditlog"
 	SecRule REQUEST_URI "/allow_me" "id:9,phase:1,allow,msg:'ALLOWED'"
-`).WithErrorCallback(errLogger(t)).WithDebugLogger(&debugLogger{t: t})
+`).WithErrorCallback(errLogger(t)).WithDebugLogger(logger)
 			if l := tCase.reqBodyLimit; l > 0 {
 				conf = conf.WithRequestBodyAccess().WithRequestBodyLimit(l).WithRequestBodyInMemoryLimit(l)
 			}
@@ -364,6 +355,10 @@ func TestHttpServerWithRuleEngineOff(t *testing.T) {
 			expectedRespBody: "Waf is Off!",
 		},
 	}
+	logger := debuglogger.Default().
+		WithOutput(testLogOutput{t}).
+		WithLevel(debuglogger.LogLevelInfo)
+
 	// Perform tests
 	for name, tCase := range tests {
 		t.Run(name, func(t *testing.T) {
@@ -373,7 +368,7 @@ func TestHttpServerWithRuleEngineOff(t *testing.T) {
 			SecRequestBodyAccess On
 			SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog"
 			SecRule REQUEST_BODY "@contains eval" "id:100, phase:2,deny, status:403,msg:'Invalid request body',log,auditlog"
-			`).WithErrorCallback(errLogger(t)).WithDebugLogger(&debugLogger{t: t}))
+			`).WithErrorCallback(errLogger(t)).WithDebugLogger(logger))
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/corazawaf/logger_test.go b/internal/corazawaf/logger_test.go
deleted file mode 100644
index c1578f6..0000000
--- a/internal/corazawaf/logger_test.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package corazawaf
-
-import (
-	"testing"
-
-	"github.com/corazawaf/coraza/v3/loggers"
-)
-
-func TestLoggerLogLevels(t *testing.T) {
-	waf := NewWAF()
-	testCases := map[string]struct {
-		logFunction                func(message string, args ...interface{})
-		expectedLowestPrintedLevel int
-	}{
-		"Trace": {
-			logFunction:                waf.Logger.Trace,
-			expectedLowestPrintedLevel: 9,
-		},
-		"Debug": {
-			logFunction:                waf.Logger.Debug,
-			expectedLowestPrintedLevel: 4,
-		},
-		"Info": {
-			logFunction:                waf.Logger.Info,
-			expectedLowestPrintedLevel: 3,
-		},
-		"Warn": {
-			logFunction:                waf.Logger.Warn,
-			expectedLowestPrintedLevel: 2,
-		},
-		"Error": {
-			logFunction:                waf.Logger.Error,
-			expectedLowestPrintedLevel: 1,
-		},
-	}
-
-	for name, tCase := range testCases {
-		t.Run(name, func(t *testing.T) {
-			for settedLevel := 0; settedLevel <= 9; settedLevel++ {
-				l := &inspectableLogger{}
-				waf.Logger.SetOutput(l)
-				waf.Logger.SetLevel(loggers.LogLevel(settedLevel))
-				tCase.logFunction("this is a log")
-
-				if settedLevel >= tCase.expectedLowestPrintedLevel && len(l.entries) != 1 {
-					t.Fatalf("Missing expected log. Level: %d, Function: %s", settedLevel, name)
-				}
-				if settedLevel < tCase.expectedLowestPrintedLevel && len(l.entries) == 1 {
-					t.Fatalf("Unexpected log. Level: %d, Function: %s", settedLevel, name)
-				}
-			}
-
-		})
-	}
-}
-
-func TestLoggerLevelDefaultsToInfo(t *testing.T) {
-	waf := NewWAF()
-	waf.Logger.SetLevel(loggers.LogLevel(10))
-	if waf.Logger.(*stdDebugLogger).Level != loggers.LogLevelInfo {
-		t.Fatalf("Unexpected log level: %d. It should default to Info (%s)", waf.Logger.(*stdDebugLogger).Level, loggers.LogLevelInfo)
-	}
-}
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index de5339f..53c9a89 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -13,10 +13,10 @@ import (
 	"testing"
 
 	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/debuglogger"
 	"github.com/corazawaf/coraza/v3/internal/collections"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
-	"github.com/corazawaf/coraza/v3/loggers"
 	"github.com/corazawaf/coraza/v3/macro"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
@@ -958,11 +958,12 @@ func TestTxProcessConnection(t *testing.T) {
 }
 
 func TestTxSetServerName(t *testing.T) {
-
 	l := &inspectableLogger{}
+
 	waf := NewWAF()
-	waf.Logger.SetOutput(l)
-	waf.Logger.SetLevel(loggers.LogLevelWarn)
+	waf.SetDebugLogOutput(l)
+	_ = waf.SetDebugLogLevel(debuglogger.LogLevelWarn)
+
 	tx := waf.NewTransaction()
 	tx.LastPhase = types.PhaseRequestHeaders
 	tx.SetServerName("coraza.io")
@@ -1186,8 +1187,8 @@ func TestProcessorsIdempotency(t *testing.T) {
 	l := &inspectableLogger{}
 
 	waf := NewWAF()
-	waf.Logger.SetOutput(l)
-	waf.Logger.SetLevel(loggers.LogLevelError)
+	waf.SetDebugLogOutput(l)
+	_ = waf.SetDebugLogLevel(debuglogger.LogLevelError)
 
 	expectedInterruption := &types.Interruption{
 		RuleID: 123,
@@ -1234,10 +1235,10 @@ func TestProcessorsIdempotency(t *testing.T) {
 				t.Fatalf("unexpected number of log entries, want %d, have %d", want, have)
 			}
 
-			expectedMessage := fmt.Sprintf("[ERROR] Calling %s but there is a preexisting interruption\n", processor)
+			expectedMessage := fmt.Sprintf("Calling %s but there is a preexisting interruption", processor)
 
-			if want, have := expectedMessage, l.entries[0]; want != have {
-				t.Fatalf("unexpected message, want %q, have %q", want, have)
+			if want, have := expectedMessage, l.entries[0]; !strings.Contains(have, want) {
+				t.Fatalf("unexpected message, want to contain %q in %q", want, have)
 			}
 
 			l.Close()
diff --git a/internal/corazawaf/waf_test.go b/internal/corazawaf/waf_test.go
index a98e0c0..d4194c9 100644
--- a/internal/corazawaf/waf_test.go
+++ b/internal/corazawaf/waf_test.go
@@ -39,34 +39,25 @@ func TestNewTransaction(t *testing.T) {
 }
 
 func TestSetDebugLogPath(t *testing.T) {
-	waf := NewWAF()
-
-	testCases := []struct {
-		path   string
-		writer io.Writer
+	tests := map[string]struct {
+		path string
+		w    io.Writer
 	}{
-		{
-			path:   "/dev/stdout",
-			writer: os.Stdout,
-		},
-		{
-			path:   "/dev/stderr",
-			writer: os.Stderr,
-		},
+		"empty path": {path: "", w: io.Discard},
+		"stdout":     {path: "/dev/stdout", w: os.Stdout},
+		"stderr":     {path: "/dev/stderr", w: os.Stderr},
 	}
 
-	for _, tCase := range testCases {
-		t.Run(tCase.path, func(t *testing.T) {
-			err := waf.SetDebugLogPath(tCase.path)
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			w, err := resolveLogPath(test.path)
 			if err != nil {
-				t.Fatalf("unexpected error: %v", err)
+				t.Errorf("unexpected error: %s", err.Error())
 			}
 
-			l := waf.Logger.(*stdDebugLogger)
-			if want, have := tCase.writer, l.logger.Writer(); want != have {
-				t.Error("unexpected logger writer")
+			if w != test.w {
+				t.Errorf("expected io.Discard, got %T", w)
 			}
-			_ = waf.SetDebugLogPath("")
 		})
 	}
 }
@@ -122,7 +113,6 @@ func TestValidate(t *testing.T) {
 					t.Fatalf("unexpected error: %s", err.Error())
 				}
 			}
-
 		})
 	}
 }
diff --git a/internal/seclang/directives_log_test.go b/internal/seclang/directives_log_test.go
index 2dd8736..437f162 100644
--- a/internal/seclang/directives_log_test.go
+++ b/internal/seclang/directives_log_test.go
@@ -83,7 +83,7 @@ func TestDebugDirectives(t *testing.T) {
 	}); err != nil {
 		t.Error(err)
 	}
-	p.options.WAF.Logger.Info("abc123")
+	p.options.WAF.Logger.Info().Msg("abc123")
 	data, err := os.ReadFile(tmp)
 	if err != nil {
 		t.Error(err)
diff --git a/internal/seclang/rule_parser_test.go b/internal/seclang/rule_parser_test.go
index 2445ea7..39eef63 100644
--- a/internal/seclang/rule_parser_test.go
+++ b/internal/seclang/rule_parser_test.go
@@ -126,18 +126,6 @@ func TestSecRuleUpdateTargetVariableNegation(t *testing.T) {
 	}
 }
 
-func TestErrorLine(t *testing.T) {
-	waf := corazawaf.NewWAF()
-	p := NewParser(waf)
-	err := p.FromString("SecAction \"id:1\"\n#test\nSomefaulty")
-	if err == nil {
-		t.Error("expected error")
-	}
-	if !strings.Contains(err.Error(), "Line 3") {
-		t.Errorf("failed to find error line, got %s", err.Error())
-	}
-}
-
 func TestDefaultActionsForPhase2(t *testing.T) {
 	waf := corazawaf.NewWAF()
 	p := NewParser(waf)