skip to content

In Go's log/slog, what does implementing the LogValuer interface on a type change about how it is logged?

level: juniorimportance: must knowfreq 38%

answer

  1. one method decides what gets printed
  2. the type substitutes a value for itself
  3. slog resolves before the handler formats
  4. returns a slog.Value, not a string
  5. LogValue() slog.Value

basics

~20 s

A type that implements LogValuer supplies a LogValue method returning a slog.Value, and slog logs that substitute instead of the value itself. Every handler sees the substitute, so a secret can be replaced with a fixed placeholder in one place.

solid answer

~40 s

`slog.LogValuer` is a one-method interface: `LogValue() slog.Value`. Before a handler formats an attribute, slog calls `Value.Resolve()`, which keeps calling `LogValue` while the value still implements the interface, and the handler only ever sees the resolved result. So a token type whose `LogValue` returns `slog.StringValue("REDACTED")` is redacted everywhere it is logged, under the JSON handler and the text handler alike, without a single call site changing. It is the right hook for redaction because it is slog's own hook rather than a formatting convention borrowed from another package: the substitution happens before encoding, not during it. It also works the other way round, letting an expensive or noisy type render itself as a small group of the fields worth keeping.

code

go · 10 lines
go
type Token string

// LogValue is what slog logs in place of the token's own text.
func (t Token) LogValue() slog.Value {
	return slog.StringValue("REDACTED")
}

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("vendor drop loaded", slog.Any("token", Token("sk-live-9f2c")))
// the record carries "token":"REDACTED"

go deeper

for a junior

Be ready to write the method from memory: LogValue with no arguments returning a slog.Value, and a constructor such as slog.StringValue for the placeholder. Say plainly that slog calls it before the handler formats anything.

for a middle

Explain the resolution step: slog calls Value.Resolve, which keeps calling LogValue while the result still implements the interface, and the handler only sees the final value. Mention the value-versus-pointer receiver trap.

for a senior

Show that you know its limits in production: it protects values that reach slog as that type, and nothing else. Conversions to string, formatted message text and fields of a struct logged whole all slip past it.

for a principal

Frame it as where a control lives. Putting redaction on the domain type makes the rule reviewable in one file and reusable across services, but it only covers types your team owns and routes through slog.

## The interface `log/slog` defines exactly one interface for a value that wants a say in how it is logged: ```go type LogValuer interface { LogValue() Value } ``` A type implements it by declaring a `LogValue` method that returns a `slog.Value`. That is all. There is no registration step, no handler configuration, and no build tag. ## Where slog calls it Every attribute in a record is a `slog.Attr`, which is a key plus a `slog.Value`. A `Value` has a `Kind`, and one of the kinds is `slog.KindLogValuer` — that is the kind reported when the value being held implements `LogValuer`. Before the handler formats the attribute it calls `Value.Resolve()`: - if the value is not a `LogValuer`, `Resolve` returns it unchanged; - if it is, `Resolve` calls `LogValue` and looks again, repeating while the result is still a `LogValuer`; - the loop is bounded, so a type whose `LogValue` returns itself produces an error value rather than hanging; - if `LogValue` panics, `Resolve` recovers and substitutes a value describing the panic, so one bad method cannot take the process down from a log statement. `Resolve` is documented to return a value whose kind is never `KindLogValuer`. That guarantee is what lets every handler — the two in the standard library and any handler you write — stay ignorant of the mechanism: by the time a handler touches the value, resolution has already happened. ## Why that matters for redaction The naive way to keep a secret out of logs is discipline: never pass it to a log call. That fails the moment somebody logs a struct that contains it, adds a field, or writes a debug line during an incident. `LogValuer` moves the decision from the call site to the type, which is the only place that can be reviewed once and trusted afterwards: ```go type Token string func (t Token) LogValue() slog.Value { return slog.StringValue("REDACTED") } ``` Now any `slog.Any("token", tok)` anywhere in the program emits the placeholder. Note the return type: it is a `slog.Value`, not a `string`. Use the constructors — `slog.StringValue`, `slog.IntValue`, `slog.AnyValue` — or `slog.GroupValue(attrs...)` when the type should render as several safe fields instead of one. ## The group form `LogValue` returning a group is the interesting case for a domain type. A `Customer` can decide, once, what a customer is allowed to say about itself: ```go func (c Customer) LogValue() slog.Value { return slog.GroupValue( slog.String("id", c.ID), slog.String("region", c.Region), ) } ``` Every field not listed is simply absent from the record. That is a much stronger property than blocking known-bad field names, because it is a permit list rather than a deny list: a field added next month is invisible until somebody deliberately adds it here. ## Two traps worth knowing on day one **Receiver type.** If `LogValue` is declared with a pointer receiver `func (t *Token) LogValue() slog.Value` and the code logs a `Token` value rather than a `*Token`, the value stored in the interface is a `Token`, whose method set does not contain the method. It is not a `LogValuer`, `Resolve` leaves it alone, and the raw value is printed. Declaring `LogValue` on the value receiver makes it work for both. **Conversions discard it.** `slog.String("token", string(tok))` converts the token to a plain `string` before slog ever sees it. The attribute's value is now of kind string, has no methods, and the placeholder never appears. The same is true of `fmt.Sprintf("token=%s", tok)` baked into the message text — the message is not an attribute and is not resolved at all. ## What it is not `LogValuer` is slog's hook and only slog's hook. It has no effect on `fmt.Println`, on `encoding/json` marshalling of an API response, or on a panic's stack trace. It also is not applied recursively to the fields of an arbitrary struct: slog resolves the attribute's own value, and if that value is a plain struct the handler's encoder takes it from there. Redaction that must hold for a whole struct belongs on the struct's own `LogValue`.

  • Does slog still call LogValue if the method is declared on the pointer receiver and the code logs a value?
    No. With a pointer receiver, only `*Token` has the method in its method set. Logging a `Token` value stores a `Token` in the interface, slog's `Resolve` sees something that is not a `LogValuer`, and the raw value is formatted. Declare `LogValue` on the value receiver so both the value and the pointer are covered, or be disciplined about always logging the pointer.
  • What happens if a LogValue method returns a value that itself implements LogValuer?
    `Value.Resolve` loops: it calls `LogValue`, checks the result, and calls again while the result still implements the interface. The loop is bounded, so a type that resolves to itself forever yields a value carrying an error rather than hanging the logger. `Resolve` is documented never to return a value of kind `slog.KindLogValuer`.
  • What does slog do if a LogValue method panics?
    `Value.Resolve` recovers from the panic and substitutes a value describing it, so the record is still emitted and the program keeps running. That is deliberate: a log statement must never be the thing that kills a request. It is still a bug — the substituted value shows up in your output and should be treated as an alert, not as working redaction.

It is like a witness with a lawyer: whatever you ask the value, the lawyer answers on its behalf, and the court only ever hears the lawyer.

saying these in an interview costs you the question

  • Claims a String method already covers structured log output
  • Returns a plain string from LogValue instead of a slog.Value
  • Thinks the placeholder must be applied at every call site
  • Assumes a json:"-" tag hides the field from every handler
  • Believes slog walks a struct's fields looking for LogValuer