[HN Gopher] Structured logging with slog
       ___________________________________________________________________
        
       Structured logging with slog
        
       Author : spacey
       Score  : 178 points
       Date   : 2023-08-22 16:02 UTC (6 hours ago)
        
 (HTM) web link (go.dev)
 (TXT) w3m dump (go.dev)
        
       | __loam wrote:
       | Slog is an amazing name
        
       | corytheboyd wrote:
       | Structured logging is such an easy to gain, massive improvement
       | to observability. Assuming you can pay for the log processor to
       | make sense of it all :)
       | 
       | I've been working on a side project to bring something like the
       | DataDog log explorer to the local development environment. The
       | prototype I made has already been extremely helpful in debugging
       | issues in a very complex async ball of Rails code. Does something
       | like that sound useful to other folks? Does it already exist and
       | I just can't find it?
        
         | merightnow wrote:
         | Have you checked lnav?
        
           | corytheboyd wrote:
           | I've seen it mentioned before, but I haven't given it a demo
           | yet. To be honest I wasn't sure if TUI was the right UI for
           | something like this, as it can't pull off all of the things a
           | GUI can. It's not that I have a TUI allergy either (I can't
           | live without lazygit).
        
       | Femolo wrote:
       | Log everything as Json!
       | 
       | We need to start making Json viewer were we look into log files
       | without tooling the default.
       | 
       | It's so much easier to use Json automatically and ship them to
       | systems out of the box.
       | 
       | Linux logging should do that too. Not just container in k8s
        
       | tastysandwich wrote:
       | I'm really glad they've introduced this, I just wish it also had
       | the traditional formatting methods, eg Infof, Debugf, Errorf,
       | etc, for backwards compatibility.
       | 
       | I've got a few packages that accept a basic logger interface, eg:
       | type debugLogger interface {           Debugf(format string, args
       | ...any)       }            type MyThing struct {         logger
       | debugLogger       }            func New(logger debugLogger)
       | *MyThing {         return &MyThing{logger}       }
       | 
       | I'd love to switch to slog but I'll have to v2 these packages
       | now.
        
         | crdrost wrote:
         | I would defend slog's decision there. Infof/Debugf/Errorf are
         | like _fine_ especially when I 'm making a little CLI tool for
         | myself, but my main consumption of logs at work is other
         | peoples' logs via a cloud log aggregator, and so when you give
         | other devs who are not me Sprintf they start to do things like
         | "[%s] default/%v - %v" or so, which makes sense to them but
         | doesn't give me great strings to search for when I'm trying to
         | figure out "what were all of the situations in which this
         | strange thing happened".
         | 
         | It's like when you're trying to internationalize, you want to
         | emit as constant of a string as reasonably practical, so that
         | it can be straightforwardly matched and substituted into a
         | different language. Except in this case that different language
         | is regexes being used to change the thing into a SQL statement
         | to fix the mess (or whatever).
         | 
         | So much easier to say "stop trying to Sprintf your logs, just
         | add the values as key-value pairs at the end of the function
         | call."
        
       | mi_lk wrote:
       | Q: what do people use for structured logging in Java?
        
         | monknomo wrote:
         | Logback has structured logging. I think log4j has it as well
        
       | ar_lan wrote:
       | I'm so happy this is a stdlib feature. This is good enough for me
       | to not need to bring in external loggers (e.g. zerolog) which is
       | nice, and I strongly think that structured logging should be the
       | default logging format.
       | 
       | Logs are data.
        
       | zknill wrote:
       | The new structured logging library is a great addition, its nice
       | to have structured logging in the standard lib.
       | 
       | It's easy to get started with log/slog and one of the built in
       | handlers, but as soon as you want to change something the library
       | design pushes you towards implementing an entire handler.
       | 
       | For example, if I want the built in JSON format, but with a
       | different formatting of the Time field, that's not easy to do.
       | It's not obvious how to change the built in handler.
       | 
       | I wrote slogmw[1] to solve this problem. It's a set of middleware
       | and examples that make it easy to make small changes to the built
       | in handlers without having to write a whole new handler from
       | scratch.
       | 
       | [1] https://github.com/zknill/slogmw
        
         | numbsafari wrote:
         | Nice middleware package.
         | 
         | I have to admit, the `log.InfoContext(ctx,...` style of
         | redundancy that permeates the standard lib at this point is
         | really gross, especially given that the most common use case
         | for go is going to have contexts everywhere.
        
           | yashap wrote:
           | Go's decision to not support function overloading leads to a
           | tonne of really ugly APIs. Obviously every decision in
           | language design is a tradeoff, but IMO they made the wrong
           | call here.
        
       | mihaitodor wrote:
       | Benthos just adopted it:
       | https://github.com/benthosdev/benthos/commit/ee0000450413ad3...
        
         | icholy wrote:
         | I might have misunderstood, but shouldn't the adapter be
         | operating on a `slog.Handler`?
        
           | mihaitodor wrote:
           | slog's top-level functions use the default logger, so using
           | that made the most sense for now. There are some custom
           | labels being injected (see the `WithFields()` method) but
           | that's about it.
        
       | baz00 wrote:
       | Now all we need is the 1,000,000 other components in the multiple
       | ecosystems to log in the same format and I won't have a perpetual
       | headache.
       | 
       | Good job Go though for being opinionated but rational.
        
       | candiddevmike wrote:
       | Looking for advice: logging for servers/services tends to be
       | different than logging for CLI-based applications. How do folks
       | differentiate them or use slog for them in a generic way? Or does
       | it make sense to have separate logging packages for CLI vs
       | services? CLI tends to be more verbose and procedural vs
       | servers/service based logging which is more errors only unless
       | debug.
        
         | zknill wrote:
         | Assuming you are logging from some package that's shared over a
         | CLI app and some webservice app; log/slog expects you to setup
         | the slog logger with a specific handler. This handler controls
         | _how_ events are written out, the format of them etc.
         | 
         | If you want to use slog, I can imagine setting up the logger
         | with a handler specific for the CLI output in the CLI tool, and
         | a json or text structured handler in the webservice app.
         | 
         | The quesiton is, do you actually want structured logging in the
         | CLI app? Yes you probably want to print something out, but is
         | it _structured_ in the sense that slog expects? Or is it just
         | some output.
         | 
         | If it's not really structured, then you probably want some
         | other interface/library that better represents the logging you
         | want to do. Slog will push you towards structured key-value
         | pairs, and you might find yourself fighting against this in the
         | CLI app.
        
           | candiddevmike wrote:
           | It seems like I could write an output handler for the CLI app
           | that is more terminal-appropriate. I'd like to abstract
           | logging for functions shared by both (especially debug logs).
           | Today I have combined the functions of logging and
           | tracing/sampling into one package/function call as they are
           | equivalent in my eyes.
        
       | kusha wrote:
       | Oof. We just converted all of our logging to zap[0] to get
       | structured JSON logging for downstream parsing. Wonder how the
       | perf stacks up.
       | 
       | [0]: https://github.com/uber-go/zap
        
         | tony_cannistra wrote:
         | It looks like they've included slog in their performance
         | benchmarks, which show zap as considerably more performant
         | (though I don't really understand the benchmark).
        
       | geodel wrote:
       | With this another most requested feature is covered by Go. This
       | leaves error handling, enum type which are often asked by users
       | but are not actively being worked on for now.
        
         | earthboundkid wrote:
         | An iterator type is being actively worked on now. After that
         | presumably the missing data types in the standard library will
         | be filled out (set, deque, a usable heap, whatever other
         | algorithms). After that, who knows. Maybe native bigints?
         | 
         | I don't really see the enum thing happened. Is lack of enums a
         | real problem? Theoretically, it would be convenient, but I
         | can't say that I see bugs caused by its lack.
        
           | howinteresting wrote:
           | Enums with associated values are a very basic data modeling
           | primitive. Writing code without them is like doing arithmetic
           | with only the multiplication sign, not the plus sign.
        
           | LambdaComplex wrote:
           | Having "type MyType int" and defining a bunch of constants
           | isn't a great replacement for enums. Yeah, it "works," but it
           | still lets the developer forget to check for a possible
           | variant, or you could have an underlying int that doesn't
           | correspond to a valid variant.
           | 
           | The addition of enums would move all these runtime checks to
           | compile time.
        
             | [deleted]
        
             | euroderf wrote:
             | As a workaround, a list in a DB combined with a foreign key
             | constraint. Ugly, yes.
        
         | jmarchello wrote:
         | The lack of Error Handling in Go is a feature, not a bug. See
         | here: https://go.dev/doc/faq#exceptions. I think I'd be
         | disappointed if Try/Catch ever made their way into the
         | language.
        
           | progbits wrote:
           | Error handling != exceptions.
           | 
           | Step one would bee sum types, so only valid value space can
           | be represented (return value or error, but not both or
           | neither).
        
             | jmarchello wrote:
             | Good point, a poor assumption on my part
        
           | geodel wrote:
           | I am not demanding either of these things. Just making
           | general comment based on observing folks.
        
           | dinkleberg wrote:
           | Yeah I have to agree that the Go-style error handling does
           | actually lead to better code. At least when I write it. It
           | makes me think through how I am going to handle error states
           | rather than chucking it in try/except in Python and hoping
           | nothing breaks lol.
        
             | [deleted]
        
       | dgb23 wrote:
       | The rationale:
       | 
       | > With many structured logging packages to choose from, large
       | programs will often end up including more than one through their
       | dependencies. The main program might have to configure each of
       | these logging packages so that the log output is consistent: it
       | all goes to the same place, in the same format. By including
       | structured logging in the standard library, we can provide a
       | common framework that all the other structured logging packages
       | can share.
       | 
       | This is IMO the right way of doing it. Provide an interface with
       | simple defaults, usable out of the box. Those who need more can
       | use a library that builds towards the interface.
       | 
       | So when evaluating any library, you can ask "How well does this
       | integrate with interfaces in the standard library?". Discovering
       | that some functionality is just a "Fooer" that pieces well
       | together with existing stuff is calming. Not only do you already
       | know how to "Foo", you also get a hidden stability benefit:
       | There's an implied API surface contract here.
       | 
       | This is in stark contrast to the "builds on top of" approach,
       | where you end up with competing, idiosyncratic interfaces. This
       | is often necessary, but there is always an implied risk in terms
       | of maintenance and compatibility.
        
         | gdprrrr wrote:
         | Rust seems to do find with a de-facto logging library, and the
         | Java ecosystem seems to have converged on a common API, but
         | with a lot of effort I think.
        
           | jjice wrote:
           | PHP did so with PSR-3 as well https://www.php-
           | fig.org/psr/psr-3/.
        
         | ljm wrote:
         | Something like this would be a welcome addition to Ruby/Rails
         | where you have to pull in dependencies that patch the multiple
         | independent loggers in the stack, some of which break messages
         | onto multiple lines, not to mention the common case of
         | inconsistent tagged and structural logging in your application
         | code.
         | 
         | It's a lot of effort when all you want is to log everything to
         | STDOUT, in JSON, but you have to choose one of half a dozen
         | logging libraries that all behave extremely differently.
        
       | nwsm wrote:
       | It's nice to have this in the standard library, but it doesn't
       | solve any existing pain points around structured log metadata and
       | contexts. We use zap [0] and store a zap logger on the request
       | context which allows different parts of the request pipeline to
       | log with things like tenantId, traceId, and correlationId
       | automatically appended. But getting a logger off the context is
       | annoying, leads to inconsistent logging practices, and creates a
       | logger dependency throughout most of our Go code.
       | 
       | [0] https://github.com/uber-go/zap
        
         | zknill wrote:
         | log/slog package essentially delegates writing log messages to
         | some "handler" interface. The key method is:
         | Handle(context.Context, Record) error
         | 
         | This method has access to the context, which means you can get
         | the logging handler to extract values from the context. Instead
         | of storing the logger on the context, you can extract the
         | traceId, etc values from the context and log those.
         | 
         | It's a little bit involved to write a whole logger from
         | scratch, but you can 'wrap' the existing logger handlers and
         | include values from the context relatively easily.
         | 
         | There are examples in this project (which aims to help solve
         | your usecase): https://github.com/zknill/slogmw
        
           | ryandotsmith wrote:
           | Here's an example of extracting context values in a custom
           | slog handler:
           | 
           | https://github.com/indexsupply/x/blob/main/wslog/slog_test.g.
           | ..
        
       | politician wrote:
       | I wish there was a better approach for the problem of avoiding
       | function calls when the log level at runtime is higher than the
       | call site.
       | 
       | So,                   slog.Info("failed to frob", "thing",
       | GetThing(1))
       | 
       | Still calls GetThing(1) when the log level is greater than Info.
       | The only solution right now for this is to test the log level
       | before making the logging call. It would be amazing if language
       | designers could make the arguments late bound instead or used
       | aspect-oriented programming approaches to protect each logging
       | call site.
        
         | jeffbee wrote:
         | There are other languages of course where the logging avoids
         | this, even in libraries written by the same company that writes
         | Go. In C++, the Abseil logging library (f.k.a. glog) will not
         | evaluate a condition for a disabled log level.
         | LOG(INFO) << WowExpensiveFunction();
         | 
         | This is safe when the log level is set to WARN or higher. For
         | the same reasons, LOG_EVERY_N and LOG_FIRST_N in the same
         | library are pretty cheap.
        
       | bobbyi wrote:
       | Walking past an eatery with outdoor seating, I overheard one
       | diner say the phrase "process raw logs" and I thought, "wow, I
       | guess that is one of those tricky problems that basically
       | everyone ends up dealing with".
       | 
       | And then I heard "... with a chainsaw. It's a chainsaw mill" and
       | realized I may have misunderstood the context.
        
         | [deleted]
        
       | mrweasel wrote:
       | 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.
        
         | candiddevmike wrote:
         | Today I'm pulling this out of a context object, can I still do
         | that?
        
         | taeric wrote:
         | 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.
        
           | nerdponx wrote:
           | I'm really happy there's a name for this idea. I've been
           | looking for something like this.
        
         | jupp0r wrote:
         | 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
        
           | masklinn wrote:
           | Slog allows doing that by creating Attr structs directly.
        
           | bpicolo wrote:
           | 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.
        
         | grose wrote:
         | 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
        
           | gdprrrr wrote:
           | Is ordering of the keys guaranteed to be the same as in the
           | literal?
        
             | grose wrote:
             | That's a good point. I think it would be random without
             | some sorting shenanigans.
        
           | ben0x539 wrote:
           | I think go loggers have tried to move away from passing log
           | entries as maps for performance reasons.
        
         | eska wrote:
         | If you had read more than the introduction you'd have found a
         | paragraph about this in the middle and multiple at the end.
        
         | richardwhiuk wrote:
         | Rust's slog ( https://docs.rs/slog/ ) does:                 use
         | slog::info;       ...       info!("hello, world"; "user" =>
         | std::env::var("USER"));
        
         | baq wrote:
         | 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"}}
        
         | packetlost wrote:
         | 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
        
           | masklinn wrote:
           | > 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.
        
             | packetlost wrote:
             | A better interface/API is really what I meant. The
             | performance characteristics are probably worth the
             | tradeoff.
        
               | returningfory2 wrote:
               | 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?
        
               | packetlost wrote:
               | it depends. I _believe_ map literals are stack allocated
               | if they aren 't shared across goroutines or globals.
        
         | hamandcheese wrote:
         | The same pattern can be seen in Java using Map.of(...)
        
         | masklinn wrote:
         | > 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://w
         | ww.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.
        
           | johnmaguire wrote:
           | It sure has maps though... logrus famously uses
           | `logrus.Fields{"key": "value"}`
        
             | morelisp wrote:
             | And logrus is one of the slowest loggers by far, in part
             | because of its heavy map usage.
        
       | insanitybit wrote:
       | Structured logging is a very sane default. Even if you end up
       | with `{"msg": "blah blah blah"}` at least you have room to grow
       | in the future.
        
         | duskwuff wrote:
         | And if your current logging is along the lines of
         | log.Printf("failed to frob %s: %s", thing, error)
         | 
         | then moving from that to:                   slog.Error("failed
         | to frob", "thing", thing, "error", error)
         | 
         | isn't terribly difficult, and will make log analysis
         | dramatically easier.
        
       ___________________________________________________________________
       (page generated 2023-08-22 23:00 UTC)