skip to content

Bridging the log Package

Half the code in a build still writes to log.Default, and http.Server.ErrorLog wants a *log.Logger, so slog.NewLogLogger is how those lines land in the same structured stream.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

What does Go's log package write by default: which stream, what prefix, and what severity?

level: juniorimportance: must knowfreq 48%

answer

  1. one logger behind the whole package
  2. which of the two standard streams
  3. date plus time, nothing else
  4. Print, Printf, Println: same severity
  5. log.Default, SetOutput, SetFlags, SetPrefix

basics

~20 s

Go's log package writes to standard error through the one logger returned by log.Default(), prefixing each line with the date and time (log.LstdFlags). It has no severity levels: log.Print, log.Printf and log.Println all produce identical, unlabelled lines.

solid answer

~40 s

The package-level helpers all write through a single `*log.Logger` you can fetch with `log.Default()`. Its destination is `os.Stderr`, its flags are `log.LstdFlags` (`Ldate|Ltime`), and its prefix is empty, so a call prints `2026/09/01 14:03:11 message` and a newline. There is no level concept at all: `log.Print`, `log.Printf` and `log.Println` differ only in formatting, not in severity, and nothing marks a line as an error. You reconfigure that one logger process-wide with `log.SetOutput`, `log.SetFlags` and `log.SetPrefix`, or build an independent one with `log.New(w, prefix, flags)`. That combination of a fixed text prefix and no severity is exactly why a structured sink has to parse these lines, and why `log/slog` exists.

code

go · 7 lines
go
log.Println("proxy listening")
// 2026/09/01 14:03:11 proxy listening    -> os.Stderr

log.SetPrefix("proxy: ")
log.SetFlags(0)
log.Println("proxy listening")
// proxy: proxy listening

go deeper

for a junior

Recall the three facts exactly: standard error, a date and time prefix from log.LstdFlags, and no severity levels at all. Know that log.SetOutput, log.SetFlags and log.SetPrefix change one shared logger for the whole process.

for a middle

Be ready to explain the mechanics: one package-level logger reachable via log.Default(), flags as a bitmask, the mutex that keeps lines whole, and why an embedded newline in a message breaks a one-record-per-line consumer.

for a senior

Show that you know what this costs in production. Unlabelled text lines force severity to be inferred by a collector, and any flag change silently reshapes what your ingest has to parse, so the prefix format is effectively a contract.

for a principal

Own the position that the log package is still a legitimate boundary, not legacy to be purged. Standard-library components and vendored code write through it, so the strategy is where to attach a structured sink, not whether to ban the package.

