Skip to content

logger: write to stdout asynchronously#39

Closed
firecow wants to merge 1 commit into
mainfrom
mjn/async-logger
Closed

logger: write to stdout asynchronously#39
firecow wants to merge 1 commit into
mainfrom
mjn/async-logger

Conversation

@firecow

@firecow firecow commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Wrap os.Stdout in a shared AsyncWriter (buffered channel + single drain goroutine) so callers don't stall when stdout's consumer (filebeat, the container runtime, kubectl logs) falls behind. Dropped lines are counted.
  • Transparent: logger.New() / logger.NewWithLevel() signatures unchanged, all existing callers become async on upgrade.
  • Added logger.Flush() for graceful shutdown and logger.Dropped() for observability.
  • Fix unrelated race in periodic.Run: when ctx.Done() and ticker.C are both ready, select can pick the tick first and run fn after cancellation. Re-check ctx.Err() after consuming a tick. Found while running this PR's tests under -race.

Motivation

Two SSO outages on 2026-05-19 traced to synchronous JSON-to-stdout writes from request handlers: when stdout backpressured, handlers stalled long enough that kubelet's 1s liveness probes timed out and pods got killed. Anything depending on this package has the same fragility.

Test plan

  • go test -race -count=10 ./logger/...
  • go test -race -count=20 ./periodic/... (verifying the fix)
  • go test -race -count=3 ./...
  • golangci-lint run ./...

Wraps os.Stdout in a shared AsyncWriter that drains on a single
background goroutine through a buffered channel, so callers don't
stall when stdout's consumer (filebeat, the container runtime,
kubectl logs) falls behind. Drops + counts when the buffer is full.

The change is transparent: logger.New() and logger.NewWithLevel()
keep their signatures, all existing callers become async on upgrade.
Added logger.Flush() for graceful shutdown and logger.Dropped() for
observability.

Also fixes a non-deterministic bug in periodic.Run: when ctx.Done()
and ticker.C are both ready, select can pick the tick first and run
fn after cancellation. Re-checks ctx.Err() after consuming a tick.
@firecow firecow self-assigned this May 20, 2026
@sonarqubecloud

Copy link
Copy Markdown

@firecow firecow closed this May 20, 2026
@firecow firecow deleted the mjn/async-logger branch May 20, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant