Skip to content

Conversation

@ashb
Copy link
Contributor

@ashb ashb commented Oct 1, 2025

Add an exported func to be able to set a new level color for Development mode

I also added a UseDefaultLevelColors function to reset it back to the stock
list, mostly for being able to reset it back to stock in the unit tests.

This is fractionally less efficient as it's now a loop rather
than a switch statement that can be unrolledm, but this is unavoidable if we
want it to be controllable by the developer at runtime.

Benchmark results:

BenchmarkLevelFormat/ERROR+5/new-12             12287250                94.46 ns/op
BenchmarkLevelFormat/ERROR+5/old-12             13530858                89.57 ns/op
BenchmarkLevelFormat/ERROR/new-12               100000000               12.95 ns/op
BenchmarkLevelFormat/ERROR/old-12               131831570               10.26 ns/op
BenchmarkLevelFormat/INFO/new-12                100000000               10.07 ns/op
BenchmarkLevelFormat/INFO/old-12                146790718                8.549 ns/op
BenchmarkLevelFormat/INFO+2/new-12              13149951                88.20 ns/op
BenchmarkLevelFormat/INFO+2/old-12              13251370                86.32 ns/op
BenchmarkLevelFormat/DEBUG/new-12               93846536                11.90 ns/op
BenchmarkLevelFormat/DEBUG/old-12               149301160                8.671 ns/op
BenchmarkLevelFormat/DEBUG-4/new-12             12009832                94.50 ns/op
BenchmarkLevelFormat/DEBUG-4/old-12             12207226                92.35 ns/op

(Note that the non-"Pure" levels are much slower anyway as l.String() hits a
fmt.Sprintf path inside slog package)

Here "old" is the current version on main using switch and "new" is the
version in this PR.

The benchmark code (not committed) was this:

func BenchmarkLevelFormat(b *testing.B) {
	for _, lvl := range []slog.Level{
		slog.LevelError + 5,
		slog.LevelError,
		slog.LevelInfo,
		slog.LevelInfo + 2,
		slog.LevelDebug,
		slog.LevelDebug - 4,
	} {
		b.Run(lvl.String(), func(b *testing.B) {
			for _, fn := range []struct {
				name string
				fn   func(buf *buffer.Buffer, l slog.Level)
			}{
				{"new", formatColorLevelValue}, {"old", old},
			} {
				b.Run(fn.name, func(b *testing.B) {
					b.ResetTimer()
					buf := buffer.New()
					defer buf.Free()
					b.ResetTimer()
					for b.Loop() {
						fn.fn(buf, lvl)
					}
				})
			}
		})
	}
}

func old(buf *buffer.Buffer, l slog.Level) {
	switch {
	case l < slog.LevelInfo: // LevelDebug
		buf.WriteString(magenta)
	case l < slog.LevelWarn: // LevelInfo
		buf.WriteString(blue)
	case l < slog.LevelError: // LevelWarn
		buf.WriteString(yellow)
	default: // LevelError
		buf.WriteString(red)
	}
	buf.WriteString(l.String())
	buf.WriteString(reset)
}

@ashb ashb force-pushed the global-log-level-color-configurable branch from d93cf0e to 9a8ba56 Compare October 1, 2025 10:38
…ent mode

I also added a `UseDefaultLevelColors` function to reset it back to the stock
list, mostly for being able to reset it back to stock in the unit tests.

This is _fractionally_ less efficient as it's now a loop rather
than a switch statement that can be unrolledm, but this is unavoidable if we
want it to be controllable by the developer at runtime.

Benchmark results:

```
BenchmarkLevelFormat/ERROR+5/new-12             12287250                94.46 ns/op
BenchmarkLevelFormat/ERROR+5/old-12             13530858                89.57 ns/op
BenchmarkLevelFormat/ERROR/new-12               100000000               12.95 ns/op
BenchmarkLevelFormat/ERROR/old-12               131831570               10.26 ns/op
BenchmarkLevelFormat/INFO/new-12                100000000               10.07 ns/op
BenchmarkLevelFormat/INFO/old-12                146790718                8.549 ns/op
BenchmarkLevelFormat/INFO+2/new-12              13149951                88.20 ns/op
BenchmarkLevelFormat/INFO+2/old-12              13251370                86.32 ns/op
BenchmarkLevelFormat/DEBUG/new-12               93846536                11.90 ns/op
BenchmarkLevelFormat/DEBUG/old-12               149301160                8.671 ns/op
BenchmarkLevelFormat/DEBUG-4/new-12             12009832                94.50 ns/op
BenchmarkLevelFormat/DEBUG-4/old-12             12207226                92.35 ns/op
```

(Note that the non-"Pure" levels are much slower anyway as `l.String()` hits a
fmt.Sprintf path inside slog package)

Here "old" is the current version on main using switch and "new" is the
version in this PR.

The benchmark code (not committed) was this:

```go
func BenchmarkLevelFormat(b *testing.B) {
	for _, lvl := range []slog.Level{
		slog.LevelError + 5,
		slog.LevelError,
		slog.LevelInfo,
		slog.LevelInfo + 2,
		slog.LevelDebug,
		slog.LevelDebug - 4,
	} {
		b.Run(lvl.String(), func(b *testing.B) {
			for _, fn := range []struct {
				name string
				fn   func(buf *buffer.Buffer, l slog.Level)
			}{
				{"new", formatColorLevelValue}, {"old", old},
			} {
				b.Run(fn.name, func(b *testing.B) {
					b.ResetTimer()
					buf := buffer.New()
					defer buf.Free()
					b.ResetTimer()
					for b.Loop() {
						fn.fn(buf, lvl)
					}
				})
			}
		})
	}
}

func old(buf *buffer.Buffer, l slog.Level) {
	switch {
	case l < slog.LevelInfo: // LevelDebug
		buf.WriteString(magenta)
	case l < slog.LevelWarn: // LevelInfo
		buf.WriteString(blue)
	case l < slog.LevelError: // LevelWarn
		buf.WriteString(yellow)
	default: // LevelError
		buf.WriteString(red)
	}
	buf.WriteString(l.String())
	buf.WriteString(reset)
}
```
@ashb ashb force-pushed the global-log-level-color-configurable branch from 9a8ba56 to ab6ef1e Compare October 1, 2025 10:39
@ashb ashb mentioned this pull request Oct 1, 2025
4 tasks
@icefed
Copy link
Owner

icefed commented Oct 3, 2025

@ashb Thank you.

A little performance reduction is acceptable in development mode.

@icefed
Copy link
Owner

icefed commented Oct 3, 2025

Have you considered adding trace level that you need? a LevelStringer in config for some users who want to custom level string, optimize l.String() at will. but, this's another pr.

@icefed icefed merged commit b14adfc into icefed:master Oct 3, 2025
1 check 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.

2 participants