Hacker News new | ask | show | jobs
by mrweasel 1036 days ago
Admittedly I'm not a huge fan of having:

  slog.Info("hello, world", "user", os.Getenv("USER"))
It's a little magical that "user" is a key. So what if you have multiple key-value pairs? Arguably it most likely going to be obvious which is the keys, but having every other value be a key and the rest values seems a little clumsy.

I really like Pythons approach where you can have user="value" it makes things a bit more clear.

13 comments

I think Uber chose a better approach for their Go logging library called Zap [1]

    logger.Info("failed to fetch URL",
      // Structured context as strongly typed Field values.
      zap.String("url", url),
      zap.Int("attempt", 3),
      zap.Duration("backoff", time.Second),
    )
They also have zap.Error(err) which generates the "error" key as a convention.

[1] https://github.com/uber-go/zap

You can reproduce that example with slog verbatim by changing zap to slog.

For example: https://pkg.go.dev/golang.org/x/exp/slog#example-Group

As mentioned in the article, Zap's SugaredLogger has roughly the same thing https://pkg.go.dev/go.uber.org/zap

    sugar.Infow("failed to fetch URL",
      "url", "http://example.com",
      "attempt", 3,
      "backoff", time.Second,
    )
Really slog presents mostly the same two APIs as Zap, with a single entrypoint.
`[level]w` supports both key/value pairs and individual `zap.Type(key, value)` values combined fwiw, so for that part the API is essentially the same too
Slog allows doing that by creating Attr structs directly.
> It's a little magical that "user" is a key. So what if you have multiple key-value pairs?

You... add them afterwards? It's really just a plist (https://www.gnu.org/software/emacs/manual/html_node/elisp/Pr...), that's hardly novel. The method takes any number of parameters and pairs them up as key, value.

Or if you really hate yourself, you use LogAttrs with explicitly constructed Attr objects.

> I really like Pythons approach where you can have user="value" it makes things a bit more clear.

The trouble is, Go doesn't have keyword parameters so that doesn't work.

It sure has maps though... logrus famously uses `logrus.Fields{"key": "value"}`
And logrus is one of the slowest loggers by far, in part because of its heavy map usage.
It's also the reflect on the value part which slows down things. It could be improved but it's hard to do that without breaking interface. It's so widely used that a major bump wouldn't that useful.
I'd expect it to be mostly called on map literals, at least a compile time constant set of keys. That should be amenable to a targeted compiler optimisation.
It’s not without precedence, for example: https://pkg.go.dev/strings#NewReplacer

I don’t mind it. You can use LogAttrs if you want to be explicit.

Although I do wonder if there’s anything tricky with the type system that is preventing something like this from being supported: https://go.dev/play/p/_YV7sYdnZ5V

I think go loggers have tried to move away from passing log entries as maps for performance reasons.
That seems like a problem that should be solved. Logging structured data is a very basic expectation.
This is structured logging. Stubbornly insisting that "structured logging" === "map" is dumb and ignores a large fraction of use cases where performance matters.
Are there languages that solve the “performance“ problem with maps? In fact, isn’t Go one of them?
In this use case using maps doesn't solve any problem, requires at least one allocation, and requires hashing each key. This is not even an interesting discussion.
Avoiding the map allocation & construction cost is way harder than avoiding the use of a map, like zap and slog.LogAttrs do.
Is ordering of the keys guaranteed to be the same as in the literal?
Order should be preserved up to the Handler. Some Handlers e.g. JSONHandler produce output with key order explicitly undefined.
That’s a good point. I think it would be random without some sorting shenanigans.
You don’t need LogAttrs to pass in Attr entries, it should work fine with normal functions

The reason it doesn’t use maps is that maps are significantly slower, TFA has an entire section on performances.

However if you prefer that interface and don’t mind the performance hit, nothing precludes writing your own Logger (it’s just a façade over the Handler interface) and taking maps.

If you had read more than the introduction you’d have found a paragraph about this in the middle and multiple at the end.
You use attrs [1] instead. I personally find this much more readable, and prefer this.

  slog.Info("hello, world", slog.String("user", os.Getenv("USER")))
[1]: https://pkg.go.dev/log/slog#hdr-Attrs_and_Values
Rust's slog ( https://docs.rs/slog/ ) does:

  use slog::info;
  ...
  info!("hello, world"; "user" => std::env::var("USER"));
It can do that because Rust’s macros can have their own mini language at the top level, and can transform that into whatever data structure they want under the cover.

For better or for worse, Go doesn’t have that.

Also funny story: in Perl “,” can be spelled “=>” specifically for this sort of use cases, when you write

    my %hash = (
        Foo => “bar”,
        Baz => “qux”,
    );
the behaviour is no different than

    my %hash = (
        Foo, “bar”,
        Baz, “qux”,
    );
it’s just that a literal plist in a hash context is interpreted as a hash.

Hence “=>” being called “fat comma” in perl.

Which means you can writer

    my %h = (a => b => c => d);
or

    my %h = (a, b => c, d);

they all mean the same thing.
That all sounds questionable.
Yup! Love the Rust slog crate. Initially thought OP was gonna be about Rust slog, before I saw the domain mention Go.
Yeah, I agree. Passing in an optional `map[string]string` or something would be better, but then you get into having to either pass in `nil` every time you don't have the extra data or needing an entirely different function for with vs without the map
> Passing in an optional `map[string]string` or something would be better

It would definitely not be better from the point of view of

> We wanted slog to be fast.

A better interface/API is really what I meant. The performance characteristics are probably worth the tradeoff.
Passing in a map would require an extra allocation for the map memory for each log line. I think the performance would probably not be great?
it depends. I believe map literals are stack allocated if they aren't shared across goroutines or globals.
While the hmap struct can be stack allocated if it does not escape, I’m pretty sure the buckets still need to be heap-allocated. I do not believe hmap has a “flat structure” variant which could live entirely on the stack, though I could be wrong.
It's most definitely not. Logging is crucial for monitoring services, and making logging statements many times slower will either sink your service or push developers to avoid logging and making the service impossible to debug.

Most log metadata will be attached by libraries and middleware, so service/application devs won't even see most of it.

I think you interpreted my comment the wrong way, I think the performance tradeoffs are worth the mediocre API design. My bad for making it ambiguous
this is not uncommon. this is what I've been dealing with earlier today in Postgres:

  json_build_object ( VARIADIC "any" ) → json

  jsonb_build_object ( VARIADIC "any" ) → jsonb

  Builds a JSON object out of a variadic argument list.
  By convention, the argument list consists of alternating keys and values.
  Key arguments are coerced to text; value arguments are converted as per to_json or to_jsonb.

      json_build_object('foo', 1, 2, row(3,'bar')) → {"foo" : 1, "2" : {"f1":3,"f2":"bar"}}
I'm really used to writing them on new lines.

  json_build_object(
    'foo', 1,
    2, row(3, 'bar')
  )
Plist or named arguments doesn't really make too big of a difference, to my eyes. I do recommend Mapped Diagnostic Context style approaches, if you can do it. Passing all arguments that you think may be useful to logs gets unwieldy quickly. Looks particularly absurd when folks start adding parameters to functions for the sole purpose of letting you log them.
I'm really happy there's a name for this idea. I've been looking for something like this.
Personally I only intend to ever use LogAttrs/AddAttrs/GroupValue, for this reason. I really don't want to count evens and odds.
Indeed. Logging a map-like object seems like a pretty basic expectation. Separate keys and values as parameters seem very C-like.
Today I'm pulling this out of a context object, can I still do that?
The same pattern can be seen in Java using Map.of(...)