## One logger behind the package Every call to `log.Print`, `log.Printf`, `log.Println` and their friends goes through a single shared `*log.Logger` value that the package owns. Since Go 1.16 you can get a handle on it with `log.Default()`, which returns that same logger, so `log.Printf(...)` and `log.Default().Printf(...)` are the same call. That logger has three configurable pieces: - **an output** - an `io.Writer`, defaulting to `os.Stderr`, changed with `log.SetOutput(w)`; - **flags** - a bitmask of what to put in front of the message, defaulting to `log.LstdFlags`, changed with `log.SetFlags(f)`; - **a prefix** - a literal string, defaulting to empty, changed with `log.SetPrefix(s)`. `log.LstdFlags` is defined as `Ldate | Ltime`: the local date as `2009/01/23` and the local time as `01:23:23`, separated by a space and followed by a space before the message. Other flags you can add are `Lmicroseconds` (fractional seconds), `Llongfile` and `Lshortfile` (the caller's file and line), `LUTC` (render the timestamp in UTC) and `Lmsgprefix` (move the prefix from the very start of the line to just before the message, rather than before the timestamp). So the default rendering of `log.Println("proxy listening")` is one line on standard error reading `2026/09/01 14:03:11 proxy listening`. The logger appends a trailing newline if the message does not already end with one, and it holds an internal mutex, so concurrent calls from many goroutines do not interleave mid-line. ## What is deliberately missing The package has **no severity levels**. `log.Print` is not "info" and there is no `log.Warn`. `log.Fatal*` and `log.Panic*` exist, but they are control-flow helpers - they print and then terminate or panic - not severity labels attached to the record. Nothing in the emitted text distinguishes a routine startup message from a serious failure unless the author wrote the distinction into the message string themselves, or set a prefix such as `log.SetPrefix("ERROR ")` on a dedicated logger. The package also has **no structure**. The output is one line of text. There are no key/value attributes, no machine-readable framing, and the timestamp is glued to the front of the message as characters. If the message itself contains a newline - a stack trace, a multi-line dump, a non-ASCII payload someone printed raw - the line breaks across several physical lines, and a downstream consumer that assumes one record per line will mis-split it. ## Why that shape matters downstream The standard logger is a fine tool for a command-line program a human reads. It becomes a problem the moment something machine-parses the output. A collector that expects JSON records will either drop these lines or, worse, wrap them as unparsed blobs; a collector that parses text has to be taught this exact prefix format, and it now has a dependency on your process's flag settings. This is the pressure that produced `log/slog` in Go 1.21: a record has a level, a message, a timestamp and typed attributes, and a handler decides the wire format. But the `log` package does not go away - the standard library itself still writes through it (for example `net/http`'s server diagnostics when you have not given it a logger of its own), and so does a great deal of existing and third-party code. That is why the interesting engineering question on this ground is not "which should I use" but "how do I get the `log` package's output into a structured pipeline without editing every call site". ## Independent loggers `log.New(w io.Writer, prefix string, flag int) *log.Logger` builds a logger unrelated to the package default, and the same three knobs exist as methods (`SetOutput`, `SetFlags`, `SetPrefix`) plus readers (`Flags`, `Prefix`, `Writer`). This matters because several standard-library types accept a `*log.Logger` as a field rather than calling the package functions - handing one of those a purpose-built logger is the injection point a structured pipeline uses. ## Things to say precisely - Default destination: `os.Stderr`, not stdout. - Default flags: `log.LstdFlags`, which is date plus time - **not** file and line, which requires `Lshortfile` or `Llongfile`. - Levels: none. Any severity you see in `log` output was typed into the message or the prefix by a human. - Concurrency: safe; the logger serialises writes.

  • How would you make the standard logger print the caller's file and line?
    Add `log.Lshortfile` (file name and line) or `log.Llongfile` (full path and line) to the flags, for example `log.SetFlags(log.LstdFlags | log.Lshortfile)`. Both are computed by walking the call stack at log time, which costs something per call, so they are usually enabled for debugging rather than left on in a hot path.
  • Two goroutines call log.Printf at the same time. Can their output interleave mid-line?
    No. `log.Logger` holds a mutex around formatting and the write, so each call emits one complete line. What is not guaranteed is ordering between goroutines, and the atomicity ends at the `io.Writer` you gave it: if that writer is itself unsafe for concurrent use, you have moved the problem rather than solved it.
  • What does log.Default() give you that the package-level functions do not?
    A `*log.Logger` value you can pass to something else. Several standard-library types take a `*log.Logger` field, and library APIs often accept one, so `log.Default()` lets you hand over the exact logger the package functions use instead of forcing that component onto its own private destination.

saying these in an interview costs you the question

  • Says log.Println writes to standard output
  • Thinks log.Print is info level and log.Fatal is error level
  • Believes the default flags include the file and line
  • Assumes each package-level call has its own logger
  • Claims log output is already machine-parseable
open as a page

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

level: middleimportance: should knowfreq 40%

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.

open as a page

http.Server.ErrorLog is nil in a reverse proxy and its TLS handshake errors never reach the structured sink. Why, and how do you fix it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

With a nil ErrorLog, net/http writes its own diagnostics through the log package's standard logger, as plain text on standard error. A JSON-only ingest discards them. Set ErrorLog to a logger from slog.NewLogLogger so those lines become structured records.

open as a page

Bridging every legacy log.Printf into slog converts a codebase at once but yields one level and no attributes. How do you decide bridge versus rewrite?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Bridge first, because it captures code you cannot edit and stops diagnostics being lost today. Then rewrite only the call sites whose fields someone actually queries, and agree with the log consumers how long unparsed bridged text is acceptable.

open as a page