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?
answer
- two separate jobs, one project
- cheap and reversible goes first
- code you cannot edit decides nothing else
- who pays for unparsed text
- rewrite what someone actually queries
basics
~20 sBridge 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.
solid answer
~50 sI treat them as sequential, not alternatives. The bridge - `slog.SetDefault` plus `slog.NewLogLogger` on the fields that take a `*log.Logger` - is cheap, lands in one change, and covers vendored code and standard-library components I cannot edit at all, so it goes first and immediately fixes lines falling outside the pipeline. But it freezes a decision into the pipeline: every bridged line is one opaque message at a level I assigned wholesale, so severity is now a property of the bridge rather than of the call. Rewriting is then triaged by demand, not by count - the call sites someone filters or alerts on get real attributes; startup chatter stays bridged forever, and that is fine. The constraint that binds me is the ingest owner: if they will not carry unparsed text, or the level flattening breaks their routing, the bridge stops being a resting place and becomes a deadline.
go deeper
Understand that redirecting where log output goes and adding fields to a log line are different jobs with very different costs, and that the first can be done without touching call sites.
Be able to describe both mechanisms concretely and state what a bridged record does and does not carry, so the tradeoff discussion rests on real behaviour rather than on preference.
Show how you would sequence it in a live service: bridge to stop losing lines, then convert the call sites that alerts and dashboards already depend on, measuring progress rather than counting converted files.
Own the negotiation. Name who carries the cost of unparsed text and a flattened level, agree which streams stay bridged permanently, and set a rule that keeps the bridged set shrinking instead of quietly becoming the design.
## Two things that look like one decision "Move the service to structured logging" hides two separable pieces of work. **Redirecting output** is about where lines go and in what encoding. `slog.SetDefault` repoints the `log` package's shared logger at your handler; `slog.NewLogLogger` does the same for any component that exposes a `*log.Logger` field. This is a handful of lines, has no per-call-site cost, and works on code you did not write and cannot patch. **Restructuring records** is about whether a line has fields. Only editing the call site can do that, because the arguments were formatted into a string before anything else saw them. The mistake is bundling them, which makes the whole migration as expensive as the second piece and delays the first indefinitely. ## Why the bridge goes first almost always The bridge fixes a live defect: lines leaving the process outside the pipeline. That includes classes of message nothing else reports - a listener's own connection diagnostics, a library's warning about a fallback it took. Until the bridge is in, those are simply unavailable during an incident, and no amount of future rewriting helps the incident you are having now. It is also reversible in a way a rewrite is not. Removing a bridge is deleting two calls; unwinding a half-finished rewrite across hundreds of sites is not. ## What the bridge costs, stated honestly Three things, and a lead should name all three unprompted. 1. **Severity becomes an assignment, not an observation.** Every bridged line shares one level. Set it high and routine chatter is promoted into whatever you page on; set it low and a genuine failure from a dependency is buried below your alert threshold. There is no correct value because the source never had a per-line severity to preserve. 2. **The message stays a blob.** Identifiers inside the text are not queryable. Anyone who needs to filter by them is writing regular expressions against your log lines, which quietly makes your message wording a contract you cannot change safely. 3. **The text can be hostile.** Legacy lines carry embedded newlines and non-ASCII payloads someone printed raw. A structured encoder handles that, but a line-oriented consumer downstream may split one record into fragments that fail to decode. ## Who can overrule you This is where the call stops being a code decision. The people who own ingest carry cost 2 and cost 3, not you. If their pipeline charges by volume, promoting a chatty dependency to a routed level is their budget. If they hold a parse-failure rate as an objective, your bridged lines land in it. A perfectly reasonable outcome is that they accept the bridge for named packages and refuse it for a component whose lines are ten times the volume of everything else. So the artefact I want out of the decision is not a plan to convert everything; it is an agreement: which streams stay bridged indefinitely, which have a conversion date, and what the ingest side measures to hold me to it. The parse-failure count sampled back to source is a good shared number, because it is visible to both sides and falls as the rewrite progresses. ## How I triage the rewrite By demand, in this order: - Call sites that appear in an alert, a dashboard query or a saved search. Somebody is already paying the regular-expression tax on these; they get attributes first. - Call sites in the code path of the last few incidents, where fields would have shortened the investigation. - Components where the flattened level is actively wrong - a dependency mixing progress and failure through one function. Where that component accepts a `*log.Logger`, a per-component bridged logger at its own level is often enough and much cheaper than a rewrite. - Everything else: leave it. Startup banners and once-per-hour progress lines gain nothing from attributes, and converting them burns review time for no query anyone runs. What I will not do is a mechanical sweep that rewrites every call site into a record with one attribute per format verb. It produces field names nobody chose, it churns the whole codebase in a diff no one can review, and it lands the same information in a more expensive shape. ## The organisational failure mode to guard against A bridge with no owner and no date becomes permanent, and the next person reads the unparsed lines as the intended design. The countermeasure is boring: record which streams are bridged and why, keep the parse-failure number visible, and make new code use the structured API directly so the bridged set only ever shrinks. If it does not shrink for two quarters, the honest move is to declare the remainder permanent and stop pretending, rather than to keep a migration open forever.
- What would make you refuse to bridge a particular component and insist on rewriting it first?Volume plus level mismatch. If one dependency produces most of the log lines and mixes routine progress with real failures through the same calls, no single bridged level is defensible - either I flood the routed stream or I hide failures. That component gets a rewrite, or its own logger at its own level, before the bridge ships.
- How would you know the bridge is working rather than just moving the problem?By watching the ingest side, not the service. The number I want is lines the collector could not decode, sampled back to their source: it should drop sharply when the bridge lands, because text that used to leave the process unrouted is now encoded by the handler. If it does not move, something is still writing outside the pipeline.
- New code is written while the migration is in flight. What rule do you give the team?New call sites use the structured API directly, with attributes, and never add to the bridged set. The bridge is a compatibility layer for code that already exists, so its scope should only ever shrink. That single rule keeps the remaining work bounded without demanding a freeze on feature development.
saying these in an interview costs you the question
- Proposes rewriting every call site before anything ships
- Claims the bridge gives legacy lines proper structure
- Ignores that vendored code cannot be rewritten at all
- Picks one bridged level without naming what it distorts
- Treats the log consumers as having no say