Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitstow/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"remote": "hephaestus:/mnt/user/james/gitstow/log",
"last_sync": "0001-01-01T00:00:00Z",
"last_access": "2026-02-26T12:24:31.584375-05:00",
"size_bytes": 794903,
"head_ref": "",
"head_sha": "",
"status": "live"
}
113 changes: 113 additions & 0 deletions console-handler-proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Proposal: Enable Timestamps by Default in log.New()

## Problem

`log.New()` omits timestamps while `log.Default()` includes them. This inconsistency surprises users.

```go
// log.Default() includes timestamps
log.Info("Hello world!")
// Output: 2023/01/23 14:23:45 INFO Hello world!

// log.New() does not
logger := log.New(os.Stderr)
logger.Info("Hello world!")
// Output: INFO Hello world!
```

## Solution

Enable timestamps by default in `log.New()`:

```go
func New(w io.Writer) *Logger {
return NewWithOptions(w, Options{
ReportTimestamp: true,
})
}
```

This uses `DefaultTimeFormat` (`"2006/01/02 15:04:05"`), matching `log.Default()` behavior.

Users can disable via `NewWithOptions()`:

```go
logger := log.NewWithOptions(os.Stderr, log.Options{
ReportTimestamp: false,
})
```

## Impact

**Breaking change**: Existing code using `log.New()` will now include timestamps.

**Benefits**:
- Consistent with `log.Default()`
- Matches user expectations
- Aligns with standard library behavior

## API Compatibility with log/slog

The `*Logger` type already implements `slog.Handler`:

```go
handler := log.NewWithOptions(os.Stderr, log.Options{
ReportTimestamp: true,
Level: log.DebugLevel,
})
logger := slog.New(handler)
```

**For a familiar slog-style API, add:**

```go
// HandlerOptions provides slog-style configuration.
type HandlerOptions struct {
AddSource bool
Level Level
}

// NewTextHandler returns a Handler with text formatting.
// Timestamps are enabled by default.
func NewTextHandler(w io.Writer, opts *HandlerOptions) *Logger {
if opts == nil {
opts = &HandlerOptions{}
}
return NewWithOptions(w, Options{
ReportTimestamp: true,
Formatter: TextFormatter,
Level: opts.Level,
ReportCaller: opts.AddSource,
})
}

// NewJSONHandler returns a Handler with JSON formatting.
// Timestamps are enabled by default.
func NewJSONHandler(w io.Writer, opts *HandlerOptions) *Logger {
if opts == nil {
opts = &HandlerOptions{}
}
return NewWithOptions(w, Options{
ReportTimestamp: true,
Formatter: JSONFormatter,
Level: opts.Level,
ReportCaller: opts.AddSource,
})
}
```

**Usage:**

```go
handler := log.NewTextHandler(os.Stderr, &log.HandlerOptions{
Level: log.DebugLevel,
})
logger := slog.New(handler)
```

## Implementation

- Update `New()` to set `ReportTimestamp: true`
- Add `HandlerOptions`, `NewTextHandler()`, `NewJSONHandler()`
- Update tests
- Document breaking change in release notes
4 changes: 2 additions & 2 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ func TestLogContext_empty(t *testing.T) {
}

func TestLogContext_simple(t *testing.T) {
l := New(io.Discard)
l := NewWithOptions(io.Discard, Options{})
ctx := WithContext(context.Background(), l)
require.Equal(t, l, FromContext(ctx))
}

func TestLogContext_fields(t *testing.T) {
var buf bytes.Buffer
l := New(&buf)
l := NewWithOptions(&buf, Options{})
l.SetLevel(DebugLevel)
ctx := WithContext(context.Background(), l.With("foo", "bar"))
l = FromContext(ctx)
Expand Down
10 changes: 5 additions & 5 deletions json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

func TestJson(t *testing.T) {
var buf bytes.Buffer
l := New(&buf)
l := NewWithOptions(&buf, Options{})
l.SetFormatter(JSONFormatter)
cases := []struct {
name string
Expand Down Expand Up @@ -132,7 +132,7 @@ func TestJson(t *testing.T) {

func TestJsonCaller(t *testing.T) {
var buf bytes.Buffer
l := New(&buf)
l := NewWithOptions(&buf, Options{})
l.SetFormatter(JSONFormatter)
l.SetReportCaller(true)
l.SetLevel(DebugLevel)
Expand Down Expand Up @@ -174,7 +174,7 @@ func TestJsonCaller(t *testing.T) {

func TestJsonTime(t *testing.T) {
var buf bytes.Buffer
logger := New(&buf)
logger := NewWithOptions(&buf, Options{})
logger.SetTimeFunction(_zeroTime)
logger.SetFormatter(JSONFormatter)
logger.SetReportTimestamp(true)
Expand All @@ -184,7 +184,7 @@ func TestJsonTime(t *testing.T) {

func TestJsonPrefix(t *testing.T) {
var buf bytes.Buffer
logger := New(&buf)
logger := NewWithOptions(&buf, Options{})
logger.SetFormatter(JSONFormatter)
logger.SetPrefix("my-prefix")
logger.Info("info")
Expand All @@ -198,7 +198,7 @@ func TestJsonCustomKey(t *testing.T) {
TimestampKey = oldTsKey
}()
TimestampKey = "other-time"
logger := New(&buf)
logger := NewWithOptions(&buf, Options{})
logger.SetTimeFunction(_zeroTime)
logger.SetFormatter(JSONFormatter)
logger.SetReportTimestamp(true)
Expand Down
2 changes: 1 addition & 1 deletion logfmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

func TestLogfmt(t *testing.T) {
var buf bytes.Buffer
l := New(&buf)
l := NewWithOptions(&buf, Options{})
l.SetFormatter(LogfmtFormatter)
cases := []struct {
name string
Expand Down
8 changes: 4 additions & 4 deletions logger_121_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

func TestSlogSimple(t *testing.T) {
var buf bytes.Buffer
h := New(&buf)
h := NewWithOptions(&buf, Options{})
h.SetLevel(DebugLevel)
l := slog.New(h)
cases := []struct {
Expand Down Expand Up @@ -73,7 +73,7 @@ func TestSlogSimple(t *testing.T) {

func TestSlogWith(t *testing.T) {
var buf bytes.Buffer
h := New(&buf)
h := NewWithOptions(&buf, Options{})
h.SetLevel(DebugLevel)
l := slog.New(h).With("a", "b")
cases := []struct {
Expand Down Expand Up @@ -124,7 +124,7 @@ func TestSlogWith(t *testing.T) {

func TestSlogWithGroup(t *testing.T) {
var buf bytes.Buffer
h := New(&buf)
h := NewWithOptions(&buf, Options{})
l := slog.New(h).WithGroup("charm").WithGroup("bracelet")
cases := []struct {
name string
Expand Down Expand Up @@ -175,7 +175,7 @@ func TestSlogCustomLevel(t *testing.T) {
for _, c := range cases {
buf.Reset()
t.Run(c.name, func(t *testing.T) {
l := New(&buf)
l := NewWithOptions(&buf, Options{})
l.SetLevel(c.minLevel)
l.Handle(context.Background(), slog.NewRecord(time.Now(), c.level, "foo", 0))
assert.Equal(t, c.expected, buf.String())
Expand Down
16 changes: 8 additions & 8 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

func TestSubLogger(t *testing.T) {
var buf bytes.Buffer
l := New(&buf)
l := NewWithOptions(&buf, Options{})
cases := []struct {
name string
expected string
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestWrongLevel(t *testing.T) {
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
buf.Reset()
l := New(&buf)
l := NewWithOptions(&buf, Options{})
l.SetLevel(c.level)
l.Info("info")
assert.Equal(t, c.expected, buf.String())
Expand All @@ -90,7 +90,7 @@ func TestWrongLevel(t *testing.T) {

func TestLogFormatter(t *testing.T) {
var buf bytes.Buffer
l := New(&buf)
l := NewWithOptions(&buf, Options{})
l.SetLevel(DebugLevel)
cases := []struct {
name string
Expand Down Expand Up @@ -139,7 +139,7 @@ func TestLogFormatter(t *testing.T) {

func TestEmptyMessage(t *testing.T) {
var buf bytes.Buffer
l := New(&buf)
l := NewWithOptions(&buf, Options{})
cases := []struct {
name string
expected string
Expand Down Expand Up @@ -196,7 +196,7 @@ func TestLogWithPrefix(t *testing.T) {
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
buf.Reset()
l := New(&buf)
l := NewWithOptions(&buf, Options{})
l.SetPrefix(c.prefix)
l.Info(c.msg)
assert.Equal(t, c.expected, buf.String())
Expand All @@ -215,7 +215,7 @@ func TestLogWithRaceCondition(t *testing.T) {
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
l := New(w)
l := NewWithOptions(w, Options{})

var done sync.WaitGroup

Expand Down Expand Up @@ -248,7 +248,7 @@ func TestRace(t *testing.T) {
t.Parallel()

w := io.Discard
l := New(w)
l := NewWithOptions(w, Options{})
for i := 0; i < 100; i++ {
t.Run("race", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -280,7 +280,7 @@ func TestRace(t *testing.T) {
func TestCustomLevel(t *testing.T) {
var buf bytes.Buffer
level500 := Level(500)
l := New(&buf)
l := NewWithOptions(&buf, Options{})
l.SetLevel(level500)
l.Logf(level500, "foo")
assert.Equal(t, "foo\n", buf.String())
Expand Down
37 changes: 36 additions & 1 deletion pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,43 @@ func SetDefault(logger *Logger) {
}

// New returns a new logger with the default options.
// Timestamps are enabled by default.
func New(w io.Writer) *Logger {
return NewWithOptions(w, Options{})
return NewWithOptions(w, Options{ReportTimestamp: true})
}

// HandlerOptions provides slog-style configuration.
type HandlerOptions struct {
AddSource bool
Level Level
}

// NewTextHandler returns a Handler with text formatting.
// Timestamps are enabled by default.
func NewTextHandler(w io.Writer, opts *HandlerOptions) *Logger {
if opts == nil {
opts = &HandlerOptions{}
}
return NewWithOptions(w, Options{
ReportTimestamp: true,
Formatter: TextFormatter,
Level: opts.Level,
ReportCaller: opts.AddSource,
})
}

// NewJSONHandler returns a Handler with JSON formatting.
// Timestamps are enabled by default.
func NewJSONHandler(w io.Writer, opts *HandlerOptions) *Logger {
if opts == nil {
opts = &HandlerOptions{}
}
return NewWithOptions(w, Options{
ReportTimestamp: true,
Formatter: JSONFormatter,
Level: opts.Level,
ReportCaller: opts.AddSource,
})
}

// NewWithOptions returns a new logger using the provided options.
Expand Down
Loading