From a7f11dde1e69dff4926223672e802968ac9cef80 Mon Sep 17 00:00:00 2001 From: mattdurham Date: Wed, 1 May 2024 15:36:56 -0400 Subject: [PATCH] Fix handling of slog logs before logging initialized (#707) * add handlers * Add support for handling slog style logging that happens BEFORE the logger is initialized. * Update changelog * unexport deferred handler * clean up and add tests * refactor tests * fix refactor misname * fix test * cleanup changes * Use slog tests. * Remove DeferredHandler from Logger and always use a wrapped deferred handler. * pr feedback * Add more support for logging --- CHANGELOG.md | 3 + internal/alloy/logging/deferred_handler.go | 105 +++++++ .../alloy/logging/deferred_handler_test.go | 267 ++++++++++++++++++ internal/alloy/logging/handler.go | 172 +++++------ internal/alloy/logging/handler_test.go | 64 +++++ internal/alloy/logging/logger.go | 79 +++--- internal/alloycli/cmd_run.go | 2 +- 7 files changed, 573 insertions(+), 119 deletions(-) create mode 100644 internal/alloy/logging/deferred_handler.go create mode 100644 internal/alloy/logging/deferred_handler_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index be30bc06be..1074b3da3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,9 @@ Main (unreleased) every 15 seconds instead of as soon as data was written to the WAL. (@rfratto) +- Imported code using `slog` logging will now not panic and replay correctly when logged before the logging + config block is initialized. (@mattdurham) + ### Other changes - Update `alloy-mixin` to use more specific alert group names (for example, diff --git a/internal/alloy/logging/deferred_handler.go b/internal/alloy/logging/deferred_handler.go new file mode 100644 index 0000000000..1e3f29679b --- /dev/null +++ b/internal/alloy/logging/deferred_handler.go @@ -0,0 +1,105 @@ +package logging + +import ( + "context" + "log/slog" + "sync" +) + +// deferredSlogHandler is used if you are using a slog handler before the logging config block is processed. +type deferredSlogHandler struct { + mut sync.RWMutex + group string + attrs []slog.Attr + children []*deferredSlogHandler + handle slog.Handler + l *Logger +} + +func newDeferredHandler(l *Logger) *deferredSlogHandler { + return &deferredSlogHandler{ + children: make([]*deferredSlogHandler, 0), + l: l, + } +} + +func (d *deferredSlogHandler) Handle(ctx context.Context, r slog.Record) error { + d.mut.RLock() + defer d.mut.RUnlock() + + if d.handle != nil { + return d.handle.Handle(ctx, r) + } + d.l.addRecord(r, d) + return nil +} + +// Enabled reports whether the handler handles records at the given level. +// The handler ignores records whose level is lower. +func (d *deferredSlogHandler) Enabled(ctx context.Context, level slog.Level) bool { + d.mut.RLock() + defer d.mut.RUnlock() + + if d.handle != nil { + return d.handle.Enabled(ctx, level) + } + return true +} + +// WithAttrs returns a new [TextHandler] whose attributes consists +// of h's attributes followed by attrs. +func (d *deferredSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + d.mut.RLock() + defer d.mut.RUnlock() + + if d.handle != nil { + return d.handle.WithAttrs(attrs) + } + + child := &deferredSlogHandler{ + attrs: attrs, + children: make([]*deferredSlogHandler, 0), + l: d.l, + } + d.children = append(d.children, child) + return child +} + +func (d *deferredSlogHandler) WithGroup(name string) slog.Handler { + d.mut.RLock() + defer d.mut.RUnlock() + + if d.handle != nil { + return d.handle.WithGroup(name) + } + + child := &deferredSlogHandler{ + children: make([]*deferredSlogHandler, 0), + group: name, + l: d.l, + } + d.children = append(d.children, child) + return child +} + +// buildHandlers will recursively build actual handlers, this should only be called before replaying once the logging config +// block is set. +func (d *deferredSlogHandler) buildHandlers(parent slog.Handler) { + d.mut.Lock() + defer d.mut.Unlock() + + // Root node will not have attrs or groups. + if parent == nil { + d.handle = d.l.handler + } else { + if d.group != "" { + d.handle = parent.WithGroup(d.group) + } else { + d.handle = parent.WithAttrs(d.attrs) + } + } + for _, child := range d.children { + child.buildHandlers(d.handle) + } + d.children = nil +} diff --git a/internal/alloy/logging/deferred_handler_test.go b/internal/alloy/logging/deferred_handler_test.go new file mode 100644 index 0000000000..45d0ceeaf3 --- /dev/null +++ b/internal/alloy/logging/deferred_handler_test.go @@ -0,0 +1,267 @@ +package logging + +import ( + "bytes" + "context" + "encoding/json" + "github.com/go-kit/log/level" + "github.com/stretchr/testify/require" + "log/slog" + "strings" + "testing" + "testing/slogtest" +) + +func TestDefferredSlogTester(t *testing.T) { + buf := &bytes.Buffer{} + var l *Logger + results := func(t *testing.T) map[string]any { + // Nothing has been written to the byte stream, it only exists in the internal logger buffer + // We need to call l.Update to flush it to the byte stream. + // This does something a bit ugly where it DEPENDS on the var in slogtest.Run, if the behavior of slogtest.Run + // changes this may break the tests. + updateErr := l.Update(Options{ + Level: "debug", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, updateErr) + line := buf.Bytes() + if len(line) == 0 { + return nil + } + var m map[string]any + unmarshalErr := json.Unmarshal(line, &m) + require.NoError(t, unmarshalErr) + // The tests expect time field and not ts. + if _, found := m["ts"]; found { + m[slog.TimeKey] = m["ts"] + delete(m, "ts") + } + // Need to reset the buffer and logger between each test. + l = nil + buf.Reset() + return m + } + + // Had to add some custom logic to handle updated for the deferred tests. + // Also ignore anything that modifies the log line, which are two tests. + slogtest.Run(t, func(t *testing.T) slog.Handler { + var err error + l, err = NewDeferred(buf) + require.NoError(t, err) + return l.Handler() + }, results) +} + +func TestDeferredHandlerWritingToBothLoggers(t *testing.T) { + bb := &bytes.Buffer{} + l, err := NewDeferred(bb) + slogger := slog.New(l.deferredSlog) + require.NoError(t, err) + l.Log("msg", "this should happen before") + slogger.Log(context.Background(), slog.LevelInfo, "this should happen after)") + + err = l.Update(Options{ + Level: "info", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + firstIndex := strings.Index(bb.String(), "this should happen before") + secondIndex := strings.Index(bb.String(), "this should happen after") + require.True(t, firstIndex < secondIndex) +} + +func TestSlogHandle(t *testing.T) { + bb := &bytes.Buffer{} + bbSl := &bytes.Buffer{} + sl, alloy, l := newLoggers(t, bb, bbSl) + logInfo(sl, alloy, "test") + err := l.Update(Options{ + Level: "debug", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + require.True(t, equal(bb, bbSl)) +} + +func TestSlogHandleWithDifferingLevelDeny(t *testing.T) { + bb := &bytes.Buffer{} + bbSl := &bytes.Buffer{} + sl, alloy, l := newLoggers(t, bb, bbSl) + logInfo(sl, alloy, "test") + err := l.Update(Options{ + Level: "warn", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + require.True(t, bb.Len() == 0) +} + +func TestSlogHandleWithDifferingLevelAllow(t *testing.T) { + bb := &bytes.Buffer{} + bbSl := &bytes.Buffer{} + sl, alloy, l := newLoggers(t, bb, bbSl) + logError(sl, alloy, "test") + err := l.Update(Options{ + Level: "warn", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + require.True(t, bb.Len() > 0) +} + +func TestNormalWithDifferingLevelDeny(t *testing.T) { + bb := &bytes.Buffer{} + l, err := newDeferredTest(bb) + require.NoError(t, err) + level.Debug(l).Log("msg", "this should not log") + err = l.Update(Options{ + Level: "error", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + require.True(t, bb.Len() == 0) +} + +func TestNormalWithDifferingLevelAllow(t *testing.T) { + bb := &bytes.Buffer{} + l, err := newDeferredTest(bb) + require.NoError(t, err) + level.Error(l).Log("msg", "this should not log") + err = l.Update(Options{ + Level: "error", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + // Since we write logs at info, but change to error then our logInfo should be clean. + require.True(t, bb.Len() > 0) +} + +func TestDeferredHandler(t *testing.T) { + type testCase struct { + name string + log func(bb *bytes.Buffer, slBB *bytes.Buffer) + } + + var testCases = []testCase{ + { + name: "Single Attr", + log: func(bb *bytes.Buffer, bbSl *bytes.Buffer) { + sl, alloy, l := newLoggers(t, bb, bbSl) + + sl, alloy = withAttrs(sl, alloy, "attr1", "value1") + logInfo(sl, alloy, "test") + err := l.Update(Options{ + Level: "debug", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + }, + }, + { + name: "Attrs Nested", + log: func(bb *bytes.Buffer, bbSl *bytes.Buffer) { + sl, alloy, l := newLoggers(t, bb, bbSl) + sl, alloy = withAttrs(sl, alloy, "attr1", "value1") + sl, alloy = withAttrs(sl, alloy, "nestedattr1", "nestedvalue1") + logInfo(sl, alloy, "test") + err := l.Update(Options{ + Level: "debug", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + }, + }, + { + name: "Group", + log: func(bb *bytes.Buffer, bbSl *bytes.Buffer) { + sl, alloy, l := newLoggers(t, bb, bbSl) + sl, alloy = withGroup(sl, alloy, "gr1") + sl, alloy = withAttrs(sl, alloy, "nestedattr1", "nestedvalue1") + logInfo(sl, alloy, "test") + err := l.Update(Options{ + Level: "debug", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + }, + }, + { + name: "Nested Group", + log: func(bb *bytes.Buffer, bbSl *bytes.Buffer) { + sl, alloy, l := newLoggers(t, bb, bbSl) + sl, alloy = withGroup(sl, alloy, "gr1") + sl, alloy = withGroup(sl, alloy, "gr2") + sl, alloy = withAttrs(sl, alloy, "nestedattr1", "nestedvalue1") + logInfo(sl, alloy, "test") + err := l.Update(Options{ + Level: "debug", + Format: "json", + WriteTo: nil, + }) + require.NoError(t, err) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bb := &bytes.Buffer{} + bbSl := &bytes.Buffer{} + tc.log(bb, bbSl) + require.True(t, equal(bb, bbSl)) + }) + } +} + +func newLoggers(t *testing.T, bb, bbSl *bytes.Buffer) (*slog.Logger, *slog.Logger, *Logger) { + l, err := newDeferredTest(bb) + require.NoError(t, err) + + jsonH := slog.NewJSONHandler(bbSl, &slog.HandlerOptions{ + AddSource: true, + Level: nil, + ReplaceAttr: testReplace, + }) + sl := slog.New(jsonH) + alloy := slog.New(l.deferredSlog) + return sl, alloy, l +} + +func withAttrs(sl *slog.Logger, alloyL *slog.Logger, attrs ...string) (*slog.Logger, *slog.Logger) { + var attrAny []any + for _, a := range attrs { + attrAny = append(attrAny, a) + } + return sl.With(attrAny...), alloyL.With(attrAny...) +} + +func withGroup(sl *slog.Logger, alloyL *slog.Logger, group string) (*slog.Logger, *slog.Logger) { + return sl.WithGroup(group), alloyL.WithGroup(group) +} + +func logInfo(sl *slog.Logger, alloyL *slog.Logger, msg string) { + ctx := context.Background() + sl.Log(ctx, slog.LevelInfo, msg) + alloyL.Log(ctx, slog.LevelInfo, msg) +} + +func logError(sl *slog.Logger, alloyL *slog.Logger, msg string) { + ctx := context.Background() + sl.Log(ctx, slog.LevelError, msg) + alloyL.Log(ctx, slog.LevelError, msg) +} + +func equal(sl *bytes.Buffer, alloy *bytes.Buffer) bool { + return sl.String() == alloy.String() +} diff --git a/internal/alloy/logging/handler.go b/internal/alloy/logging/handler.go index aacfdb37e0..d85f798a5a 100644 --- a/internal/alloy/logging/handler.go +++ b/internal/alloy/logging/handler.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "log/slog" - "slices" "sync" "time" ) @@ -26,12 +25,18 @@ type handler struct { leveler slog.Leveler formatter formatter - attrs []slog.Attr - group []string + nested []nesting mut sync.RWMutex currentFormat Format inner slog.Handler + replacer func(groups []string, a slog.Attr) slog.Attr +} + +// nesting is used since attrs and groups need to be nested in the order they were entered. +type nesting struct { + attrs []slog.Attr + group string } type formatter interface { @@ -75,61 +80,7 @@ func (h *handler) buildHandler() slog.Handler { // Replace attributes with how they were represented in go-kit/log for // consistency. - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - if len(groups) > 0 { - return a - } - - switch a.Key { - case slog.TimeKey: - return slog.Attr{ - Key: "ts", - Value: slog.StringValue(a.Value.Time().UTC().Format(time.RFC3339Nano)), - } - - case slog.SourceKey: - source, ok := a.Value.Any().(*slog.Source) - if !ok { - // The attribute value doesn't match our expected type. This probably - // indicates it's from a usage of go-kit/log that happens to also - // have a field called [slog.SourceKey]. - // - // Return the attribute unmodified. - return a - } - - if source.File == "" && source.Line == 0 { - // Drop attributes with no source information. - return slog.Attr{} - } - - return a - - case slog.MessageKey: - if a.Value.String() == "" { - // Drop empty message keys. - return slog.Attr{} - } - - case slog.LevelKey: - level := a.Value.Any().(slog.Level) - - // Override the value names to match go-kit/log, which would otherwise - // print as all-caps DEBUG/INFO/WARN/ERROR. - switch level { - case slog.LevelDebug: - return slog.Attr{Key: "level", Value: slog.StringValue("debug")} - case slog.LevelInfo: - return slog.Attr{Key: "level", Value: slog.StringValue("info")} - case slog.LevelWarn: - return slog.Attr{Key: "level", Value: slog.StringValue("warn")} - case slog.LevelError: - return slog.Attr{Key: "level", Value: slog.StringValue("error")} - } - } - - return a - }, + ReplaceAttr: h.replacer, } switch expectFormat { @@ -141,7 +92,14 @@ func (h *handler) buildHandler() slog.Handler { panic(fmt.Sprintf("unknown format %v", expectFormat)) } - newHandler = newHandler.WithAttrs(h.attrs) + // Need to replay our groups and attrs in the correct order. + for _, n := range h.nested { + if n.group != "" { + newHandler = newHandler.WithGroup(n.group) + } else { + newHandler = newHandler.WithAttrs(n.attrs) + } + } h.currentFormat = expectFormat h.inner = newHandler @@ -149,46 +107,90 @@ func (h *handler) buildHandler() slog.Handler { } func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler { - newAttrs := slices.Clone(h.attrs) - - if len(h.group) > 0 { - // Deeply nest the attributes under the appropriate group. - groupAttr := slog.Attr{ - Key: h.group[len(h.group)-1], - Value: slog.GroupValue(attrs...), - } - rem := h.group[:len(h.group)-1] - - for len(rem) > 0 { - groupAttr = slog.Attr{ - Key: rem[len(rem)-1], - Value: slog.GroupValue(groupAttr), - } - rem = rem[:len(rem)-1] - } - - newAttrs = append(newAttrs, groupAttr) - } else { - newAttrs = append(newAttrs, attrs...) - } + newNest := make([]nesting, 0, len(h.nested)+1) + newNest = append(newNest, h.nested...) + newNest = append(newNest, nesting{ + attrs: attrs, + }) return &handler{ w: h.w, leveler: h.leveler, formatter: h.formatter, - attrs: newAttrs, - group: h.group, + nested: newNest, + replacer: h.replacer, } } func (h *handler) WithGroup(name string) slog.Handler { + newNest := make([]nesting, 0, len(h.nested)+1) + newNest = append(newNest, h.nested...) + newNest = append(newNest, nesting{ + group: name, + }) return &handler{ w: h.w, leveler: h.leveler, formatter: h.formatter, - attrs: h.attrs, - group: append(slices.Clone(h.group), name), + nested: newNest, + replacer: h.replacer, + } +} + +func replace(groups []string, a slog.Attr) slog.Attr { + if len(groups) > 0 { + return a } + + switch a.Key { + case slog.TimeKey: + return slog.Attr{ + Key: "ts", + Value: slog.StringValue(a.Value.Time().UTC().Format(time.RFC3339Nano)), + } + + case slog.SourceKey: + source, ok := a.Value.Any().(*slog.Source) + if !ok { + // The attribute value doesn't match our expected type. This probably + // indicates it's from a usage of go-kit/log that happens to also + // have a field called [slog.SourceKey]. + // + // Return the attribute unmodified. + return a + } + + if source.File == "" && source.Line == 0 { + // Drop attributes with no source information. + return slog.Attr{} + } + + return a + + case slog.MessageKey: + if a.Value.String() == "" { + // Drop empty message keys. + return slog.Attr{} + } + + case slog.LevelKey: + level := a.Value.Any().(slog.Level) + + // Override the value names to match go-kit/log, which would otherwise + // print as all-caps DEBUG/INFO/WARN/ERROR. + switch level { + case slog.LevelDebug: + return slog.Attr{Key: "level", Value: slog.StringValue("debug")} + case slog.LevelInfo: + return slog.Attr{Key: "level", Value: slog.StringValue("info")} + case slog.LevelWarn: + return slog.Attr{Key: "level", Value: slog.StringValue("warn")} + case slog.LevelError: + return slog.Attr{Key: "level", Value: slog.StringValue("error")} + } + } + + return a } diff --git a/internal/alloy/logging/handler_test.go b/internal/alloy/logging/handler_test.go index a8d076cfb3..43fee94724 100644 --- a/internal/alloy/logging/handler_test.go +++ b/internal/alloy/logging/handler_test.go @@ -3,9 +3,11 @@ package logging import ( "bytes" "context" + "encoding/json" "io" "log/slog" "testing" + "testing/slogtest" "time" "github.com/stretchr/testify/require" @@ -43,6 +45,35 @@ func TestGroups(t *testing.T) { require.Equal(t, expect, buf.String()) } +func TestSlogTester(t *testing.T) { + var buf bytes.Buffer + l, err := New(&buf, Options{ + Level: "debug", + Format: "json", + }) + require.NoError(t, err) + results := func() []map[string]any { + var ms []map[string]any + for _, line := range bytes.Split(buf.Bytes(), []byte{'\n'}) { + if len(line) == 0 { + continue + } + var m map[string]any + unmarshalErr := json.Unmarshal(line, &m) + require.NoError(t, unmarshalErr) + // The tests expect time field and not ts. + if _, found := m["ts"]; found { + m[slog.TimeKey] = m["ts"] + delete(m, "ts") + } + ms = append(ms, m) + } + return ms + } + err = slogtest.TestHandler(l.handler, results) + require.NoError(t, err) +} + func newTestRecord(msg string) slog.Record { return slog.NewRecord(time.Time{}, slog.LevelInfo, msg, 0) } @@ -58,3 +89,36 @@ func getTestHandler(t *testing.T, w io.Writer) slog.Handler { return l.handler } + +// testReplace is used for unit tests so we can ensure the time and source fields are consistent. +func testReplace(groups []string, a slog.Attr) slog.Attr { + ra := replace(groups, a) + switch a.Key { + case "ts": + fallthrough + case "time": + return slog.Attr{ + Key: "ts", + Value: slog.StringValue("2024-04-29T18:26:21.37723798Z"), + } + case "source": + return slog.Attr{ + Key: "source", + Value: slog.StringValue("test_source"), + } + default: + return ra + } +} + +// newDeferredTest creates a new logger with the default log level and format. Used for tests. +// The logger is not updated during initialization. +func newDeferredTest(w io.Writer) (*Logger, error) { + l, err := NewDeferred(w) + if err != nil { + return nil, err + } + l.handler.replacer = testReplace + + return l, nil +} diff --git a/internal/alloy/logging/logger.go b/internal/alloy/logging/logger.go index b5b9aa8ba9..7416bf7fad 100644 --- a/internal/alloy/logging/logger.go +++ b/internal/alloy/logging/logger.go @@ -24,13 +24,14 @@ type Logger struct { inner io.Writer // Writer passed to New. bufferMut sync.RWMutex - buffer [][]interface{} // Store logs before correctly determine the log format + buffer []*bufferedItem // Store logs before correctly determine the log format hasLogFormat bool // Confirmation whether log format has been determined - level *slog.LevelVar // Current configured level. - format *formatVar // Current configured format. - writer *writerVar // Current configured multiwriter (inner + write_to). - handler *handler // Handler which handles logs. + level *slog.LevelVar // Current configured level. + format *formatVar // Current configured format. + writer *writerVar // Current configured multiwriter (inner + write_to). + handler *handler // Handler which handles logs. + deferredSlog *deferredSlogHandler // This handles deferred logging for slog. } var _ EnabledAware = (*Logger)(nil) @@ -42,29 +43,11 @@ func (l *Logger) Enabled(ctx context.Context, level slog.Level) bool { // New creates a New logger with the default log level and format. func New(w io.Writer, o Options) (*Logger, error) { - var ( - leveler slog.LevelVar - format formatVar - writer writerVar - ) - - l := &Logger{ - inner: w, - - buffer: [][]interface{}{}, - hasLogFormat: false, - - level: &leveler, - format: &format, - writer: &writer, - handler: &handler{ - w: &writer, - leveler: &leveler, - formatter: &format, - }, + l, err := NewDeferred(w) + if err != nil { + return nil, err } - - if err := l.Update(o); err != nil { + if err = l.Update(o); err != nil { return nil, err } @@ -83,7 +66,7 @@ func NewDeferred(w io.Writer) (*Logger, error) { l := &Logger{ inner: w, - buffer: [][]interface{}{}, + buffer: []*bufferedItem{}, hasLogFormat: false, level: &leveler, @@ -93,15 +76,17 @@ func NewDeferred(w io.Writer) (*Logger, error) { w: &writer, leveler: &leveler, formatter: &format, + replacer: replace, }, } + l.deferredSlog = newDeferredHandler(l) return l, nil } // Handler returns a [slog.Handler]. The returned Handler remains valid if l is // updated. -func (l *Logger) Handler() slog.Handler { return l.handler } +func (l *Logger) Handler() slog.Handler { return l.deferredSlog } // Update re-configures the options used for the logger. func (l *Logger) Update(o Options) error { @@ -124,11 +109,23 @@ func (l *Logger) Update(o Options) error { } l.writer.Set(newWriter) + // Build all our deferred handlers + if l.deferredSlog != nil { + l.deferredSlog.buildHandlers(nil) + } // Print out the buffered logs since we determined the log format already for _, bufferedLogChunk := range l.buffer { - // the buffered logs are currently only sent to the standard output - // because the components with the receivers are not running yet - slogadapter.GoKit(l.handler).Log(bufferedLogChunk...) + if len(bufferedLogChunk.kvps) > 0 { + // the buffered logs are currently only sent to the standard output + // because the components with the receivers are not running yet + slogadapter.GoKit(l.handler).Log(bufferedLogChunk.kvps...) + } else { + // We now can check to see if if our buffered log is at the right level. + if bufferedLogChunk.handler.Enabled(context.Background(), bufferedLogChunk.record.Level) { + // These will always be valid due to the build handlers call above. + bufferedLogChunk.handler.Handle(context.Background(), bufferedLogChunk.record) + } + } } l.buffer = nil @@ -144,7 +141,7 @@ func (l *Logger) Log(kvps ...interface{}) error { l.bufferMut.Lock() // Check hasLogFormat again; could have changed since the unlock. if !l.hasLogFormat { - l.buffer = append(l.buffer, kvps) + l.buffer = append(l.buffer, &bufferedItem{kvps: kvps}) l.bufferMut.Unlock() return nil } @@ -158,6 +155,16 @@ func (l *Logger) Log(kvps ...interface{}) error { return slogadapter.GoKit(l.handler).Log(kvps...) } +func (l *Logger) addRecord(r slog.Record, df *deferredSlogHandler) { + l.bufferMut.Lock() + defer l.bufferMut.Unlock() + + l.buffer = append(l.buffer, &bufferedItem{ + record: r, + handler: df, + }) +} + type lokiWriter struct { f []loki.LogsReceiver } @@ -226,3 +233,9 @@ func (w *writerVar) Write(p []byte) (n int, err error) { return w.w.Write(p) } + +type bufferedItem struct { + kvps []interface{} + handler *deferredSlogHandler + record slog.Record +} diff --git a/internal/alloycli/cmd_run.go b/internal/alloycli/cmd_run.go index bc81018e35..f6970da38f 100644 --- a/internal/alloycli/cmd_run.go +++ b/internal/alloycli/cmd_run.go @@ -197,7 +197,7 @@ func (fr *alloyRun) Run(configPath string) error { // Set the memory limit, this will honor GOMEMLIMIT if set // If there is a cgroup will follow that if fr.minStability.Permits(featuregate.StabilityPublicPreview) { - memlimit.SetGoMemLimitWithOpts(memlimit.WithLogger(slog.Default())) + memlimit.SetGoMemLimitWithOpts(memlimit.WithLogger(slog.New(l.Handler()))) } // Enable the profiling.