Repository: corazawaf/coraza
Pull request #682 (2023-03-02 09:16:43)

Rework Logger API
This PR follows this PR https://github.com/manojgop/coraza/pull/3 and conflates some of the efforts from @manojgop, @ns-sundar and me around reducing allocations in logging in the context of the @intel's research https://owasp.slack.com/archives/C02BXH135AT/p1677616370248079

codecov[bot]: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Patch coverage: **`75.62`**% and project coverage change: **`-0.25`** :warning:
> Comparison is base [(`fb4a3eb`)](https://codecov.io/gh/corazawaf/coraza/commit/fb4a3eb185a6d751f86834d4a4e5801224fdeeb5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) 80.50% compared to head [(`0f8ae9a`)](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) 80.25%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           v3/dev     #682      +/-   ##
==========================================
- Coverage   80.50%   80.25%   -0.25%     
==========================================
  Files         145      146       +1     
  Lines        7771     7948     +177     
==========================================
+ Hits         6256     6379     +123     
- Misses       1284     1337      +53     
- Partials      231      232       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| default | `76.35% <75.31%> (-0.16%)` | :arrow_down: |
| examples | `26.95% <37.50%> (-0.09%)` | :arrow_down: |
| ftw | `50.68% <29.68%> (-0.75%)` | :arrow_down: |
| ftw-multiphase | `50.77% <29.68%> (-0.75%)` | :arrow_down: |
| tinygo | `75.31% <72.81%> (-0.19%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | Coverage Δ | |
|---|---|---|
| [actions/expirevar.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9leHBpcmV2YXIuZ28=) | `67.85% <0.00%> (ø)` | |
| [actions/setenv.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9zZXRlbnYuZ28=) | `37.83% <0.00%> (-5.92%)` | :arrow_down: |
| [actions/skipafter.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9za2lwYWZ0ZXIuZ28=) | `76.92% <ø> (-1.65%)` | :arrow_down: |
| [actions/ctl.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9jdGwuZ28=) | `69.68% <11.11%> (-8.56%)` | :arrow_down: |
| [internal/seclang/directives.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvc2VjbGFuZy9kaXJlY3RpdmVzLmdv) | `70.91% <65.00%> (-0.40%)` | :arrow_down: |
| [actions/setvar.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9zZXR2YXIuZ28=) | `58.76% <66.66%> (+1.44%)` | :arrow_up: |
| [internal/corazawaf/waf.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL3dhZi5nbw==) | `92.71% <72.00%> (-1.33%)` | :arrow_down: |
| [internal/seclang/parser.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvc2VjbGFuZy9wYXJzZXIuZ28=) | `92.36% <80.00%> (-0.18%)` | :arrow_down: |
| [debuglogger/nop.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-ZGVidWdsb2dnZXIvbm9wLmdv) | `83.33% <83.33%> (ø)` | |
| [internal/corazawaf/transaction.go](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL3RyYW5zYWN0aW9uLmdv) | `78.72% <83.33%> (+0.13%)` | :arrow_up: |
| ... and [8 more](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/corazawaf/coraza/pull/682?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).

jcchavezs: PTAL @anuraaga @ns-sundar @manojgop 
jcchavezs: @anuraaga @ns-sundar @manojgop you can see the new logger API in action here: https://github.com/corazawaf/coraza-proxy-wasm/pull/160
jcchavezs: Logger is immutable by design, mainly because that allow us to pass logger
among layers with enriched context (for example, the logger we create at
WAF then is passed down to the transaction with the tx_id, if we do mutate
the context then all logs in the transaction will carry a rule ID which is
incorrect. I think in this case we can roll back the change and don't
create a new logger, just pass the rule_id every time inside the method.

On Sat, 4 Mar 2023, 05:49 manojgop, ***@***.***> wrote:

> ***@***.**** commented on this pull request.
> ------------------------------
>
> In internal/corazawaf/rule.go
> <https://github.com/corazawaf/coraza/pull/682#discussion_r1125342872>:
>
> > @@ -174,10 +175,12 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[tran
>  		rid = r.ParentID_
>  	}
>
> +	logger := tx.DebugLogger().With(debuglogger.Int("rule_id", rid))
>
> Creating logger instance here is resulting in additional heap allocations
> and is impacting performance. Can we just update the context with new
> "rule_id" instead of creating new logger instance
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/corazawaf/coraza/pull/682#pullrequestreview-1324939283>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAXOYAVBNAP67LKBYJ6OTWDW2LCUTANCNFSM6AAAAAAVNDNZGI>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

manojgop: > Logger is immutable by design, mainly because that allow us to pass logger among layers with enriched context (for example, the logger we create at WAF then is passed down to the transaction with the tx_id, if we do mutate the context then all logs in the transaction will carry a rule ID which is incorrect. I think in this case we can roll back the change and don't create a new logger, just pass the rule_id every time inside the method.
> […](#)
> On Sat, 4 Mar 2023, 05:49 manojgop, ***@***.***> wrote: ***@***.**** commented on this pull request. ------------------------------ In internal/corazawaf/rule.go <[#682 (comment)](https://github.com/corazawaf/coraza/pull/682#discussion_r1125342872)>: > @@ -174,10 +175,12 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[tran rid = r.ParentID_ } + logger := tx.DebugLogger().With(debuglogger.Int("rule_id", rid)) Creating logger instance here is resulting in additional heap allocations and is impacting performance. Can we just update the context with new "rule_id" instead of creating new logger instance — Reply to this email directly, view it on GitHub <[#682 (review)](https://github.com/corazawaf/coraza/pull/682#pullrequestreview-1324939283)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/AAXOYAVBNAP67LKBYJ6OTWDW2LCUTANCNFSM6AAAAAAVNDNZGI> . You are receiving this because you authored the thread.Message ID: ***@***.***>

In that case , would be better to rollback this change for rule id. 
jcchavezs: @manojgop please do re-check and let me know if you are happy in terms of allocations so I can finally merge this PR.
manojgop: > @manojgop please do re-check and let me know if you are happy in terms of allocations so I can finally merge this PR.

Performance looks good now after rollback. Just FYI - In my system creating the new logger instance resulted in 30% reduction in performance when I ran testing/coreruleset bench mark. I think it might be good to add some perf tests as part of GitHub Action CI, so that it'll be easy to capture performance regression (in a different PR)
manojgop: > > @manojgop please do re-check and let me know if you are happy in terms of allocations so I can finally merge this PR.
> 
> Performance looks good now after rollback. Just FYI - In my system creating the new logger instance resulted in 30% reduction in performance when I ran testing/coreruleset bench mark. I think it might be good to add some perf tests as part of GitHub Action CI, so that it'll be easy to capture performance regression (in a different PR)

**After roll back**
![image](https://user-images.githubusercontent.com/40452026/222877842-aafb5ced-26b0-4b78-bd16-0f182408d7e8.png)

**Before Rollback (creating new logger instance for every rule id)**
![image](https://user-images.githubusercontent.com/40452026/222877885-c0482825-eb50-4457-88a2-01a62ba4d8c8.png)

![image](https://user-images.githubusercontent.com/40452026/222877913-e9ad595c-de0c-4762-8a76-b9142b91347d.png)

![image](https://user-images.githubusercontent.com/40452026/222877949-5bbdc705-2860-4714-af9f-56c4f91184de.png)




jcchavezs: > I think it might be good to add some perf tests as part of GitHub Action
CI, so that it'll be easy to capture performance regression (in a different
PR)

@manojgop I think we have that but maybe not working properly
https://github.com/corazawaf/coraza/blob/v3/dev/.github/workflows/benchmark.yml#L36

Also I think it mixes results among PRs, anyways would you be up to
introduce this checks on CI?

On Sat, 4 Mar 2023, 06:37 manojgop, ***@***.***> wrote:

> ***@***.**** approved this pull request.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/corazawaf/coraza/pull/682#pullrequestreview-1324943836>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAXOYAXINKUELQ2LDJORAKLW2LIH3ANCNFSM6AAAAAAVNDNZGI>
> .
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>


Patch:
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)