Skip to content

Comments

Wide event logging#60

Closed
mishankov wants to merge 14 commits intomainfrom
log2
Closed

Wide event logging#60
mishankov wants to merge 14 commits intomainfrom
log2

Conversation

@mishankov
Copy link
Member

@mishankov mishankov commented Feb 16, 2026

  • Remove default time attr from log output

wide-events uses slog under the hood, and the standard time field is added by the handler.
Drop it with HandlerOptions.ReplaceAttr:

opts := &slog.HandlerOptions{
	Level: level,
	ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
		if a.Key == slog.TimeKey {
			return slog.Attr{} // remove default slog "time"
		}
		return a
	},
}

Then use opts in both handlers:

slog.NewJSONHandler(w, opts)
slog.NewTextHandler(w, opts)

Summary by CodeRabbit

  • New Features

    • Added a comprehensive wide-event logging system with configurable formats, sampling, and emission controls
    • Added structured event lifecycle (steps, errors, attributes) and safe context helpers for attaching events
    • Added HTTP middleware that automatically creates and finishes per-request events, capturing status, route, and panic info
    • Added sampling rules and a default sampler for selective event retention
  • Chores

    • Updated dev scripts and docs server invocation for the development workflow

@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

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Adds two wide-event logging implementations (log2 and log3) with event lifecycle, sampling, and HTTP middleware; introduces demo programs; tweaks dev tooling and trace-id middleware imports; minor lint and test adjustments.

Changes

Cohort / File(s) Summary
Build & Dev scripts
Taskfile.dist.yml, docs/package.json
Switched docs task to bun run dev and added --host to Astro dev script.
Linting
log/log.go
Added //nolint:revive package directive.
Trace ID adjustments & tests
log/traceid.go, log/traceid_test.go, demo-app/cmd/api/main.go, demo-app/cmd/auth/main.go
Switched trace-id middleware origin to local log package, removed external import, updated test package and assertions, and updated demo-app middleware usage.
log2 — Core config & context
log2/config.go, log2/context.go
Added Config struct with DefaultConfig and context helpers to attach/retrieve *Event in context.
log2 — Event model
log2/event.go
Introduced Event type with Add/Step/Error/Finish, concurrency safety, attr/step/error handling, payload creation, and ErrEventAlreadyFinished.
log2 — Logger & emit
log2/logger.go
Added Logger, Default instance, New/SetDefault, Start helpers, and emit logic to build/write slog.Record.
log2 — Middleware
log2/middleware.go
HTTP middleware that starts an event per request, records status/bytes, handles panics, and finishes the event.
log2 — Sampling
log2/sampling.go
Rule-based tail sampler (TailSampler) with DefaultTailSamplerConfig, keep rules, slow/random/status logic, and attribute lookup.
log3 — Event & sampler
log3/event.go, log3/sampler.go
Alternate Event builder (thread-safe) and Sampler interface with DefaultSampler implementing error/duration/status/random rules.
log3 — Logger
log3/logger.go
WideEventLogger wrapping slog.Logger with WriteEvent that finishes events and consults sampler before logging.
Demos
demo-app/cmd/wide-events/main.go
New demo program creating a WideEventLogger, building an event with step, error, attributes, and emitting to stdout.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Middleware as HTTP Middleware
    participant Handler as App Handler
    participant Event as Event
    participant Sampler as Sampler
    participant Logger as Logger

    Client->>Middleware: HTTP Request
    activate Middleware
    Middleware->>Event: Start(ctx, "http_request")
    Middleware->>Handler: call next handler (ctx contains event)
    activate Handler
    Handler-->>Middleware: Response (status, bytes)
    deactivate Handler
    Middleware->>Event: Add(request attrs, response attrs)
    Middleware->>Event: Finish()
    activate Event
    Event->>Sampler: ShouldSample(EventView)
    activate Sampler
    Sampler-->>Event: SamplingDecision{Keep/Drop, Reason}
    deactivate Sampler
    alt Keep
      Event->>Logger: emit(eventPayload)
      activate Logger
      Logger->>Logger: Build slog.Record and write
      deactivate Logger
    end
    Event-->>Middleware: done
    Middleware-->>Client: Final Response
    deactivate Middleware
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰
I hopped through code with bits so bright,
Events and samplers leaped to light.
Middleware drums a per-request beat,
Logs hop home, concise and neat.
Scrumptious bytes — a rabbit's treat! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Wide event logging' is vague and does not clearly describe the specific changes in the pull request. While related to the overall theme, it lacks specificity about what is being implemented. Use a more specific title that describes the main change, such as 'Add wide-event logging system with sampling' or 'Implement configurable wide-event logger with tail sampling'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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 22071736233

