Skip to content

Wide events logging#62

Merged
mishankov merged 38 commits intomainfrom
log2
Feb 17, 2026
Merged

Wide events logging#62
mishankov merged 38 commits intomainfrom
log2

Conversation

@mishankov
Copy link
Member

@mishankov mishankov commented Feb 17, 2026

Summary by CodeRabbit

  • New Features

    • Wide-event logging: create, enrich, and finalize multi-step events with error capture
    • Tail sampling for events with configurable thresholds and HTTP-status rules
    • Per-request wide-event middleware and trace ID middleware for automatic enrichment
  • Documentation

    • Revised log package docs with new APIs, middleware guide, and embedded examples
  • Chores

    • Dev server scripts updated; linting rule adjustments; demo app includes a wide-event example

@mishankov mishankov linked an issue Feb 17, 2026 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Wide events logging' directly and clearly describes the main feature addition—wide-event logging capabilities—which is central to this PR's changes across the log package.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch log2

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

@coveralls
Copy link

coveralls commented Feb 17, 2026

Pull Request Test Coverage Report for Build 22088253765

Details

  • 1 of 316 (0.32%) changed or added relevant lines in 6 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-7.3%) to 30.855%

Changes Missing Coverage Covered Lines Changed/Added Lines %
log/event_context.go 0 6 0.0%
log/sampler.go 0 30 0.0%
log/wideevent_logger.go 0 46 0.0%
log/wideevent_middleware.go 0 86 0.0%
log/event.go 0 147 0.0%
Totals Coverage Status
Change from base Build 21670678201: -7.3%
Covered Lines: 498
Relevant Lines: 1614

💛 - Coveralls

Copy link

@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: 3

🧹 Nitpick comments (6)
log/event.go (2)

183-195: Consider using a set for reserved key lookup.

slices.Contains is O(n) per key. For events with many custom attributes, this could add overhead. A map[string]struct{} would provide O(1) lookups.

