Live data from Hacker News

Structured logging with slog

go.dev

131–140 of 174 posts

Re: Structured logging with slog

#131

Earlier quoted context omitted.

In the last few months I've realized what I desperately need: a way to wrap an error with a call stack at the point where it enters our code base. This would probably save me on average 20-30 minutes a week. I see this all the time: main.go:141 error: could not transmogrify the thing: a144cd21c48 And then I literally grep the code base to find the error message. That works ~50% of the time, but the other 50%, I see t…

Its flaws or merits aside, when you have no other useful context to add to the error, that's precisely what Errorf is for. func bar() error { err := baz.Transmogrify() return fmt.Errorf("transmogrify: %w", err) } func foo() error { err := bar() return fmt.Errorf("bar: %w", err) } func main() { err := foo() fmt.Printf("foo: %v", err) // foo: bar: transmogrify: not found } There's your callstack, without the cost of ca…

Indeed, our code base is littered with fmt.Errorf("...: %w", err), but that only works if enough places in the code add context. Currently only about 15% of return sites do this.

And I disagree that the cost of carrying around the callstack is something to worry about. Errors are akin to exceptions in C++/Java: no happy path should rely on errors for control flow (except io.EOF, but that won't generate a call stack). They should be rare enough that any cost below about 1ms and 10k is negligible.

Re: Structured logging with slog

#132
post #63

Earlier quoted context omitted.

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.

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.

Re: Structured logging with slog

#133
post #93

Earlier quoted context omitted.

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.

Rust-style error handling works better though - similar to Go, but with the addition of enums such that the precise types of errors which may be encountered can be easily documented in the type system.

Agreed, I’d love if they would take inspiration from Rust and bring that to Go.

Re: Structured logging with slog

#134

Earlier quoted context omitted.

Its flaws or merits aside, when you have no other useful context to add to the error, that's precisely what Errorf is for. func bar() error { err := baz.Transmogrify() return fmt.Errorf("transmogrify: %w", err) } func foo() error { err := bar() return fmt.Errorf("bar: %w", err) } func main() { err := foo() fmt.Printf("foo: %v", err) // foo: bar: transmogrify: not found } There's your callstack, without the cost of ca…

Indeed, our code base is littered with fmt.Errorf("...: %w", err), but that only works if enough places in the code add context. Currently only about 15% of return sites do this. And I disagree that the cost of carrying around the callstack is something to worry about. Errors are akin to exceptions in C++/Java: no happy path should rely on errors for control flow (except io.EOF, but that won't generate a call stack).…

Are you suggesting it's OK if ParseInt failures take 1ms? Or should ParseInt use a different "kind of error" that's not commensurate with the regular error kind?

Do you think most errors look more like ParseInt, or more like sql.Open where 1ms might be acceptable? (Do you think a call stack from the insides of sql.Open would be useful? My experience, mostly not...)

So the stacks should probably only be for "complex errors", and only for frames that happen in code you (hand waving) "care about". Maybe your programs just have far too complex internal error handling?

Re: Structured logging with slog

#135

Earlier quoted context omitted.

In the last few months I've realized what I desperately need: a way to wrap an error with a call stack at the point where it enters our code base. This would probably save me on average 20-30 minutes a week. I see this all the time: main.go:141 error: could not transmogrify the thing: a144cd21c48 And then I literally grep the code base to find the error message. That works ~50% of the time, but the other 50%, I see t…

Its flaws or merits aside, when you have no other useful context to add to the error, that's precisely what Errorf is for. func bar() error { err := baz.Transmogrify() return fmt.Errorf("transmogrify: %w", err) } func foo() error { err := bar() return fmt.Errorf("bar: %w", err) } func main() { err := foo() fmt.Printf("foo: %v", err) // foo: bar: transmogrify: not found } There's your callstack, without the cost of ca…

Nitpicking here, but I prefer a different convention.

  func bar() error {
    err := baz.Transmogrify()
    return fmt.Errorf("bar: %w", err)
  }

  func foo() error {
    err := bar()
    return fmt.Errorf("foo: %w", err)
  }

  func main() {
    err := foo()
    fmt.Printf(err)
    // foo: bar: transmogrify: not found
  }
Also, I tend to skip quite a lot of layers. The (only?) advantage of manual wrapping over stack traces is that a human can leave just 3 wrappings which are deemed sufficient for another human, while stack trace would contain 100 lines of crap.

Re: Structured logging with slog

#136
post #29
post #22

Earlier quoted context omitted.

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

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.

Re: Structured logging with slog

#137

Earlier quoted context omitted.

Its flaws or merits aside, when you have no other useful context to add to the error, that's precisely what Errorf is for. func bar() error { err := baz.Transmogrify() return fmt.Errorf("transmogrify: %w", err) } func foo() error { err := bar() return fmt.Errorf("bar: %w", err) } func main() { err := foo() fmt.Printf("foo: %v", err) // foo: bar: transmogrify: not found } There's your callstack, without the cost of ca…

Nitpicking here, but I prefer a different convention. func bar() error { err := baz.Transmogrify() return fmt.Errorf("bar: %w", err) } func foo() error { err := bar() return fmt.Errorf("foo: %w", err) } func main() { err := foo() fmt.Printf(err) // foo: bar: transmogrify: not found } Also, I tend to skip quite a lot of layers. The (only?) advantage of manual wrapping over stack traces is that a human can leave just 3…

    func bar() error {
        err := baz.Transmogrify()
        return fmt.Errorf("bar: %w", err)
    }
This is broken. If baz.Transmogrify() returns a nil error, bar will return a non-nil error.

Also, annotations like this, which repeat the name of the function, are backwards. The caller knows the function they called, they can include that information if they choose. Annotations should only include information which callers don't have access to, in this case that would be "transmogrify".

The correct version of this code would be something like the following.

    func main() {
        fmt.Printf("err=%v\n", foo())
    }
    
    func foo() error {
        if err := bar(); err != nil {
            return fmt.Errorf("bar: %w", err)
        }
        return nil
    }
    
    func bar() error {
        if err := baz.Transmogrify(); err != nil {
            return fmt.Errorf("transmogrify: %w", err)
        }
        return nil
    }

Re: Structured logging with slog

#138

Earlier quoted context omitted.

Its flaws or merits aside, when you have no other useful context to add to the error, that's precisely what Errorf is for. func bar() error { err := baz.Transmogrify() return fmt.Errorf("transmogrify: %w", err) } func foo() error { err := bar() return fmt.Errorf("bar: %w", err) } func main() { err := foo() fmt.Printf("foo: %v", err) // foo: bar: transmogrify: not found } There's your callstack, without the cost of ca…

Indeed, our code base is littered with fmt.Errorf("...: %w", err), but that only works if enough places in the code add context. Currently only about 15% of return sites do this. And I disagree that the cost of carrying around the callstack is something to worry about. Errors are akin to exceptions in C++/Java: no happy path should rely on errors for control flow (except io.EOF, but that won't generate a call stack).…

> that only works if enough places in the code add context.

It would be a bit odd to not add context, wouldn't it? Same goes for any value. This is not exclusive to errors. If you consider a function which returns T, the T value could equally be hard to trace back if you find you need to determine its call site and someone blindly returned it up the stack. There is nothing special about errors.

While ideally you are returning more context than Errorf allows, indeed, it is a good last resort. If your codebase is littered with blind returns, the good news is that it shouldn't be too hard to create a static analyzer which finds blind returns of the error type and injects the Errorf pattern.

Re: Structured logging with slog

#139

Earlier quoted context omitted.

Its flaws or merits aside, when you have no other useful context to add to the error, that's precisely what Errorf is for. func bar() error { err := baz.Transmogrify() return fmt.Errorf("transmogrify: %w", err) } func foo() error { err := bar() return fmt.Errorf("bar: %w", err) } func main() { err := foo() fmt.Printf("foo: %v", err) // foo: bar: transmogrify: not found } There's your callstack, without the cost of ca…

Indeed, our code base is littered with fmt.Errorf("...: %w", err), but that only works if enough places in the code add context. Currently only about 15% of return sites do this. And I disagree that the cost of carrying around the callstack is something to worry about. Errors are akin to exceptions in C++/Java: no happy path should rely on errors for control flow (except io.EOF, but that won't generate a call stack).…

Every error should be annotated at the call site. fmt.Errorf("...: %w", err) isn't litter, it should be a basic expectation of any code which passes code review.

> Errors are akin to exceptions in C++/Java: no happy path should rely on errors for control flow (except io.EOF, but that won't generate a call stack). They should be rare enough that any cost below about 1ms and 10k is negligible.

This may be true in C++ or Java, but in Go, it is absolutely not the case.

Errors are essential to, and actually the primary driver of, control flow!

Any method or function which is not guaranteed to succeed by the language specification should, generally, return an error. Code which calls such a method or function must always receive and evaluate the returned error.

Happy paths always involve the evaluation and processing of errors received from called methods/functions! Errors are normal, not exceptional.

(Understanding errors as normal rather than exceptional is one of the major things that distinguish junior vs. senior engineers.)

Re: Structured logging with slog

#140

Earlier quoted context omitted.

In the last few months I've realized what I desperately need: a way to wrap an error with a call stack at the point where it enters our code base. This would probably save me on average 20-30 minutes a week. I see this all the time: main.go:141 error: could not transmogrify the thing: a144cd21c48 And then I literally grep the code base to find the error message. That works ~50% of the time, but the other 50%, I see t…

Its flaws or merits aside, when you have no other useful context to add to the error, that's precisely what Errorf is for. func bar() error { err := baz.Transmogrify() return fmt.Errorf("transmogrify: %w", err) } func foo() error { err := bar() return fmt.Errorf("bar: %w", err) } func main() { err := foo() fmt.Printf("foo: %v", err) // foo: bar: transmogrify: not found } There's your callstack, without the cost of ca…

Not exactly -- you should only fmt.Errorf wrap errors which are non-nil.

See my sibling comment: https://news.ycombinator.com/item?id=37234455

Post reply on HN