Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: log/slog: add Value.MarshalJSON #63204

Open
jba opened this issue Sep 25, 2023 · 1 comment
Open

proposal: log/slog: add Value.MarshalJSON #63204

jba opened this issue Sep 25, 2023 · 1 comment
Labels
Milestone

Comments

@jba
Copy link
Contributor

jba commented Sep 25, 2023

The encoding/json package marshals a slog.Value as {}. That means if a slog.Value appears inside something that is logged, slog.JSONHandler (and any other handler that outputs JSON and doesn't treat slog.Value specially) will lose its information. (See #62699.)

To fix that, I propose adding

package slog

func (v Value) MarshalJSON() ([]byte, error) {
    return json.Marshal(v.Any())
}
@jba jba added the Proposal label Sep 25, 2023
@gopherbot gopherbot added this to the Proposal milestone Sep 25, 2023
@rliebz
Copy link

rliebz commented Nov 21, 2023

While this would handle a slog.Value in a slice correctly, it unfortunately does not handle a slog.LogValuer in a slice.

Take this demonstration based on the example for using slog.LogValuer to hide secrets in the slog docs:

package main

import (
	"log/slog"
	"os"
)

// A token is a secret value that grants permissions.
type Token string

// LogValue implements slog.LogValuer.
// It avoids revealing the token.
func (Token) LogValue() slog.Value {
	return slog.StringValue("REDACTED_TOKEN")
}

// This example demonstrates a Value that replaces itself
// with an alternative representation to avoid revealing secrets.
func main() {
	t := Token("shhhh!")
	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
	logger.Info("permission granted", "user", "Perry", "token", t)

	tokens := []Token{"keep it secret", "keep it safe"}
	logger.Info("permission revoked", "tokens", tokens)
}

Playground link: https://go.dev/play/p/FBcVYD0patm

I would like to see the secrets staying secret, but the design today does not respect slog.LogValuer in those contexts, leading to unfortunate consequences (a less disastrous version of which I have run into in real application code).

Because of that behavior, I've found that for custom types that need to reliably serialize, implementing slog.LogValuer is not actually sufficient—you also need to implement MarshalJSON. Which in some cases is unfortunate, and in others where there is already a canonical JSON representation that is for whatever reason undesirable for logging, a separate type or manual serialization is required.

I'm not opposed to this proposal, but I'm wondering if there isn't a solution that solves the problem more generally. The ideal behavior IMO would be that for slog-serialized JSON, LogValue() takes precedence over MarshalJSON() at any level of nesting in slices, structs, etc. I'm not sure if there's a simple way to make something like that work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Incoming
Development

No branches or pull requests

3 participants