Details

  • 1 of 647 (0.15%) changed or added relevant lines in 10 files are covered.
  • 6 unchanged lines in 1 file lost coverage.
  • Overall coverage decreased (-12.8%) to 25.296%

Changes Missing Coverage Covered Lines Changed/Added Lines %
log3/logger.go 0 12 0.0%
log2/config.go 0 13 0.0%
log2/context.go 0 14 0.0%
log3/sampler.go 0 32 0.0%
log2/middleware.go 0 63 0.0%
log2/logger.go 0 80 0.0%
log3/event.go 0 88 0.0%
log2/sampling.go 0 115 0.0%
log2/event.go 0 229 0.0%
Files with Coverage Reduction New Missed Lines %
database/service.go 6 75.0%
Totals Coverage Status
Change from base Build 21670678201: -12.8%
Covered Lines: 492
Relevant Lines: 1945

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

🤖 Fix all issues with AI agents
In `@log2/logger.go`:
- Around line 29-34: The handler creation never applies the requested
ReplaceAttr to remove the default time attribute; update both
slog.NewTextHandler and slog.NewJSONHandler calls to pass a HandlerOptions that
sets ReplaceAttr (in addition to Level) and implement ReplaceAttr to filter out
attributes where a.Key == slog.TimeKey (returning the zero/empty Attr to drop
it), using the existing symbols handler, slog.NewTextHandler,
slog.NewJSONHandler, slog.TimeKey and slog.HandlerOptions so the log output no
longer contains the default time field.

In `@log3/event.go`:
- Around line 64-74: The AddError method on Event may dereference a nil error
because it calls err.Error(); update Event.AddError to guard against nil: check
if err == nil and either return early (no-op) or record a sentinel string (e.g.,
"<nil error>") instead of calling err.Error(); ensure this logic is applied
before calling e.setLevelNoLock or appending to e.errors (referencing
Event.AddError, e.mu, setLevelNoLock, and the errorRecord slice).
- Around line 1-8: Add a package-level doc comment at the top and add Go doc
comments for each exported symbol (Event, NewEvent, SetLevel, AddAttrs, AddStep,
AddError, Finish, ToAttrs) describing purpose and usage to satisfy linters;
place a short package comment immediately above "package log3" and add brief
sentences starting with the symbol name above each exported type or function
(e.g., "Event ...", "NewEvent ..." etc.), keeping comments idiomatic and
complete sentences to resolve the missing doc comment lint failures.

In `@log3/logger.go`:
- Around line 11-22: The Logger struct declares an unused w io.Writer field
which is not set in NewWideEventLogger; either remove the w field from the
Logger type or assign it when constructing a new Logger. Update the Logger
struct or the constructor accordingly: for the former, delete the w field; for
the latter, set w: w in the return value of NewWideEventLogger so the writer is
stored alongside sampler and logger (referencing Logger and NewWideEventLogger).

