Skip to content

Comments

Wide events logging#61

Closed
mishankov wants to merge 32 commits intomainfrom
log2
Closed

Wide events logging#61
mishankov wants to merge 32 commits intomainfrom
log2

Conversation

@mishankov
Copy link
Member

@mishankov mishankov commented Feb 16, 2026

Summary by CodeRabbit

  • New Features

    • Added wide-event logging: Event API, tail-sampling, a WideEventLogger, request middleware, and a demo tool to emit sample events.
  • Refactor

    • Switched trace ID middleware to the centralized logging provider.
  • Documentation

    • Updated logging package docs to reflect new APIs and usage.
  • Chores

    • Updated local dev/docs run scripts and added linter exclusions for the logging package.

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

coderabbitai bot commented Feb 16, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds a wide-event logging subsystem (Event type, sampling, WideEventLogger, middleware), relocates TraceID middleware into the log package and updates demos to use it, adds a wide-events demo program, updates docs/dev scripts, and adds a golangci revive exclusion for log/ files.

Changes

Cohort / File(s) Summary
Log — Core event types & context
log/event.go, log/event_context.go
Adds a concurrency-safe Event type with lifecycle APIs (NewEvent, SetLevel, AddAttrs, AddStep, AddError, Finish, ToAttrs) and a context key WideEventKey plus EventFromContext.
Log — Sampling & logger
log/sampler.go, log/wideevent_logger.go
Introduces Sampler interface, DefaultSampler (error/slow/status/random criteria) and WideEventLogger that finalizes events, applies sampling, and emits via slog JSON/text handlers.
Log — HTTP middleware & trace ID
log/wideevent_middleware.go, log/traceid.go, log/traceid_test.go
Adds WideEventMiddleware to capture request attrs, status, panics, and always emit events; moves/adjusts TraceID middleware into log and updates tests.
Demo apps
demo-app/cmd/api/main.go, demo-app/cmd/auth/main.go, demo-app/cmd/wide-events/main.go
Switches demos to use the relocated TraceID middleware and adds a new wide-events demo program that constructs and writes a sample Event.
Docs & frontend scripts
Taskfile.dist.yml, docs/package.json
Changes docs dev command invocation (e.g., bun run dev / astro dev --host) and adds an astro script entry.
Linter config
.golangci.yml
Adds a revive exclusion rule targeting log/.*\.go to avoid var-naming conflicts with stdlib names.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Middleware as WideEventMiddleware
    participant Event as Event
    participant Handler as NextHandler
    participant Sampler as Sampler
    participant Logger as WideEventLogger

    Client->>Middleware: HTTP Request
    Middleware->>Event: NewEvent("http.request") / AddAttrs(method,path,remote_addr)
    Middleware->>Handler: Call next handler
    Handler->>Handler: Process request

    alt panic
        Handler-->>Middleware: panic()
        Middleware->>Event: AddError(panic) / set status 500 if needed
    else normal
        Handler-->>Middleware: returns (status)
        Middleware->>Event: AddAttrs(request.status)
    end

    Middleware->>Event: Finish()
    Middleware->>Sampler: ShouldSample(ctx, Event)
    alt sample == true
        Sampler-->>Logger: allow
        Middleware->>Logger: WriteEvent(ctx, Event)
        Logger->>Logger: emit via slog (JSON/Text)
    else sample == false
        Sampler-->>Middleware: drop
    end
    Middleware->>Client: HTTP Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped a log with tails to keep,
I nudged each event from shallow to deep.
I marked the slow, the failed, the bright,
Sampled some, wrote some in JSON light.
Hooray — wide events take flight!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% 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' accurately summarizes the main change: introducing a new wide events logging system with Event types, samplers, middleware, and logger implementations 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 16, 2026

Pull Request Test Coverage Report for Build 22087197604

Details

  • 1 of 283 (0.35%) changed or added relevant lines in 6 files are covered.
  • 6 unchanged lines in 1 file lost coverage.
  • Overall coverage decreased (-7.0%) to 31.12%

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 43 0.0%
log/wideevent_middleware.go 0 58 0.0%
log/event.go 0 145 0.0%
Files with Coverage Reduction New Missed Lines %
database/service.go 6 75.0%
Totals Coverage Status
Change from base Build 21670678201: -7.0%
Covered Lines: 492
Relevant Lines: 1581

💛 - 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

🤖 Fix all issues with AI agents
In `@log/sampler.go`:
- Around line 39-57: DefaultSampler.ShouldSample currently reads Event fields
(e.errors, e.duration, e.attrs) without synchronization causing a data race; add
thread-safe getters on Event (e.g., HasErrors() bool, Duration() time.Duration,
Attr(key string) (any, bool)) that lock e.mu, return the requested values, and
then update DefaultSampler.ShouldSample to call e.HasErrors(), e.Duration(), and
e.Attr("request.status") instead of accessing fields directly so all reads are
protected by the Event mutex.
- Around line 55-57: The current check "if httpStatus >= s.keepHTTPStatusAtLeast
{ return true }" treats a zero httpStatus (absent) as a match when
keepHTTPStatusAtLeast is 0; update the guard in the sampler (the code
referencing httpStatus and s.keepHTTPStatusAtLeast) to explicitly treat 0 as
"absent" by requiring httpStatus > 0 before comparing (or alternatively document
the current behavior); change the condition to require httpStatus > 0 &&
httpStatus >= s.keepHTTPStatusAtLeast so absent statuses are not accidentally
sampled, and add a brief comment explaining the intent.

In `@log/wideevent_logger.go`:
- Around line 41-46: The WriteEvent method reads e.level unsafely after calling
e.Finish(); to fix, add a synchronized getter on Event (e.g., func (e *Event)
Level() slog.Level that locks e.mu, returns e.level) and replace direct access
to e.level in WideEventLogger.WriteEvent with e.Level(), or alternatively modify
Event.ToAttrs() to return the level alongside attrs and use that return value in
WideEventLogger.WriteEvent; update references in WriteEvent to use the new
getter/return value (Event.Level or the new ToAttrs signature) so no field is
accessed without the mutex.
🧹 Nitpick comments (4)
log/wideevent_middleware.go (2)

1-1: Static analysis: package name conflicts with Go stdlib log.

The linter flags that package log shadows the standard library package. This is a package-level design decision affecting all files. Users of this package will need to use import aliases (as seen in log/traceid_test.go with platformalog).

If renaming isn't desired, consider documenting the recommended import alias in package documentation.


80-103: Implement Unwrap() method to expose underlying ResponseWriter capabilities.

statusResponseWriter wraps http.ResponseWriter but does not implement Unwrap(). This breaks functionality for handlers using http.Flusher (SSE), http.Hijacker (WebSockets), or http.Pusher (HTTP/2 push). Adding Unwrap() allows http.ResponseController to access the underlying writer's capabilities:

 type statusResponseWriter struct {
 	http.ResponseWriter
 	statusCode  int
 	wroteHeader bool
 }

+// Unwrap returns the underlying ResponseWriter for http.ResponseController.
+func (w *statusResponseWriter) Unwrap() http.ResponseWriter {
+	return w.ResponseWriter
+}
log/event.go (2)

82-88: Duration calculated in both Finish() and ToAttrs() - intentional?

Finish() sets e.duration = time.Since(e.timestamp), but ToAttrs() recalculates it (line 95). This means:

  1. If ToAttrs() is called without Finish(), it still works
  2. If both are called, the duration reflects ToAttrs() call time, not Finish() time

If Finish() is meant to snapshot the duration, line 95 should be removed or guarded.

