Skip to content

Add slog hook#1407

Merged
thaJeztah merged 5 commits intosirupsen:masterfrom
flimzy:slog
Mar 9, 2026
Merged

Add slog hook#1407
thaJeztah merged 5 commits intosirupsen:masterfrom
flimzy:slog

Conversation

@flimzy
Copy link
Copy Markdown
Contributor

@flimzy flimzy commented Nov 9, 2023

This addresses half of #1401. The easy half, honestly.

I have an implementation of the other half we've been using in our own project, but it will need some polish before making it public, so I'm submitting this PR now to gauge interest of the project maintainers. If we like this, I'll work on polishing the other half in a later PR.

@flimzy
Copy link
Copy Markdown
Contributor Author

flimzy commented Nov 9, 2023

I see that CI is failing, and probably needs to be updated.

For the tests in this PR to be meaningful, we'll alsoneed to run tests against Go 1.21 (since the new code is behind a build tag).

If you'd like me to create a PR to address any of these, please let me know. I'm quite happy to contribute, but don't want to waste my effort if the package maintainers aren't interested 😉

@thaJeztah
Copy link
Copy Markdown
Collaborator

(did a quick rebase to make CI run)

Copy link
Copy Markdown

@mdelapenya mdelapenya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thaJeztah this LGTM, although I added some minor concerns regarding the usage of a logger Vs a handler. It's basically about not swallowing errors.

@flimzy 👋 Happy to support you here and make progress to eventually merge this into the project.

Copy link
Copy Markdown

@mdelapenya mdelapenya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM thanks for addressing my comments!

I'd only extend the comment regarding why runtime.Callers(3, pcs[:]), but not a strong requirement.

@flimzy
Copy link
Copy Markdown
Contributor Author

flimzy commented Jan 15, 2026

I'd only extend the comment regarding why runtime.Callers(3, pcs[:]), but not a strong requirement.

That was just copied verbatim from the log/slog package's implementation of log(), without considering whether 3 is the correct number of frames to skip... 🤔

@flimzy
Copy link
Copy Markdown
Contributor Author

flimzy commented Jan 15, 2026

I've added a test to validate that we report the proper caller, and updated the skip argument accordingly.

Comment on lines +72 to +78
attrs := make([]any, 0, len(entry.Data))
for k, v := range entry.Data {
attrs = append(attrs, slog.Any(k, v))
}
var pcs [1]uintptr
// skip 8 callers to get to the original logrus caller
runtime.Callers(8, pcs[:])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking if there's some optimisations to be made here; there's also entry.Caller - wondering if that should be used?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh - I was lazy; this is what ChatGPT suggested for this; not entirely sure if the runtime.Callers suggestion is good;

	var pc uintptr
	if entry.Caller != nil {
		pc = entry.Caller.PC
	} else {
		var pcs [1]uintptr
		// Skip runtime.Callers + this function; adjust as needed or make configurable.
		runtime.Callers(2, pcs[:])
		pc = pcs[0]
	}

	r := slog.NewRecord(entry.Time, lvl, entry.Message, pc)

	if len(entry.Data) != 0 {
		attrs := make([]slog.Attr, 0, len(entry.Data))
		for k, v := range entry.Data {
			attrs = append(attrs, slog.Any(k, v))
		}
		r.AddAttrs(attrs...)
	}

	return handler.Handle(ctx, r)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely can do that, but it subtly changes behavior. Perhaps in a good way?

If we use entry.Caller.PC, then to get source= in the final output, both Logrus and the slog handler need to be configured to show sources. With the current implementation, the behavior depends only on slog's configuration.

I'm not sure which is "more correct". I suppose in practice, they'll likely both be enabled or both disabled. So I'll make the change to use entry.Caller.PC unless you feel that's not appropriate :)

Copy link
Copy Markdown

@mdelapenya mdelapenya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines +73 to +76
attrs := make([]any, 0, len(entry.Data))
for k, v := range entry.Data {
attrs = append(attrs, slog.Any(k, v))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I noticed: logrus used to sort its values when printing the log line.
However here the iteration order of the entry.Data hashmap is not deterministic. The resulting values are put into a slice, which slog keeps ordered as is. This means bridged key-value-pairs will always be shuffled, making the log lines hard to read as a human.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that could be done. Though I'd push back on it a bit, as it adds computation to something that should be as lightweight as possible. Though I know that's a difference of philosophy between logrus and log/slog. I guess logrus likes to make logs "nice" at the expense of performance. log/slog aims to make logs very efficient, even if the ergonomics are a bit less than ideal. As an example, when logging JSON with log/slog, it writes the key/value pairs as they are received, rather than buffering them, to reduce allocations and other overhead. One of the results of this is that it means you can actually produce logs with duplicate JSON keys.

Now which philosophy is more appropriate for a feature bridging two worlds? Hard to say. It's a coin toss I guess. But my feeling is that if you want this sort of niceness-over-performance, you're probably not going to be using slog in the first place, so you wouldn't be using this hook. 🤷

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I'd push back on it a bit, as it adds computation to something that should be as lightweight as possible. Though I know that's a difference of philosophy between logrus and log/slog. I guess logrus likes to make logs "nice" at the expense of performance. log/slog aims to make logs very efficient, even if the ergonomics are a bit less than ideal.

I once wrote a similar bridge myself and also did some benchmarks. Interestingly, logrus bridged to slog (with sorting) is still significantly faster than plain logrus.
(at least when I used logrus as a mere facade like slf4j in Java world, which means I have to call SetOutput and SetFormatter and provide no-op implementations to disable logrus' own processing)

Another thing I think is worth considering:
slog has consistent value ordering. It's just that the values are not lexicographically sorted, but by insertion order. Hashmaps, on the other hand, are totally unpredictable in their order. IMHO that's worse than insertion order (but feel free to disagree, just wanted to give some aspects to think about 🙂)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear: I don't have a strong feeling on this. I won't likely be using this feature any longer any way 😉 I had forgotten that this PR even existed until I was pinged to make some changes. I've long since retired logrus from any active projects I'm on.

So whatever direction the maintainers want to go I'm happy with.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new slog hook for logrus that forwards logrus log entries to a Go log/slog logger. It addresses half of issue #1401, providing a bridge for applications transitioning from logrus to slog. The hook maps logrus levels to slog levels, forwards entry data as slog attributes, and supports custom level mapping and caller source information.

Changes:

  • New hooks/slog/slog.go implementing SlogHook with level mapping, field forwarding, and caller propagation
  • New hooks/slog/slog_test.go with tests for default behavior, fields, custom level mapper, error propagation, and source/caller forwarding

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
hooks/slog/slog.go Implements SlogHook struct, NewSlogHook constructor, default level mapping, and Fire/Levels methods for the logrus.Hook interface
hooks/slog/slog_test.go Tests covering default logging, fields, custom level mapper, slog handler errors propagating to stderr, and source location forwarding

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

flimzy and others added 4 commits March 9, 2026 18:03
Co-authored-by: Jonathan Hall <flimzy@flimzy.com>
Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Co-authored-by: Jonathan Hall <flimzy@flimzy.com>
Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Co-authored-by: Jonathan Hall <flimzy@flimzy.com>
Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Co-authored-by: Jonathan Hall <jonathan@jhall.io>
Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
@thaJeztah thaJeztah self-requested a review March 9, 2026 17:16
Co-authored-by: Jonathan Hall <jonathan@jhall.io>
Co-authored-by: Sebastiaan van Stijn <github@gone.nl>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Copy link
Copy Markdown
Collaborator

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think I pleased all the bots enough now 😂

LGTM, thank you!!

@thaJeztah thaJeztah merged commit 5e2a5f0 into sirupsen:master Mar 9, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants