Skip to content

Commit 6d888c7

Browse files
Add timestamp to console logs
It looks like we dropped this out of the zap logs, way back in #69. @emcfarlane correctly ported the lack of a timestamp over to slog in #3844. This probably made sense when `buf` was running fairly quick CLI commands, but now that we have various server processes running from `buf`, it makes sense to add a timestamp for the various logs being emitted. I specifically wanted this when debugging `buf lsp serve`. We _could_ just make this optional (only for certain commands), but this feels fairly uncontroversial.
1 parent 5781e7d commit 6d888c7

File tree

2 files changed

+29
-8
lines changed

2 files changed

+29
-8
lines changed

private/pkg/slogapp/console.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"log/slog"
2323
"os"
2424
"sync"
25+
"time"
2526

2627
"github.com/mattn/go-colorable"
2728
"golang.org/x/term"
@@ -132,6 +133,10 @@ func (c *consoleHandler) Handle(ctx context.Context, r slog.Record) error {
132133
c.lock.Lock()
133134
defer c.lock.Unlock()
134135
c.buffer.Reset()
136+
if !r.Time.IsZero() {
137+
c.buffer.WriteString(r.Time.Format(time.RFC3339))
138+
c.buffer.WriteString(consoleSeparator)
139+
}
135140
if c.enableColor {
136141
c.buffer.WriteString(colorize(r.Level.String(), getColor(r.Level)))
137142
} else {

private/pkg/slogapp/console_test.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os"
2323
"strings"
2424
"testing"
25+
"time"
2526

2627
"github.com/stretchr/testify/assert"
2728
"github.com/stretchr/testify/require"
@@ -44,7 +45,7 @@ func TestIsWriterTTY(t *testing.T) {
4445
func TestConsoleLogOutput(t *testing.T) {
4546
t.Parallel()
4647

47-
testConsolLogOutput(t, func(logger *slog.Logger) {
48+
testConsoleLogOutput(t, func(logger *slog.Logger) {
4849
logger.Info("hello", slog.String("a", "b"))
4950
logger.Info("hello world")
5051
}, []map[string]any{{
@@ -56,7 +57,7 @@ func TestConsoleLogOutput(t *testing.T) {
5657
slog.MessageKey: "hello world",
5758
}}, withConsoleColor(true))
5859

59-
testConsolLogOutput(t, func(logger *slog.Logger) {
60+
testConsoleLogOutput(t, func(logger *slog.Logger) {
6061
logger.Info("info", slog.String("a", "b"))
6162
logger.Error("error")
6263
}, []map[string]any{{
@@ -68,7 +69,7 @@ func TestConsoleLogOutput(t *testing.T) {
6869
slog.MessageKey: "error",
6970
}})
7071

71-
testConsolLogOutput(t, func(logger *slog.Logger) {
72+
testConsoleLogOutput(t, func(logger *slog.Logger) {
7273
logger = logger.With(slog.String("a", "b"))
7374
logger = logger.WithGroup("g")
7475
logger.Error("error message", slog.String("c", "d"))
@@ -87,7 +88,7 @@ func TestConsoleLogOutput(t *testing.T) {
8788
"a": "b",
8889
}}, withConsoleColor(true))
8990

90-
testConsolLogOutput(t, func(logger *slog.Logger) {
91+
testConsoleLogOutput(t, func(logger *slog.Logger) {
9192
logger.Info("key spaces", slog.String("a key", "with spaces"))
9293
}, []map[string]any{{
9394
slog.LevelKey: colorize("INFO", getColor(slog.LevelInfo)),
@@ -96,7 +97,7 @@ func TestConsoleLogOutput(t *testing.T) {
9697
}}, withConsoleColor(true))
9798
}
9899

99-
func testConsolLogOutput(t *testing.T, run func(logger *slog.Logger), expects []map[string]any, options ...consoleHandlerOption) {
100+
func testConsoleLogOutput(t *testing.T, run func(logger *slog.Logger), expects []map[string]any, options ...consoleHandlerOption) {
100101
t.Helper()
101102
var buf bytes.Buffer
102103
consoleHandler := newConsoleHandler(&buf, slog.LevelInfo, options...)
@@ -118,16 +119,31 @@ func testConsolLogOutput(t *testing.T, run func(logger *slog.Logger), expects []
118119
require.Equal(t, len(expects), len(outputs))
119120
for i := range len(outputs) {
120121
output, expect := outputs[i], expects[i]
122+
// Verify the timestamp is present and parseable, then remove it before
123+
// comparing the rest of the output (timestamps are dynamic).
124+
ts, ok := output[slog.TimeKey]
125+
if assert.True(t, ok, "expected timestamp in log output") {
126+
timestamp, ok := ts.(string)
127+
assert.True(t, ok, "expected timestamp to be string type in log output")
128+
_, err := time.Parse(time.RFC3339, timestamp)
129+
assert.NoError(t, err, "timestamp should be valid RFC3339")
130+
}
131+
delete(output, slog.TimeKey)
121132
assert.Equal(t, expect, output)
122133
}
123134
}
124135

125-
// testParseLogLine passes the output of a single log line.
136+
// testParseLogLine parses the output of a single log line.
137+
// The format is: TIME\tLEVEL\tMESSAGE\t{...JSON attrs...}\n
126138
func testParseLogLine(lineBytes []byte) (map[string]any, error) {
127139
top := map[string]any{}
128140
line := string(bytes.TrimSpace(lineBytes))
129-
index, line, _ := strings.Cut(line, consoleSeparator)
130-
top[slog.LevelKey] = index
141+
// First field is the timestamp.
142+
timestamp, line, _ := strings.Cut(line, consoleSeparator)
143+
top[slog.TimeKey] = timestamp
144+
// Second field is the level.
145+
level, line, _ := strings.Cut(line, consoleSeparator)
146+
top[slog.LevelKey] = level
131147
if len(line) == 0 {
132148
return top, nil
133149
}

0 commit comments

Comments
 (0)