Skip to content

[RAP-435] feat: add structured JSON logger middleware#1

Open
ajit216 wants to merge 1 commit into
masterfrom
feature/rap-435-json-logger-middleware
Open

[RAP-435] feat: add structured JSON logger middleware#1
ajit216 wants to merge 1 commit into
masterfrom
feature/rap-435-json-logger-middleware

Conversation

@ajit216
Copy link
Copy Markdown
Owner

@ajit216 ajit216 commented Apr 7, 2026

Summary

  • Adds JSONLogger() and JSONLoggerWithConfig() as production-ready companions to the built-in Logger() middleware
  • Each request emits a single JSON line: timestamp, status, latency_ms, method, path, client_ip, body_bytes, errors
  • Supports SkipPaths to suppress noisy health/metrics endpoints
  • Supports custom Skip func(*Context) bool for dynamic skip logic
  • Supports MaskHeaders to redact sensitive header values from error output
  • 7 unit tests covering all config options and concurrent safety

Motivation

Gin's built-in Logger() outputs colored text — unsuitable for production log aggregators (Datadog, ELK, CloudWatch). Teams currently roll their own JSON loggers inconsistently. This provides a standard, configurable solution that follows existing gin middleware patterns.

Jira

RAP-435 — Structured JSON logger middleware for gin

Test plan

  • go build ./... passes
  • go test -run TestJSONLogger -v — all 7 tests pass
  • Verify each request produces exactly one JSON line
  • Verify SkipPaths suppresses output for matched paths
  • Verify MaskHeaders redacts header values in error messages
  • Run with -race flag to check for data races

🤖 Generated with Claude Code


Summary by cubic

Add structured JSON logger middleware for gin, a production-ready companion to Logger(), that logs one JSON line per request with timestamp, status, latency_ms, method, path, client_ip, body_bytes, and errors. Addresses RAP-435.

  • New Features
    • JSONLogger() and JSONLoggerWithConfig(JSONLoggerConfig) with configurable Output.
    • Skipping via SkipPaths and Skip func(*Context) bool.
    • Redaction via MaskHeaders to hide sensitive header values.
    • 7 unit tests cover config options and concurrency.

Written for commit 1817deb. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added structured JSON logging middleware for HTTP requests, capturing timestamp, status code, latency, method, path, client IP, and response size
    • Support for skipping logs on specified paths
    • Header value masking to redact sensitive information in logs
    • Configurable output destination and error handling
  • Tests

    • Added comprehensive test coverage for logging functionality, including concurrent request handling

Introduces JSONLogger() and JSONLoggerWithConfig() as production-ready
companions to the built-in Logger() middleware. Each request emits a
single JSON line with timestamp, status, latency_ms, method, path,
client_ip, body_bytes and errors — suitable for Datadog, ELK and
CloudWatch ingestion.

Supports SkipPaths, custom Skip func, and MaskHeaders for redacting
sensitive header values from error output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

A new Gin middleware for structured JSON request logging is introduced, capturing request details including timestamp, HTTP status, latency, method, path, client IP, response size, and errors. Configuration options allow customizing output destination, skipping specific paths or conditionally, and masking sensitive header values.

Changes

Cohort / File(s) Summary
JSON Logger Implementation
jsonlogger.go
Adds JSONLogEntry struct with timestamp, status, latency, method, path, client IP, body bytes, and errors fields. Introduces JSONLoggerConfig with output writer, skip paths, skip function, and header masking options. Implements JSONLogger() and JSONLoggerWithConfig() middleware handlers that normalize configuration, extract error messages after request processing, redact masked headers, and emit single-line JSON logs.
JSON Logger Tests
jsonlogger_test.go
Comprehensive test suite covering JSON field validation, path-based log skipping, conditional skip function behavior, header masking redaction, 5xx status logging, query string inclusion, and concurrent request handling via mutex-protected buffer implementation.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A logger born of JSON dreams,
Captures requests, logs, and schemes,
With masks and skips to guard thy way,
Per-request tales in structured lay! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a structured JSON logger middleware for Gin, which is the primary focus of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/rap-435-json-logger-middleware

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
jsonlogger_test.go (1)

130-149: Concurrent test should assert output integrity, not just completion.

Right now Line 130-Line 149 verifies goroutines finished but does not verify that 50 valid JSON lines were actually written.

Suggested enhancement
 import (
 	"bytes"
 	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
+	"strings"
 	"sync"
 	"testing"
@@
 func TestJSONLogger_ConcurrentRequests(t *testing.T) {
@@
 	for i := 0; i < 50; i++ {
 		<-done
 	}
+
+	lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
+	require.Len(t, lines, 50)
+	for _, line := range lines {
+		var entry JSONLogEntry
+		require.NoError(t, json.Unmarshal([]byte(line), &entry))
+		assert.Equal(t, "/concurrent", entry.Path)
+		assert.Equal(t, http.StatusOK, entry.Status)
+	}
 }
@@
 func (s *safeBuffer) Write(p []byte) (n int, err error) {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 	return s.buf.Write(p)
 }
+
+func (s *safeBuffer) String() string {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	return s.buf.String()
+}

Also applies to: 151-161

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jsonlogger_test.go` around lines 130 - 149, Update
TestJSONLogger_ConcurrentRequests to validate output integrity: after the
goroutines finish, read buf.String(), split into lines, assert there are 50
non-empty lines and that each line is valid JSON (e.g., by unmarshaling into
map[string]interface{}), and optionally assert each JSON contains expected keys
(status, method, path). Locate the safeBuffer used as Output via
JSONLoggerWithConfig(JSONLoggerConfig{Output: buf}) and add the
parsing/assertions after the done channel loop; apply the same pattern to the
other concurrent test block (lines 151-161) that uses a safeBuffer to ensure it
verifies 50 valid JSON log entries rather than only goroutine completion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@jsonlogger.go`:
- Around line 27-44: The middleware currently always appends
request.URL.RawQuery into the logged "Path", risking sensitive query data; add a
config flag on JSONLoggerConfig (e.g., LogQueryString bool or IncludeQueryString
bool, default false for safety) and update the code that builds the "Path" (the
place that currently reads request.URL.RawQuery into Path) to only append
RawQuery when JSONLoggerConfig.LogQueryString is true; keep existing
Skip/SkipPaths/MaskHeaders behavior unchanged and ensure the new flag is checked
before any masking or logging of the query string.
- Around line 37-39: The comment and implementation disagree about which skip
rule wins; update the logging decision so the Skip Skipper is evaluated before
SkipPaths (i.e., call Skip(...) first and if it returns true skip logging
regardless of SkipPaths), and update the declaration comment on Skip to state
that Skip takes precedence; specifically change the conditional in the
log/handler decision logic that currently checks SkipPaths first to check Skip
first and keep SkipPaths as a fallback.

---

Nitpick comments:
In `@jsonlogger_test.go`:
- Around line 130-149: Update TestJSONLogger_ConcurrentRequests to validate
output integrity: after the goroutines finish, read buf.String(), split into
lines, assert there are 50 non-empty lines and that each line is valid JSON
(e.g., by unmarshaling into map[string]interface{}), and optionally assert each
JSON contains expected keys (status, method, path). Locate the safeBuffer used
as Output via JSONLoggerWithConfig(JSONLoggerConfig{Output: buf}) and add the
parsing/assertions after the done channel loop; apply the same pattern to the
other concurrent test block (lines 151-161) that uses a safeBuffer to ensure it
verifies 50 valid JSON log entries rather than only goroutine completion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 572bc824-3e8d-41a3-98a6-a2b94d0a7ca9

📥 Commits

Reviewing files that changed from the base of the PR and between d3ffc99 and 1817deb.

📒 Files selected for processing (2)
  • jsonlogger.go
  • jsonlogger_test.go

Comment thread jsonlogger.go
Comment on lines +27 to +44
// JSONLoggerConfig defines configuration for the JSONLogger middleware.
type JSONLoggerConfig struct {
// Output is the writer where JSON log lines are written.
// Defaults to gin.DefaultWriter (os.Stdout).
Output io.Writer

// SkipPaths is a list of URL paths that will not be logged.
// Useful for suppressing health-check or metrics endpoints.
SkipPaths []string

// Skip is an optional function that returns true when a request
// should not be logged. Takes precedence over SkipPaths.
Skip Skipper

// MaskHeaders is a list of header names whose values will be
// redacted to "***" in error output to prevent leaking credentials.
MaskHeaders []string
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a query-string opt-out to prevent sensitive data leakage in logs.

Line 75-Line 78 always logs RawQuery into Path. That can expose credentials/tokens passed via query parameters in production logs.

Suggested fix
 type JSONLoggerConfig struct {
@@
 	// Skip is an optional function that returns true when a request
 	// should not be logged. Takes precedence over SkipPaths.
 	Skip Skipper
+
+	// SkipQueryString indicates that query strings should not be included
+	// in logged paths (useful when secrets are passed via query params).
+	// Optional. Default is false.
+	SkipQueryString bool
@@
 	return func(c *Context) {
 		start := time.Now()
 		path := c.Request.URL.Path
-		if raw := c.Request.URL.RawQuery; raw != "" {
-			path = path + "?" + raw
+		if !cfg.SkipQueryString {
+			if raw := c.Request.URL.RawQuery; raw != "" {
+				path = path + "?" + raw
+			}
 		}

Also applies to: 75-78

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jsonlogger.go` around lines 27 - 44, The middleware currently always appends
request.URL.RawQuery into the logged "Path", risking sensitive query data; add a
config flag on JSONLoggerConfig (e.g., LogQueryString bool or IncludeQueryString
bool, default false for safety) and update the code that builds the "Path" (the
place that currently reads request.URL.RawQuery into Path) to only append
RawQuery when JSONLoggerConfig.LogQueryString is true; keep existing
Skip/SkipPaths/MaskHeaders behavior unchanged and ensure the new flag is checked
before any masking or logging of the query string.

Comment thread jsonlogger.go
Comment on lines +37 to +39
// Skip is an optional function that returns true when a request
// should not be logged. Takes precedence over SkipPaths.
Skip Skipper
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Config docs and implementation order are inconsistent for skip behavior.

Line 37-Line 39 says Skip takes precedence, but Line 82-Line 86 checks SkipPaths first. Please align comment wording with actual behavior to avoid misuse.

Suggested wording update
-	// Skip is an optional function that returns true when a request
-	// should not be logged. Takes precedence over SkipPaths.
+	// Skip is an optional function that returns true when a request
+	// should not be logged. Evaluated in addition to SkipPaths.
 	Skip Skipper

Also applies to: 82-86

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jsonlogger.go` around lines 37 - 39, The comment and implementation disagree
about which skip rule wins; update the logging decision so the Skip Skipper is
evaluated before SkipPaths (i.e., call Skip(...) first and if it returns true
skip logging regardless of SkipPaths), and update the declaration comment on
Skip to state that Skip takes precedence; specifically change the conditional in
the log/handler decision logic that currently checks SkipPaths first to check
Skip first and keep SkipPaths as a fallback.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="jsonlogger.go">

<violation number="1" location="jsonlogger.go:39">
P2: The doc comment says `Skip` "takes precedence over `SkipPaths`", but the implementation checks `SkipPaths` first (line 82-84) and returns before `Skip` is ever evaluated. This means `Skip` **cannot** override `SkipPaths` — the opposite of the documented behavior. Either reorder the checks so `Skip` is evaluated first, or update the comment to say they are evaluated independently (both cause skipping).</violation>

<violation number="2" location="jsonlogger.go:98">
P2: `c.Writer.Size()` returns -1 when no body has been written, producing `"body_bytes": -1` in the JSON output. For machine-parsed structured logs, clamp to 0.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread jsonlogger.go
Method: c.Request.Method,
Path: path,
ClientIP: c.ClientIP(),
BodyBytes: c.Writer.Size(),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: c.Writer.Size() returns -1 when no body has been written, producing "body_bytes": -1 in the JSON output. For machine-parsed structured logs, clamp to 0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At jsonlogger.go, line 98:

<comment>`c.Writer.Size()` returns -1 when no body has been written, producing `"body_bytes": -1` in the JSON output. For machine-parsed structured logs, clamp to 0.</comment>

<file context>
@@ -0,0 +1,127 @@
+			Method:    c.Request.Method,
+			Path:      path,
+			ClientIP:  c.ClientIP(),
+			BodyBytes: c.Writer.Size(),
+			Errors:    errMsg,
+		}
</file context>
Fix with Cubic

Comment thread jsonlogger.go

// Skip is an optional function that returns true when a request
// should not be logged. Takes precedence over SkipPaths.
Skip Skipper
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The doc comment says Skip "takes precedence over SkipPaths", but the implementation checks SkipPaths first (line 82-84) and returns before Skip is ever evaluated. This means Skip cannot override SkipPaths — the opposite of the documented behavior. Either reorder the checks so Skip is evaluated first, or update the comment to say they are evaluated independently (both cause skipping).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At jsonlogger.go, line 39:

<comment>The doc comment says `Skip` "takes precedence over `SkipPaths`", but the implementation checks `SkipPaths` first (line 82-84) and returns before `Skip` is ever evaluated. This means `Skip` **cannot** override `SkipPaths` — the opposite of the documented behavior. Either reorder the checks so `Skip` is evaluated first, or update the comment to say they are evaluated independently (both cause skipping).</comment>

<file context>
@@ -0,0 +1,127 @@
+
+	// Skip is an optional function that returns true when a request
+	// should not be logged. Takes precedence over SkipPaths.
+	Skip Skipper
+
+	// MaskHeaders is a list of header names whose values will be
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants