skip to content

After slog.SetDefault, what happens to a dependency's log.Printf calls and at what level do they arrive?

level: middleimportance: should knowfreq 40%

answer

  1. SetDefault does two jobs, not one
  2. the log package keeps working
  3. one message string, zero attributes
  4. every bridged line shares one level
  5. SetLogLoggerLevel picks that level

basics

~20 s

slog.SetDefault redirects the log package's shared logger into the new default logger's handler, so log.Printf lines become slog records. Each arrives as a plain message with no attributes, at the level slog.SetLogLoggerLevel selects, which is Info unless changed.

solid answer

~40 s

`slog.SetDefault(l)` does two things: it installs `l` behind `slog.Info` and friends, and it points `log.Default()`'s output at `l`'s handler. From then on a `log.Printf` anywhere in the process - including in a package you did not write - produces a `slog.Record` whose `Message` is the formatted text, with **zero attributes**, at the level returned by `slog.SetLogLoggerLevel`, which defaults to `slog.LevelInfo`. `SetDefault` also clears the standard logger's flags, so you do not get the old `LstdFlags` date and time embedded in the message text next to the handler's own timestamp. That is the whole bridge: it is process-wide, needs no edits to call sites, and buys you routing and encoding, not structure - the lines are still one opaque string each.

code

go · 7 lines
go
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
slog.SetDefault(logger)                // log.Print* now flow through logger's handler
slog.SetLogLoggerLevel(slog.LevelWarn) // ...and arrive at Warn instead of Info

// somewhere in a package you do not own:
log.Printf("cache refresh failed: %v", err)
// -> one record, message only, no attrs, level Warn

go deeper

for a junior

Remember that installing a default slog logger does not orphan existing log package calls; they keep working and start coming out of the new handler. Know that they arrive at Info unless something changes the level.

for a middle

Explain the mechanics both ways: the built-in default routes slog through log, and SetDefault reverses it by pointing log's output at your handler and clearing the flags. State clearly that a bridged record has a message and no attributes.

for a senior

Show judgment about the flattening. One level for every bridged line means either promoting chatter or demoting failures, so be ready to say when you would instead inject a per-component logger from slog.NewLogLogger.

for a principal

Frame the bridge as a temporary contract with whoever consumes the logs. Bridged lines are unparsed text with a level you assigned wholesale, and that is a cost the ingest owner carries, so agree how long it stands.

## The two directions of one small API There are two bridges between `log` and `log/slog`, and `slog.SetLogLoggerLevel` controls both, which is why it confuses people. **Before you call `slog.SetDefault`**, the default `slog` logger is a built-in handler that writes *through the log package*. In that state `slog.Info("x")` ends up in `log.Default()`'s output, and `slog.SetLogLoggerLevel` sets the minimum level for those top-level `slog` calls - so `slog.Debug` is dropped by default. **After you call `slog.SetDefault(l)`** with a real handler, the flow reverses. `SetDefault` calls `log.SetOutput` with a writer that turns each written line into a `slog.Record` and hands it to `l`'s handler, and it clears the standard logger's flags with `log.SetFlags(0)`. Now `log.Print`, `log.Printf` and every standard-library or third-party component that writes through the package logger is emitted by your handler, at the level `SetLogLoggerLevel` holds - `slog.LevelInfo` unless you changed it. ## What survives the bridge and what does not A bridged line becomes a record with: - **Message** - the formatted text of the `log` call, with the trailing newline removed. - **Time** - stamped by the bridge, and rendered by your handler. - **Level** - a single value for *every* bridged line, chosen once by `SetLogLoggerLevel`. - **Attrs** - none. There is nothing to extract them from; the call site formatted everything into one string. So a `log.Printf("cache refresh failed for tenant %s: %v", id, err)` arrives as one message string. The tenant id and the error are inside the text, not addressable as fields, and no amount of handler configuration recovers them. Anything downstream that wants to filter on tenant is back to regular expressions. The level flattening is the sharper edge. A vendored package that logs one routine progress line and one genuine failure through the same `log.Printf` produces two records at the same level. If you set the bridged level high so failures are not lost, the routine chatter is promoted with it; if you set it low, the failures are demoted. There is no per-line answer available, because the source had no per-line severity to begin with. ## Why clearing the flags matters If you wire the bridge by hand instead - `log.SetOutput(someWriter)` without touching the flags - the `LstdFlags` date and time stay on. Every message then starts with `2026/09/01 14:03:11 ` **inside** the message field, next to whatever timestamp your handler already emits. Downstream that shows up as a duplicated, differently formatted time that a parser has to strip. `SetDefault` avoids it by dropping the flags; if you build a bridge yourself, do the same. The other text hazard is embedded newlines. A `log.Printf` that prints a multi-line block produces one record whose message contains newlines. A handler that escapes its output survives that fine; a line-oriented consumer downstream may split one record into several fragments, none of which parse. ## Per-component bridging instead of process-wide The process-wide bridge is not the only option, and often not the best one. `slog.NewLogLogger(h slog.Handler, level slog.Level) *log.Logger` returns a `*log.Logger` whose writes become records on `h` at that fixed level. Where a component accepts a `*log.Logger` field, handing it one of these gives that component its own level and its own handler, without touching the process default. That is how you can say "this subsystem's chatter is Debug, that one's diagnostics are Error" even though both write through the `log` package. ## Ordering, and one deadlock to avoid `SetDefault` is what installs the bridge, so call it before the code you want captured runs - a package's `init` function that logs during import will have already written through the unbridged logger. And do not build a handler that itself logs through the `log` package: the bridge would feed that handler's own output back into itself. The standard library guards its own default case, but a hand-rolled handler wrapping `log.Printf` can loop. ## How to answer this crisply Say that `SetDefault` installs the default `slog` logger **and** redirects `log`; that bridged lines are messages with no attributes at one fixed level from `SetLogLoggerLevel`; that flags are cleared so the timestamp is not doubled; and that `slog.NewLogLogger` is the per-component alternative when one global level is too blunt.

  • Why does slog.SetDefault clear the standard logger's flags?
    Because the flags would otherwise be rendered into the message text. With `LstdFlags` left on, every bridged record's message begins with a date and time, sitting next to the timestamp the handler emits itself. Clearing them leaves the message as just the message and lets the handler own time formatting.
  • What does slog.SetLogLoggerLevel control before slog.SetDefault has been called?
    The other direction. Until a default logger is installed, `slog`'s top-level functions write through the log package, and `SetLogLoggerLevel` is the minimum level for those calls - which is why `slog.Debug` produces nothing out of the box. After `SetDefault`, the same setting becomes the level that bridged log package lines are recorded at.
  • A dependency logs both progress and failures through log.Printf. Can the bridge give them different levels?
    Not through the process-wide bridge - it applies one level to everything. If the component accepts a `*log.Logger`, hand it its own from `slog.NewLogLogger` at the level that suits it. Otherwise the only remaining lever is a handler that inspects the message text, which is fragile and worth avoiding.

saying these in an interview costs you the question

  • Thinks SetDefault silences the log package
  • Expects bridged lines to gain structured attributes
  • Believes each bridged line keeps its own severity
  • Assumes slog.Debug works before SetDefault is called
  • Leaves LstdFlags on and ships a doubled timestamp