-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Add slog.Handler writing to logrus.Logger #1553
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
Merged
+594
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| package slog | ||
|
|
||
| import ( | ||
| "context" | ||
| "log/slog" | ||
| "maps" | ||
| "slices" | ||
| "strings" | ||
|
|
||
| "github.com/sirupsen/logrus" | ||
| ) | ||
|
|
||
| // Handler is a [slog.Handler] that writes records to a [logrus.Logger]. | ||
| // | ||
| // It is intended for bridging libraries or application code that log via slog | ||
| // into an existing Logrus logger, for example during a gradual migration. | ||
| // | ||
| // By default, slog levels map to their corresponding Logrus levels. Trace, | ||
| // Fatal, and Panic use custom slog levels below Debug and above Error, | ||
| // respectively, preserving their relative severity: | ||
| // | ||
| // - [slog.LevelDebug] - 4 -> [logrus.TraceLevel] | ||
| // - [slog.LevelDebug] -> [logrus.DebugLevel] | ||
| // - [slog.LevelInfo] -> [logrus.InfoLevel] | ||
| // - [slog.LevelWarn] -> [logrus.WarnLevel] | ||
| // - [slog.LevelError] -> [logrus.ErrorLevel] | ||
| // - [slog.LevelError] + 2 -> [logrus.FatalLevel] | ||
| // - [slog.LevelError] + 4 -> [logrus.PanicLevel] | ||
| // | ||
| // Levels between these boundaries map to the next lower Logrus severity. | ||
| // Levels below Debug map to [logrus.TraceLevel], and levels above Panic map | ||
| // to [logrus.PanicLevel]. | ||
| // | ||
| // Mapping to [logrus.FatalLevel] or [logrus.PanicLevel] preserves the level | ||
| // only; handling a record does not exit or panic. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // logger := logrus.New() | ||
| // slog.SetDefault(slog.New(NewHandler(logger))) | ||
| // slog.Info("hello", "key", "value") | ||
| type Handler struct { | ||
| logger *logrus.Logger | ||
|
|
||
| // LevelMapper maps slog levels to Logrus levels. If nil, the default | ||
| // mapping is used. Set it to customize level mapping, for example to map | ||
| // custom slog levels to specific Logrus levels. | ||
| LevelMapper func(slog.Level) logrus.Level | ||
|
|
||
| // fields holds attributes from prior WithAttrs calls, already resolved | ||
| // under the group prefix that applied when they were added. | ||
| fields logrus.Fields | ||
|
|
||
| // groups is the current group name stack for attributes added later. | ||
| groups []string | ||
| } | ||
|
|
||
| var _ slog.Handler = (*Handler)(nil) | ||
|
|
||
| // NewHandler creates a [slog.Handler] that writes to the provided | ||
| // [logrus.Logger]. | ||
| // | ||
| // The provided logger must not be nil. NewHandler panics if logger is nil. | ||
| func NewHandler(logger *logrus.Logger) *Handler { | ||
| if logger == nil { | ||
| panic("cannot create handler from nil logger") | ||
| } | ||
| return &Handler{ | ||
| logger: logger, | ||
| } | ||
| } | ||
|
|
||
| // toLogrusLevel maps a slog level using LevelMapper or the default mapping. | ||
| func (h *Handler) toLogrusLevel(level slog.Level) logrus.Level { | ||
| if h.LevelMapper != nil { | ||
| return h.LevelMapper(level) | ||
| } | ||
| switch { | ||
| case level >= slogLevelPanic: | ||
| return logrus.PanicLevel | ||
| case level >= slogLevelFatal: | ||
| return logrus.FatalLevel | ||
| case level >= slogLevelError: | ||
| return logrus.ErrorLevel | ||
| case level >= slogLevelWarn: | ||
| return logrus.WarnLevel | ||
| case level >= slogLevelInfo: | ||
| return logrus.InfoLevel | ||
| case level >= slogLevelDebug: | ||
| return logrus.DebugLevel | ||
| case level >= slogLevelTrace: | ||
| return logrus.TraceLevel | ||
| default: | ||
| return logrus.TraceLevel | ||
| } | ||
| } | ||
|
|
||
| // Enabled reports whether the handler handles records at the given level. | ||
| // It maps the slog level to a logrus level and consults the underlying logger. | ||
| func (h *Handler) Enabled(_ context.Context, level slog.Level) bool { | ||
| return h.logger.IsLevelEnabled(h.toLogrusLevel(level)) | ||
| } | ||
|
|
||
| // Handle converts the slog record into a logrus entry and logs it. | ||
| // Record time, context, message, and attributes (including those from | ||
| // [Handler.WithAttrs]/[Handler.WithGroup]) are preserved. Attributes are | ||
| // attached as logrus fields; group names are joined with "." as a key prefix | ||
| // (similar to [slog.TextHandler]). | ||
| func (h *Handler) Handle(ctx context.Context, record slog.Record) error { | ||
| level := h.toLogrusLevel(record.Level) | ||
| if !h.logger.IsLevelEnabled(level) { | ||
| return nil | ||
| } | ||
|
|
||
| entry := &logrus.Entry{ | ||
| Logger: h.logger, | ||
| Data: h.fields, | ||
| Time: record.Time, | ||
| Context: ctx, | ||
| } | ||
|
|
||
| if n := record.NumAttrs(); n > 0 { | ||
| // Clone before mutating the handler's fields. | ||
| entry.Data = maps.Clone(h.fields) | ||
| if entry.Data == nil { | ||
| entry.Data = make(logrus.Fields, n) | ||
| } | ||
| record.Attrs(func(a slog.Attr) bool { | ||
| appendAttr(entry.Data, h.groups, a) | ||
| return true | ||
| }) | ||
| } | ||
|
|
||
| entry.Log(level, record.Message) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll probably pay the price of duplicating the entry 2 - 3 times here (each entry := &logrus.Entry{
Logger: h.logger,
Data: fields,
Time: record.Time,
Context: ctx,
}
entry.Log(level, record.Message) |
||
| return nil | ||
| } | ||
|
|
||
| // WithAttrs returns a new Handler whose attributes consist of h's attributes | ||
| // followed by attrs. | ||
| func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { | ||
| if len(attrs) == 0 { | ||
| return h | ||
| } | ||
| h2 := h.clone() | ||
| h2.fields = maps.Clone(h.fields) | ||
| if h2.fields == nil { | ||
| h2.fields = make(logrus.Fields, len(attrs)) | ||
| } | ||
| for _, a := range attrs { | ||
| appendAttr(h2.fields, h.groups, a) | ||
| } | ||
| return h2 | ||
| } | ||
|
|
||
| // WithGroup returns a new Handler with a group appended to the receiver's | ||
| // existing groups. All attributes added to the returned handler (via WithAttrs | ||
| // or Handle) are nested under the combined group names. | ||
| // If name is empty, WithGroup returns h unchanged. | ||
| func (h *Handler) WithGroup(name string) slog.Handler { | ||
| if name == "" { | ||
| return h | ||
| } | ||
| h2 := h.clone() | ||
| h2.groups = append(slices.Clip(h.groups), name) | ||
| return h2 | ||
| } | ||
|
|
||
| // clone returns a shallow copy of h. Callers must clone fields or groups | ||
| // before modifying them. | ||
| func (h *Handler) clone() *Handler { | ||
| return &Handler{ | ||
| logger: h.logger, | ||
| LevelMapper: h.LevelMapper, | ||
| fields: h.fields, | ||
| groups: h.groups, | ||
| } | ||
| } | ||
|
|
||
| // appendAttr adds attr to fields, resolving it and applying group prefixes. | ||
| // Group attributes are flattened with "."-separated keys. | ||
| func appendAttr(fields logrus.Fields, groups []string, attr slog.Attr) { | ||
| attr.Value = attr.Value.Resolve() | ||
| if attr.Equal(slog.Attr{}) { | ||
| return | ||
| } | ||
| if attr.Value.Kind() == slog.KindGroup { | ||
| gs := attr.Value.Group() | ||
| if len(gs) == 0 { | ||
| return | ||
| } | ||
| g := groups | ||
| if attr.Key != "" { | ||
| g = append(slices.Clip(groups), attr.Key) | ||
| } | ||
| for _, a := range gs { | ||
| appendAttr(fields, g, a) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| key := attr.Key | ||
| if len(groups) > 0 { | ||
| key = strings.Join(groups, ".") | ||
| if attr.Key != "" { | ||
| key += "." + attr.Key | ||
| } | ||
| } | ||
| fields[key] = attr.Value.Any() | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the extra processing needed to do the conversion, perhaps it's worth taking advantage of the level here and return early if the log-level is not enabled;