In `@log3/sampler.go`:
- Around line 42-54: The HTTP status comparison erroneously treats a missing or
zero status as matching when s.keepHttpStatusAtLeast is 0; change the check so
you only compare when a non-zero status was actually present: after extracting
statusFromMap and casting into httpStatus (using e.attrs["request.status"] and
statusFromMap.(int)), ensure you verify that exists is true and httpStatus != 0
before evaluating httpStatus >= s.keepHttpStatusAtLeast; treat httpStatus == 0
as "no status" and skip/return false for this branch so absent statuses are not
sampled when the threshold is 0.
- Around line 56-58: The sampling branch in sampler.go is inverted: change the
condition that uses rand.Float64() so we keep events with probability
s.randomKeepRate rather than keep ~1-randomKeepRate; replace the
`s.randomKeepRate <= rand.Float64()` check with the correct comparison (e.g. use
`s.randomFloat() < s.randomKeepRate` or `rand.Float64() < s.randomKeepRate`) in
the method that decides to drop/keep events (referencing s.randomKeepRate and
the current rand.Float64() usage), and if you keep math/rand for probabilistic
sampling add a `//nolint:gosec` with a short justification comment.
🧹 Nitpick comments (8)
Taskfile.dist.yml (1)

27-27: Switching to Bun requires Bun to be installed.

This change introduces an implicit dependency on the Bun runtime. Ensure team members and CI environments have Bun installed, or document this requirement in the project's setup instructions.

docs/package.json (1)

6-7: Inconsistency between dev and start scripts.

The dev script now binds to all interfaces with --host, while start still runs astro dev without it. Consider aligning them or documenting when to use each:

  • dev → external access (network-accessible)
  • start → local-only (localhost)

If the distinction is intentional, this is fine. Otherwise, consider renaming or consolidating.

log3/event.go (2)

76-81: Duration is calculated in both Finish() and ToAttrs().

Both methods compute e.duration = time.Since(e.timestamp). If Finish() is called before ToAttrs(), the duration is overwritten. If ToAttrs() is called without Finish(), it still works. This creates ambiguity about the intended usage pattern.

Consider either:

  1. Only compute duration in Finish() and have ToAttrs() use the stored value
  2. Remove Finish() if ToAttrs() is always expected to finalize the event

Also applies to: 83-87


120-126: stepRecord.LogValue() appears unused.

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

log2/config.go (1)

29-42: ContextKeys is not initialized in DefaultConfig().

The Config.ContextKeys field is not set, so it defaults to nil. If downstream code (e.g., log.New() in the log package) expects a non-nil map, this could cause issues. Consider initializing it to an empty map if a nil map could cause problems:

 func DefaultConfig() Config {
 	return Config{
 		Writer: os.Stdout,
 		Format: defaultFormat,
 		Level:  slog.LevelInfo,
+		ContextKeys: map[string]any{},
 		Sampler: NewDefaultTailSampler(DefaultTailSamplerConfig{

If nil is explicitly acceptable downstream, this is fine as-is.

log3/logger.go (1)

24-30: Consider checking for nil sampler.

If l.sampler is nil (e.g., if NewWideEventLogger is called with a nil sampler), line 27 will panic. Consider adding a nil check or defaulting to always-sample behavior:

 func (l *Logger) WriteEvent(ctx context.Context, e *Event) {
 	e.Finish()
 
-	if l.sampler.ShouldSample(ctx, e) {
+	if l.sampler == nil || l.sampler.ShouldSample(ctx, e) {
 		l.logger.LogAttrs(ctx, e.level, "", e.ToAttrs()...)
 	}
 }
log2/middleware.go (1)

83-99: statusRecorder may break streaming, WebSocket, and SSE handlers.

Wrapping http.ResponseWriter without implementing optional interfaces (http.Flusher, http.Hijacker, http.CloseNotifier, http.Pusher) causes type assertions in downstream handlers to fail. For example, SSE handlers calling w.(http.Flusher).Flush() will panic.

♻️ Proposed fix to support http.Flusher
 type statusRecorder struct {
 	http.ResponseWriter
 	status int
 	bytes  int
 }

+func (r *statusRecorder) Flush() {
+	if f, ok := r.ResponseWriter.(http.Flusher); ok {
+		f.Flush()
+	}
+}
+
+func (r *statusRecorder) Unwrap() http.ResponseWriter {
+	return r.ResponseWriter
+}

Consider also implementing http.Hijacker if WebSocket support is needed, or use a library like httpsnoop that handles this transparently.

log2/event.go (1)

140-147: Use toInt for consistent type coercion.

Status extraction uses a direct .(int) type assertion, but toInt (defined at lines 293-335) handles int, int8-64, uint, uint8-64, float32/64, and string. Using toInt here would be more robust if request.status is set from JSON unmarshaling (often float64) or other sources.

♻️ Proposed fix
 	status := 0
-	statusFromAttrs, ok := e.attrs["request.status"]
-	if ok {
-		status, ok = statusFromAttrs.(int)
-		if !ok {
-			status = 0
-		}
+	if statusFromAttrs, ok := e.attrs["request.status"]; ok {
+		status, _ = toInt(statusFromAttrs)
 	}

Comment on lines +29 to +34
var handler slog.Handler
if cfg.Format == "text" {
handler = slog.NewTextHandler(writer, &slog.HandlerOptions{Level: cfg.Level})
} else {
handler = slog.NewJSONHandler(writer, &slog.HandlerOptions{Level: cfg.Level})
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

PR objective not implemented: slog.TimeKey not removed.

The PR description explicitly requests removing the default time attribute from log output using ReplaceAttr, but the handler options only set Level. The suggested code from the PR description should be applied here.

🐛 Proposed fix per PR objectives
+	opts := &slog.HandlerOptions{
+		Level: cfg.Level,
+		ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
+			if a.Key == slog.TimeKey {
+				return slog.Attr{} // remove default slog "time"
+			}
+			return a
+		},
+	}
+
 	var handler slog.Handler
 	if cfg.Format == "text" {
-		handler = slog.NewTextHandler(writer, &slog.HandlerOptions{Level: cfg.Level})
+		handler = slog.NewTextHandler(writer, opts)
 	} else {
-		handler = slog.NewJSONHandler(writer, &slog.HandlerOptions{Level: cfg.Level})
+		handler = slog.NewJSONHandler(writer, opts)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var handler slog.Handler
if cfg.Format == "text" {
handler = slog.NewTextHandler(writer, &slog.HandlerOptions{Level: cfg.Level})
} else {
handler = slog.NewJSONHandler(writer, &slog.HandlerOptions{Level: cfg.Level})
}
opts := &slog.HandlerOptions{
Level: cfg.Level,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{} // remove default slog "time"
}
return a
},
}
var handler slog.Handler
if cfg.Format == "text" {
handler = slog.NewTextHandler(writer, opts)
} else {
handler = slog.NewJSONHandler(writer, opts)
}
🤖 Prompt for AI Agents
In `@log2/logger.go` around lines 29 - 34, The handler creation never applies the
requested ReplaceAttr to remove the default time attribute; update both
slog.NewTextHandler and slog.NewJSONHandler calls to pass a HandlerOptions that
sets ReplaceAttr (in addition to Level) and implement ReplaceAttr to filter out
attributes where a.Key == slog.TimeKey (returning the zero/empty Attr to drop
it), using the existing symbols handler, slog.NewTextHandler,
slog.NewJSONHandler, slog.TimeKey and slog.HandlerOptions so the log output no
longer contains the default time field.

Comment on lines +1 to +8
package log3

import (
"log/slog"
"maps"
"sync"
"time"
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add missing package and export documentation to fix lint failures.

Static analysis reports missing doc comments on the package and all exported symbols. Add documentation to resolve these lint failures:

Suggested documentation additions
+// Package log3 provides wide-event logging with sampling support.
 package log3

Then add doc comments above each exported symbol (Event, NewEvent, SetLevel, AddAttrs, AddStep, AddError, Finish, ToAttrs).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
package log3
import (
"log/slog"
"maps"
"sync"
"time"
)
// Package log3 provides wide-event logging with sampling support.
package log3
import (
"log/slog"
"maps"
"sync"
"time"
)
🧰 Tools
🪛 GitHub Check: lint

[failure] 1-1:
package-comments: should have a package comment (revive)

🤖 Prompt for AI Agents
In `@log3/event.go` around lines 1 - 8, Add a package-level doc comment at the top
and add Go doc comments for each exported symbol (Event, NewEvent, SetLevel,
AddAttrs, AddStep, AddError, Finish, ToAttrs) describing purpose and usage to
satisfy linters; place a short package comment immediately above "package log3"
and add brief sentences starting with the symbol name above each exported type
or function (e.g., "Event ...", "NewEvent ..." etc.), keeping comments idiomatic
and complete sentences to resolve the missing doc comment lint failures.

Comment on lines +64 to +74
func (e *Event) AddError(err error) {
e.mu.Lock()
defer e.mu.Unlock()

e.setLevelNoLock(slog.LevelError)

e.errors = append(e.errors, errorRecord{
Timestamp: time.Now(),
Error: err.Error(),
})
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential nil pointer dereference if err is nil.

AddError calls err.Error() without checking if err is nil. This will panic if a caller passes a nil error.

Suggested fix
 func (e *Event) AddError(err error) {
+	if err == nil {
+		return
+	}
+
 	e.mu.Lock()
 	defer e.mu.Unlock()
 
 	e.setLevelNoLock(slog.LevelError)
 
 	e.errors = append(e.errors, errorRecord{
 		Timestamp: time.Now(),
 		Error:     err.Error(),
 	})
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (e *Event) AddError(err error) {
e.mu.Lock()
defer e.mu.Unlock()
e.setLevelNoLock(slog.LevelError)
e.errors = append(e.errors, errorRecord{
Timestamp: time.Now(),
Error: err.Error(),
})
}
func (e *Event) AddError(err error) {
if err == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.setLevelNoLock(slog.LevelError)
e.errors = append(e.errors, errorRecord{
Timestamp: time.Now(),
Error: err.Error(),
})
}
🧰 Tools
🪛 GitHub Check: lint

[failure] 64-64:
exported: exported method Event.AddError should have comment or be unexported (revive)

🤖 Prompt for AI Agents
In `@log3/event.go` around lines 64 - 74, The AddError method on Event may
dereference a nil error because it calls err.Error(); update Event.AddError to
guard against nil: check if err == nil and either return early (no-op) or record
a sentinel string (e.g., "<nil error>") instead of calling err.Error(); ensure
this logic is applied before calling e.setLevelNoLock or appending to e.errors
(referencing Event.AddError, e.mu, setLevelNoLock, and the errorRecord slice).

Comment on lines +11 to +22
type Logger struct {
w io.Writer
sampler Sampler
logger *slog.Logger
}

func NewWideEventLogger(w io.Writer, s Sampler, loggerType string, contextKeys map[string]any) *Logger {
return &Logger{
sampler: s,
logger: log.New(w, loggerType, slog.LevelDebug, contextKeys),
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Unused field w in Logger struct.

The w io.Writer field (line 12) is declared but never assigned in NewWideEventLogger. The writer is passed directly to log.New() but not stored. Either remove the unused field or store it if needed for future use.

Suggested fix
 type Logger struct {
-	w       io.Writer
 	sampler Sampler
 	logger  *slog.Logger
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type Logger struct {
w io.Writer
sampler Sampler
logger *slog.Logger
}
func NewWideEventLogger(w io.Writer, s Sampler, loggerType string, contextKeys map[string]any) *Logger {
return &Logger{
sampler: s,
logger: log.New(w, loggerType, slog.LevelDebug, contextKeys),
}
}
type Logger struct {
sampler Sampler
logger *slog.Logger
}
func NewWideEventLogger(w io.Writer, s Sampler, loggerType string, contextKeys map[string]any) *Logger {
return &Logger{
sampler: s,
logger: log.New(w, loggerType, slog.LevelDebug, contextKeys),
}
}
🤖 Prompt for AI Agents
In `@log3/logger.go` around lines 11 - 22, The Logger struct declares an unused w
io.Writer field which is not set in NewWideEventLogger; either remove the w
field from the Logger type or assign it when constructing a new Logger. Update
the Logger struct or the constructor accordingly: for the former, delete the w
field; for the latter, set w: w in the return value of NewWideEventLogger so the
writer is stored alongside sampler and logger (referencing Logger and
NewWideEventLogger).

Comment on lines +42 to +54
httpStatus := 0
statusFromMap, exists := e.attrs["request.status"]
if exists {
success := false
httpStatus, success = statusFromMap.(int)
if !success {
httpStatus = 0
}
}

if httpStatus >= s.keepHttpStatusAtLeast {
return true
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

HTTP status check triggers on status 0 when keepHttpStatusAtLeast is 0.

When keepHttpStatusAtLeast is set to 0 (or left uninitialized), and no request.status attribute exists (causing httpStatus to remain 0), the condition httpStatus >= s.keepHttpStatusAtLeast evaluates to 0 >= 0, which is true—forcing every event without an HTTP status to be sampled.

Consider whether status 0 should be treated as "no status" and excluded from the comparison, or require a minimum sensible threshold.

🤖 Prompt for AI Agents
In `@log3/sampler.go` around lines 42 - 54, The HTTP status comparison erroneously
treats a missing or zero status as matching when s.keepHttpStatusAtLeast is 0;
change the check so you only compare when a non-zero status was actually
present: after extracting statusFromMap and casting into httpStatus (using
e.attrs["request.status"] and statusFromMap.(int)), ensure you verify that
exists is true and httpStatus != 0 before evaluating httpStatus >=
s.keepHttpStatusAtLeast; treat httpStatus == 0 as "no status" and skip/return
false for this branch so absent statuses are not sampled when the threshold is
0.

Comment on lines +56 to +58
if s.randomKeepRate <= rand.Float64() {
return true
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Inverted random sampling logic.

The condition s.randomKeepRate <= rand.Float64() keeps events when the random value is greater than or equal to the keep rate. This is inverted—if randomKeepRate is 0.1 (10%), this keeps ~90% of events instead of 10%.

Compare with log2/sampling.go line 111 which correctly uses s.randomFloat() < s.randomKeepRate.

🐛 Proposed fix
-	if s.randomKeepRate <= rand.Float64() {
+	if rand.Float64() < s.randomKeepRate {
 		return true
 	}

Regarding the gosec G404 warning: math/rand/v2 is acceptable here since this is probabilistic sampling, not a security-sensitive operation. Consider adding a //nolint:gosec directive with justification if suppression is preferred over switching to crypto/rand.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if s.randomKeepRate <= rand.Float64() {
return true
}
if rand.Float64() < s.randomKeepRate {
return true
}
🧰 Tools
🪛 GitHub Actions: CI

[error] 56-56: G404: Use of weak random number generator (math/rand) detected by gosec. Use crypto/rand instead.

🪛 GitHub Check: lint

[failure] 56-56:
G404: Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand) (gosec)

🤖 Prompt for AI Agents
In `@log3/sampler.go` around lines 56 - 58, The sampling branch in sampler.go is
inverted: change the condition that uses rand.Float64() so we keep events with
probability s.randomKeepRate rather than keep ~1-randomKeepRate; replace the
`s.randomKeepRate <= rand.Float64()` check with the correct comparison (e.g. use
`s.randomFloat() < s.randomKeepRate` or `rand.Float64() < s.randomKeepRate`) in
the method that decides to drop/keep events (referencing s.randomKeepRate and
the current rand.Float64() usage), and if you keep math/rand for probabilistic
sampling add a `//nolint:gosec` with a short justification comment.

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