♻️ Proposed optimization
-	customAttrKeys := make([]string, 0, len(e.attrs))
-	for key := range e.attrs {
-		if slices.Contains(reservedAttrKeys, key) {
-			continue
-		}
-
-		customAttrKeys = append(customAttrKeys, key)
-	}
+	reservedSet := make(map[string]struct{}, len(reservedAttrKeys))
+	for _, k := range reservedAttrKeys {
+		reservedSet[k] = struct{}{}
+	}
+
+	customAttrKeys := make([]string, 0, len(e.attrs))
+	for key := range e.attrs {
+		if _, reserved := reservedSet[key]; reserved {
+			continue
+		}
+		customAttrKeys = append(customAttrKeys, key)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@log/event.go` around lines 183 - 195, The loop building customAttrKeys uses
slices.Contains against reservedAttrKeys causing O(n) checks per attribute;
convert reservedAttrKeys into a map[string]struct{} (e.g., reservedAttrSet) once
before the loop and replace slices.Contains(reservedAttrKeys, key) with a
constant-time lookup like "if _, ok := reservedAttrSet[key]; ok { continue }",
leaving the rest of the logic (customAttrKeys slice,
sort.Strings(customAttrKeys), and appending attrs with slog.Any) unchanged and
referenced by the same symbols (e.attrs, customAttrKeys, reservedAttrKeys,
attrs, slog.Any).

200-209: JSON tags on internal record types are unused.

The stepRecord and errorRecord types have JSON struct tags, but toAttrs() manually constructs map[string]any representations (lines 149-153, 158-161) rather than marshaling these structs. Consider removing the tags to avoid misleading future maintainers, or use the structs directly with slog.Any.

Option: Use structs directly
-	steps := make([]map[string]any, 0, len(e.steps))
-	for _, step := range e.steps {
-		steps = append(steps, map[string]any{
-			"timestamp": step.Timestamp,
-			"level":     step.Level.String(),
-			"name":      step.Name,
-		})
-	}
+	steps := make([]stepRecord, 0, len(e.steps))
+	for _, step := range e.steps {
+		steps = append(steps, step)
+	}

Note: This would require adjusting the Level field serialization since slog.Level doesn't serialize to string by default.

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

In `@log/event.go` around lines 200 - 209, The JSON struct tags on stepRecord and
errorRecord are misleading because toAttrs() builds maps manually; either remove
the `json:"..."` tags from the type definitions (stepRecord, errorRecord) to
avoid confusion, or refactor to use the structs directly in toAttrs() by
returning slog.Any("step", stepRecord{/*...*/}) / slog.Any("error",
errorRecord{/*...*/}) and ensure slog-friendly serialization for
stepRecord.Level (e.g., add a string field or implement a method to convert
slog.Level to its string before attaching), updating the toAttrs() code paths
that currently construct maps to use the chosen approach.
demo-app/cmd/wide-events/main.go (1)

14-19: Consider adjusting keepHTTPStatusAtLeast for a more illustrative demo.

The keepHTTPStatusAtLeast: 200 means all HTTP responses would be kept since 200 is the baseline success code. For a demo showcasing tail-sampling, a value like 400 or 500 (to keep only errors) would better illustrate the sampling behavior.

Note: This doesn't affect the current demo since WriteEvent is called directly without HTTP context, but it may confuse readers trying to understand the sampler's purpose.

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

In `@demo-app/cmd/wide-events/main.go` around lines 14 - 19, The sampler creation
uses log.NewDefaultSampler with keepHTTPStatusAtLeast set to 200 which preserves
all HTTP responses; update the NewDefaultSampler invocation to use a higher
threshold (e.g., keepHTTPStatusAtLeast = 400 or 500) so the demo shows
tail-sampling decisions (only error responses kept) — modify the call site where
NewDefaultSampler is constructed (the logger := log.NewWideEventLogger(...)
block) to pass the new threshold value.
docs/package.json (1)

6-7: Minor inconsistency between dev and start scripts.

The dev script now includes --host while start does not. If both scripts serve similar purposes (local development), consider aligning them, or add a comment explaining the distinction.

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

In `@docs/package.json` around lines 6 - 7, The package.json scripts show an
inconsistency: "dev" uses the --host flag while "start" does not; decide whether
both should behave the same and update accordingly—either add --host to the
"start" script to match "dev" or document the difference with a comment in
package.json (or README) clarifying that "dev" exposes the dev server on the
network while "start" intentionally does not; update the "dev" and/or "start"
entries to reflect the chosen behavior (scripts "dev" and "start").
log/sampler.go (1)

29-35: Clamp or validate randomKeepRate to [0,1].

Out-of-range values silently change sampling behavior (always keep or always drop). Consider clamping or panicking on invalid configuration.

🧰 Suggested clamp
 func NewDefaultSampler(slowThreshold time.Duration, keepHTTPStatusAtLeast int, randomKeepRate float64) *DefaultSampler {
+	if randomKeepRate < 0 {
+		randomKeepRate = 0
+	} else if randomKeepRate > 1 {
+		randomKeepRate = 1
+	}
 	return &DefaultSampler{
 		slowThreshold:         slowThreshold,
 		keepHTTPStatusAtLeast: keepHTTPStatusAtLeast,
 		randomKeepRate:        randomKeepRate,
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@log/sampler.go` around lines 29 - 35, NewDefaultSampler currently accepts
randomKeepRate without bounds, letting out-of-range values alter sampling;
modify NewDefaultSampler to validate and clamp randomKeepRate into [0,1] before
storing it on the returned *DefaultSampler (e.g., if randomKeepRate < 0 set to
0, if > 1 set to 1) so sampling behavior is deterministic; reference the
NewDefaultSampler function and the DefaultSampler.randomKeepRate field when
making this change (use simple conditionals or math.Min/Max to clamp).
log/wideevent_middleware.go (1)

80-103: Optional: Delegate http.ResponseWriter optional interfaces for better middleware compatibility.

While no handlers in the codebase currently use these interfaces, properly delegating Flusher, Hijacker, and Pusher is a best practice for response writer wrappers. This prevents potential issues if future handlers rely on streaming, SSE, or WebSocket support. The embedded http.ResponseWriter already provides these methods, but explicit delegation with type-checks ensures they work correctly when the underlying writer supports them.

Consider adding:

  • Flush() delegation for http.Flusher
  • Hijack() delegation for http.Hijacker
  • Push() delegation for http.Pusher
  • Optional Unwrap() method to expose the wrapped writer
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@log/wideevent_middleware.go` around lines 80 - 103, The statusResponseWriter
wrapper should explicitly delegate optional interfaces so wrapped handlers that
expect streaming/SSE/WebSocket/HTTP/2 push still work: add methods on
statusResponseWriter that type-assert the embedded ResponseWriter and forward
calls — implement Flush() to call http.Flusher, Hijack() returning (net.Conn,
*bufio.ReadWriter, error) to call http.Hijacker, Push(target string, opts
*http.PushOptions) error to call http.Pusher, and an Unwrap()
http.ResponseWriter method that returns the underlying ResponseWriter; each
method should return appropriate errors if the underlying writer does not
support the interface and otherwise call-through to the underlying
implementation to preserve behavior for WriteHeader/Write.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@log/wideevent_logger.go`:
- Around line 17-40: The constructor NewWideEventLogger may leave the sampler
field nil causing a panic in WriteEvent when calling l.sampler.ShouldSample;
ensure NewWideEventLogger checks if the provided Sampler s is nil and if so
assigns a default “keep-all” sampler implementation (or return an error/fail
fast) before storing it on the WideEventLogger, so WriteEvent can safely call
l.sampler.ShouldSample; update the sampler assignment in NewWideEventLogger (and
add a lightweight keep-all sampler type or factory if not present) and ensure
WideEventLogger.sampler is never nil.

In `@log/wideevent_middleware.go`:
- Around line 42-47: The middleware is currently logging raw client IPs via the
"request.remoteAddr" attribute in the NewEvent -> event.AddAttrs call, which
exposes PII; change this to either redact or pseudonymize the value before
adding it (e.g., replace with a masked string, hash/HMAC it using a server-side
secret, or omit it) and make the behavior controlled by a configuration flag
(e.g., EnablePIILogging or AllowRemoteAddrLogging) so it's opt-in; update the
code that sets "request.remoteAddr" in wideevent_middleware.go (where NewEvent
and event.AddAttrs are used) to perform the chosen redaction/hashing and respect
the config flag.
- Around line 23-36: NewWideEventMiddleware currently assigns a nil
WideEventLogger directly, which will cause a panic when WideEventMiddleware
methods call m.logger.WriteEvent; update NewWideEventMiddleware to guard against
a nil logger (parameter logger) and either return/fail-fast or substitute a
no-op logger instance. Specifically, inside NewWideEventMiddleware check if
logger == nil and set logger to a no-op WideEventLogger (implement a minimal
no-op writer) or return an error/panic as your API policy prefers so that
WideEventMiddleware (and calls to WriteEvent) never nil-dereference; reference
the constructor NewWideEventMiddleware, type WideEventMiddleware, method
WriteEvent, and symbols WideEventLogger, WideEventKey, defaultWideEventName when
making the change.

---

Nitpick comments:
In `@demo-app/cmd/wide-events/main.go`:
- Around line 14-19: The sampler creation uses log.NewDefaultSampler with
keepHTTPStatusAtLeast set to 200 which preserves all HTTP responses; update the
NewDefaultSampler invocation to use a higher threshold (e.g.,
keepHTTPStatusAtLeast = 400 or 500) so the demo shows tail-sampling decisions
(only error responses kept) — modify the call site where NewDefaultSampler is
constructed (the logger := log.NewWideEventLogger(...) block) to pass the new
threshold value.

In `@docs/package.json`:
- Around line 6-7: The package.json scripts show an inconsistency: "dev" uses
the --host flag while "start" does not; decide whether both should behave the
same and update accordingly—either add --host to the "start" script to match
"dev" or document the difference with a comment in package.json (or README)
clarifying that "dev" exposes the dev server on the network while "start"
intentionally does not; update the "dev" and/or "start" entries to reflect the
chosen behavior (scripts "dev" and "start").

In `@log/event.go`:
- Around line 183-195: The loop building customAttrKeys uses slices.Contains
against reservedAttrKeys causing O(n) checks per attribute; convert
reservedAttrKeys into a map[string]struct{} (e.g., reservedAttrSet) once before
the loop and replace slices.Contains(reservedAttrKeys, key) with a constant-time
lookup like "if _, ok := reservedAttrSet[key]; ok { continue }", leaving the
rest of the logic (customAttrKeys slice, sort.Strings(customAttrKeys), and
appending attrs with slog.Any) unchanged and referenced by the same symbols
(e.attrs, customAttrKeys, reservedAttrKeys, attrs, slog.Any).
- Around line 200-209: The JSON struct tags on stepRecord and errorRecord are
misleading because toAttrs() builds maps manually; either remove the
`json:"..."` tags from the type definitions (stepRecord, errorRecord) to avoid
confusion, or refactor to use the structs directly in toAttrs() by returning
slog.Any("step", stepRecord{/*...*/}) / slog.Any("error", errorRecord{/*...*/})
and ensure slog-friendly serialization for stepRecord.Level (e.g., add a string
field or implement a method to convert slog.Level to its string before
attaching), updating the toAttrs() code paths that currently construct maps to
use the chosen approach.

In `@log/sampler.go`:
- Around line 29-35: NewDefaultSampler currently accepts randomKeepRate without
bounds, letting out-of-range values alter sampling; modify NewDefaultSampler to
validate and clamp randomKeepRate into [0,1] before storing it on the returned
*DefaultSampler (e.g., if randomKeepRate < 0 set to 0, if > 1 set to 1) so
sampling behavior is deterministic; reference the NewDefaultSampler function and
the DefaultSampler.randomKeepRate field when making this change (use simple
conditionals or math.Min/Max to clamp).

In `@log/wideevent_middleware.go`:
- Around line 80-103: The statusResponseWriter wrapper should explicitly
delegate optional interfaces so wrapped handlers that expect
streaming/SSE/WebSocket/HTTP/2 push still work: add methods on
statusResponseWriter that type-assert the embedded ResponseWriter and forward
calls — implement Flush() to call http.Flusher, Hijack() returning (net.Conn,
*bufio.ReadWriter, error) to call http.Hijacker, Push(target string, opts
*http.PushOptions) error to call http.Pusher, and an Unwrap()
http.ResponseWriter method that returns the underlying ResponseWriter; each
method should return appropriate errors if the underlying writer does not
support the interface and otherwise call-through to the underlying
implementation to preserve behavior for WriteHeader/Write.

Copy link

@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)
log/event.go (1)

137-199: Optional: stabilize custom attribute order for deterministic logs/tests.

Map iteration order is random; sorting keys yields consistent output.

♻️ Suggested tweak
 	customAttrKeys := make([]string, 0, len(e.attrs))
 	for key := range e.attrs {
 		if _, reserved := reservedSet[key]; reserved {
 			continue
 		}
 		customAttrKeys = append(customAttrKeys, key)
 	}
+	slices.Sort(customAttrKeys)

 	for _, key := range customAttrKeys {
 		attrs = append(attrs, slog.Any(key, e.attrs[key]))
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@log/event.go` around lines 137 - 199, The iteration over map keys in
Event.toAttrs produces non-deterministic ordering; to stabilize logs/tests, sort
the customAttrKeys slice before appending attributes. Inside the toAttrs method
(which builds reservedAttrKeys, reservedSet and customAttrKeys from e.attrs and
wideEventBuiltinAttrKeys), after populating customAttrKeys call
sort.Strings(customAttrKeys) (importing sort) and then iterate that sorted slice
when appending slog.Any entries for each key; no other logic changes are needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@log/event.go`:
- Around line 48-54: Add a nil-guard and lazy init for the Event attrs map
inside the AddAttrs method: before calling maps.Copy, acquire the lock (already
done), check if e.attrs is nil and if so assign a new map (e.attrs =
make(map[string]any)), then call maps.Copy(e.attrs, attrs) only when attrs is
non-nil; keep using e.mu.Lock() / defer e.mu.Unlock() as currently implemented.
Ensure you reference Event.AddAttrs, e.attrs and maps.Copy when making the
change.

In `@log/wideevent_logger.go`:
- Around line 48-55: The WriteEvent method can panic when called with a nil
event because e.Finish() is invoked unguarded; update WideEventLogger.WriteEvent
to first check if e is nil and return early (no-op) if so, then call e.Finish()
and proceed to the existing sampling/logging logic (sampler.ShouldSample and
logger.LogAttrs) to prevent nil dereference. Ensure the nil check is done before
calling e.Finish() and references the WideEventLogger.WriteEvent, Event.Finish,
sampler.ShouldSample, and logger.LogAttrs symbols so the change is applied in
the right place.

---

Duplicate comments:
In `@log/wideevent_middleware.go`:
- Around line 45-84: The middleware currently logs raw r.RemoteAddr (PII);
update WideEventMiddleware.Wrap to avoid logging it directly by making redaction
opt-in and pseudonymizing when enabled: add a bool option/field like
RedactRemoteAddr on WideEventMiddleware (or use existing config), and when
building the attrs call use a local variable remote := r.RemoteAddr, strip the
port if present, then if m.RedactRemoteAddr compute a stable pseudonym (e.g.
hash the stripped IP) and set "request.remoteAddr" to that value else set it to
the original; keep the rest of the event.AddAttrs call and use the same
event.AddAttrs map to insert the redacted/pseudonymized value so consumers get
no raw PII unless the flag is explicitly false.

---

Nitpick comments:
In `@log/event.go`:
- Around line 137-199: The iteration over map keys in Event.toAttrs produces
non-deterministic ordering; to stabilize logs/tests, sort the customAttrKeys
slice before appending attributes. Inside the toAttrs method (which builds
reservedAttrKeys, reservedSet and customAttrKeys from e.attrs and
wideEventBuiltinAttrKeys), after populating customAttrKeys call
sort.Strings(customAttrKeys) (importing sort) and then iterate that sorted slice
when appending slog.Any entries for each key; no other logic changes are needed.

@mishankov mishankov merged commit 0b2bd95 into main Feb 17, 2026
9 checks passed
@mishankov mishankov deleted the log2 branch February 17, 2026 06:45
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.

Wide events loggin support

2 participants

Comments