💡 Only calculate duration if not already set
 func (e *Event) ToAttrs() []slog.Attr {
 	e.mu.Lock()
 	defer e.mu.Unlock()

-	e.duration = time.Since(e.timestamp)
+	if e.duration == 0 {
+		e.duration = time.Since(e.timestamp)
+	}

Also applies to: 90-95


124-141: Unused code: stepRecord.LogValue() and JSON struct tags.

stepRecord implements LogValue() but ToAttrs() manually converts steps to []map[string]any instead of using it. The JSON struct tags on both stepRecord and errorRecord also appear unused since serialization is handled manually.

Consider either:

  • Removing the unused LogValue() method and JSON tags
  • Or refactoring ToAttrs() to use slog.Any("steps", e.steps) which would invoke LogValue()

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.

🧹 Nitpick comments (1)
.golangci.yml (1)

75-78: Consider using revive configuration instead of text-based exclusion for stronger stability.

The text matcher relies on exact diagnostic wording, which can shift across golangci-lint or revive version updates. Instead, configure the var-naming rule directly in linters-settings to disable the package-name collision check:

linters-settings:
  revive:
    rules:
      - name: var-naming
        arguments:
          - []
          - []
          - skip-package-name-collision-with-go-std: true

This targets the rule configuration directly (stable across versions) rather than matching prose output, and makes the intent clearer to future maintainers.

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: 1

🤖 Fix all issues with AI agents
In `@log/event.go`:
- Around line 69-80: Add a nil-check to avoid calling err.Error() when no error
is provided: in AddError, return early if err == nil before taking the mutex so
you don't escalate level or append an error record for a nil value. Keep using
e.setLevelNoLock and appending to e.errors/errorRecord only when err is non-nil
(call err.Error() after the nil guard).
🧹 Nitpick comments (2)
log/event.go (2)

116-148: ToAttrs mutates state unexpectedly.

Line 121 sets e.duration = time.Since(e.timestamp), which is a mutation inside what appears to be a read-only conversion method. This creates potential confusion:

  1. If Finish() was already called, ToAttrs() will overwrite the recorded duration.
  2. Calling ToAttrs() multiple times yields different durations.
  3. The method name suggests a pure conversion, not a state mutation.

Consider either removing the mutation (relying on Finish() being called first) or renaming/documenting the side effect explicitly.

♻️ Option A: Remove mutation, use existing duration
 func (e *Event) ToAttrs() []slog.Attr {
 	e.mu.Lock()
 	defer e.mu.Unlock()

-	e.duration = time.Since(e.timestamp)
+	duration := e.duration
+	if duration == 0 {
+		duration = time.Since(e.timestamp)
+	}

 	steps := make([]map[string]any, 0, len(e.steps))
 	// ... rest unchanged ...

 	return []slog.Attr{
 		slog.String("name", e.name),
 		slog.Time("timestamp", e.timestamp),
-		slog.Duration("duration", e.duration),
+		slog.Duration("duration", duration),
 		slog.Any("attrs", e.attrs),
 		slog.Any("steps", steps),
 		slog.Any("errors", eventErrors),
 	}
 }

150-167: stepRecord.LogValue() is unused.

The LogValue() method on stepRecord implements slog.LogValuer, but ToAttrs() manually constructs map[string]any slices instead of using it. Either remove the unused method or refactor ToAttrs() to leverage it for consistency.

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: 1

🤖 Fix all issues with AI agents
In `@log/event.go`:
- Around line 124-155: To prevent concurrent map access panics, clone the
Event.attrs map inside Event.ToAttrs() while still holding e.mu and return the
cloned map in the slog.Attr instead of the original e.attrs; locate the ToAttrs
method on type Event and replace slog.Any("attrs", e.attrs) with a shallow copy
(new map[string]any populated from e.attrs) so that after unlocking the original
e.attrs can be mutated by AddAttrs without impacting the logged data consumed
asynchronously by LogAttrs.

@mishankov mishankov closed this Feb 17, 2026
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