Code Monkey home page Code Monkey logo

tint's Introduction

tint: ๐ŸŒˆ slog.Handler that writes tinted logs

Go Reference Go Report Card



Package tint implements a zero-dependency slog.Handler that writes tinted (colorized) logs. Its output format is inspired by the zerolog.ConsoleWriter and slog.TextHandler.

The output format can be customized using Options which is a drop-in replacement for slog.HandlerOptions.

go get github.com/lmittmann/tint

Usage

w := os.Stderr

// create a new logger
logger := slog.New(tint.NewHandler(w, nil))

// set global logger with custom options
slog.SetDefault(slog.New(
    tint.NewHandler(w, &tint.Options{
        Level:      slog.LevelDebug,
        TimeFormat: time.Kitchen,
    }),
))

Customize Attributes

ReplaceAttr can be used to alter or drop attributes. If set, it is called on each non-group attribute before it is logged. See slog.HandlerOptions for details.

// create a new logger that doesn't write the time
w := os.Stderr
logger := slog.New(
    tint.NewHandler(w, &tint.Options{
        ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
            if a.Key == slog.TimeKey && len(groups) == 0 {
                return slog.Attr{}
            }
            return a
        },
    }),
)

Automatically Enable Colors

Colors are enabled by default and can be disabled using the Options.NoColor attribute. To automatically enable colors based on the terminal capabilities, use e.g. the go-isatty package.

w := os.Stderr
logger := slog.New(
    tint.NewHandler(w, &tint.Options{
        NoColor: !isatty.IsTerminal(w.Fd()),
    }),
)

Windows Support

Color support on Windows can be added by using e.g. the go-colorable package.

w := os.Stderr
logger := slog.New(
    tint.NewHandler(colorable.NewColorable(w), nil),
)

tint's People

Contributors

abhinav avatar bersace avatar irridia avatar jylitalo avatar karlmutch avatar kwargs avatar lmittmann avatar telemachus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

tint's Issues

Ignored group values

slog.Group("rpc", slog.String("request", "test"))

5:33PM INF call rpc= duration=161.792ยตs req=

Having an option to keep built-in attributes in the log

Hey, it would be great if there is an option that allows preserving built-in attributes (time, level, msg..) in the log.
So basically, instead of having logs like

2024-06-24T20:29:08Z INF this is a message key_1="1234" key_2=456

The log will looks like below with the new option.

time=2024-06-24T20:29:08Z level=INF msg="this is a message" key_1="1234" key_2=456

Let me know if this makes sense.
Maybe I can provide a PR if this looks ok.

Don't always name the `slog.Any()` of type `error` to `"err`"

For example:

logger.Debug("failed to load config file", slog.Any("the_error", err))

Using default TextHandler from slog the output it:

DEBUG msg="failed to load config file" the_error="open /etc/fhomed/config.toml: no such file or directory"

but using this package the output is:

DBG failed to load config file err="open /etc/fhomed/config.toml: no such file or directory"

I'd like to see the_error instead of err.

Thanks for the package! I like it :)

ReplaceAttrs incompatible with slog

I found two things that tint does different to slog, which makes Options not quite a drop-in replacement for slog.HandlerOptions:

First, slog calls Resolve before ReplaceAttr, as documented:

ReplaceAttr is called to rewrite each non-group attribute before it is logged. The attribute's value has been resolved (see [Value.Resolve]).

tint calls Resolve after ReplaceAttr.

Second, tint does not call ReplaceAttr recursively for groups. With the tint handler, slog.Info("foo", slog.Group("bar", "x", "y")) causes a call to ReplaceAttr with an empty groups argument and slog.GroupValue("bar", "x", "y"). slog's handlers instead call ReplaceAttr with []string{"bar"}, slog.Attr{Key:"x", Value: slog.StringValue("y")}.

The documentation on slog.HandlerOptions.ReplaceAttr reads

ReplaceAttr is never called for Group attributes, only their contents.

The two things together interact surprisingly in slog in Go 1.21.1. With a value that Resolves to a GroupValue, slog does call ReplaceAttr with the Group attribute (and after that once for each member of the group). This is either a bug in the implementation or the docs. I opened golang/go#62731 about this. Maybe keep an eye on that.

Wrong colors somehow

Howdy,

first of all: thanks for the module, very handy!

I've got a little problem though: the colors are somewhat wrong, at least the output doesn't look like the screenshot here.

Sample code as I use the module in my code:

package main

import (
	"io"
	"log/slog"
	"os"

	"github.com/lmittmann/tint"
)

func main() {
	os.Exit(Main(os.Stdout))
}

func Main(w io.Writer) int {
	logLevel := &slog.LevelVar{}

	opts := &tint.Options{
		Level: logLevel,
	}

	logLevel.Set(slog.LevelDebug)

	handler := tint.NewHandler(w, opts)
	debuglogger := slog.New(handler).With(
		slog.Group("program_info",
			slog.Int("pid", os.Getpid()),
		),
	)
	slog.SetDefault(debuglogger)

	slog.Info("info")
	slog.Warn("warn")
	slog.Error("err")
	slog.Debug("debug")

	return 0
}

This how the output looks like:

slog-colors

The error color is correct, the others are just greyscale colors.

Do I something wrong, is there a bug or can I possibly customize the colors?

Thanks in advance,
Tom

err/error field is not colored red

slog.Error("blah", "err", "xd", "error", "xd")
slog.Error("blah", "err", http.ErrNoCookie, "error", http.ErrNoCookie)
slog.Error("blah", slog.Any("err", http.ErrNoCookie), slog.Any("error", http.ErrNoCookie))

results in:

Screenshot 2024-08-11 at 14 17 55

but the screenshot in README demonstrates that err is colored:

img

isatty no longer necessary

For a few years now golang.org/x/term has shipped with a IsTerminal(fd int) method for determining whether you are writing to a terminal. This can help reduce the number of dependencies used in a project.

Sorting attrs?

In my output, similar attrs in different log lines are not printed in a consist order. For example:

Oct  6 12:05:22.593 INF Query sql="-- name: Ping :one\nSELECT $1\n" args="[1]" time=2.085667ms rowCount=1 pid=6185 module=grpc-public traceID=e0be34409ac17877e5f664456f044058
Oct  6 12:05:22.823 INF Query args="[1]" sql="-- name: Ping :one\nSELECT $1\n" rowCount=1 time=1.956472ms pid=6185 traceID=e0be34409ac17877e5f664456f044058 module=grpc-public

This makes human reading of such log lines difficult, especially if many of them are printed continuously and you need to search for a particular attr every time.

Would it be possible to sort attrs as they are printed, so that they're always printed in the same order?

feature request: support for custom message prefix

zerolog.ConsoleWriter has a nice feature to print "caller" at the beginning of a message line. Caller by default is being set to <filename>:<number> (just like the AddSource feature), but the user can easily override it.

Could we introduce a similar capability in tint?

11:00PM INF main > Starting server addr=: 8080 env=production
11:00PM DBG main > Connected to DB db=myapp host=localhost: 5432
11:00PM WRN database > Slow request method=GET path=/users duration=497ms
11:00PM ERR database > DB connection lost err="connection reset" db=myapp

TimeFormat does not appear to take effect

I'm using the example from the Readme. However, it appears TimeFormat is not changing to time.Kitchen.

Aug 26 23:13:57.950 INF reconciling kind=File apiVersion=files.psion.io/v1alpha1
go version go1.21.0 darwin/amd64

Panic when using tint.Err() in goroutine

Hi,

I hit a panic when wrapping a nil error:

	slog.Info("Error", tint.Err(fmt.Errorf("pouet")))
	slog.Info("no error.", tint.Err(nil))
Apr 17 16:08:16.201 INF Error err=pouet
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
        panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x647744]

goroutine 1 [running]:
testing.(*InternalExample).processRunResult(0xc0000d3c18, {0x0, 0x0}, 0x47847e?, 0x0, {0x7b8fe0, 0xacfb70})
        /usr/local/lib/go-1.20/src/testing/example.go:95 +0x64a
testing.runExample.func2()
        /usr/local/lib/go-1.20/src/testing/run_example.go:59 +0x11c
panic({0x7b8fe0, 0xacfb70})
        /usr/local/lib/go-1.20/src/runtime/panic.go:884 +0x213
github.com/lmittmann/tint.(*tintError).Error(0xc0000135f0?)
        <autogenerated>:1 +0x24
github.com/lmittmann/tint.(*handler).appendTintError(0xc0001c6a00, 0xc0000135f0, {0x89bbc0, 0xc0001f0150}, {0x0?, 0x200000003?})
        /home/bersace/.cache/gopath/pkg/mod/github.com/lmittmann/[email protected]/handler.go:356 +0x222
github.com/lmittmann/tint.(*handler).appendAttr(0x4a84a6?, 0xc0000135f0, {{0x80c1c9, 0x3}, {0x0, {0x7ca6a0, 0xc0001f0140}}}, {0x0, 0x0})
        /home/bersace/.cache/gopath/pkg/mod/github.com/lmittmann/[email protected]/handler.go:303 +0x3ea
github.com/lmittmann/tint.(*handler).Handle.func1({{0x80c1c9, 0x3}, {0x0, {0x7ca6a0, 0xc0001f0140}}})
        /home/bersace/.cache/gopath/pkg/mod/github.com/lmittmann/[email protected]/handler.go:197 +0x127
golang.org/x/exp/slog.Record.Attrs({{0xc10772940c0267e9, 0x226c58, 0xaddf40}, {0x80dbea, 0x9}, 0x0, 0x7649b8, {{{0x80c1c9, 0x3}, {0x0, ...}}, ...}, ...}, ...)
        /home/bersace/.cache/gopath/pkg/mod/golang.org/x/[email protected]/slog/record.go:93 +0x9e
github.com/lmittmann/tint.(*handler).Handle(_, {_, _}, {{0xc10772940c0267e9, 0x226c58, 0xaddf40}, {0x80dbea, 0x9}, 0x0, 0x7649b8, ...})
        /home/bersace/.cache/gopath/pkg/mod/github.com/lmittmann/[email protected]/handler.go:193 +0x1105
golang.org/x/exp/slog.(*Logger).log(0xc0001f0100, {0x0, 0x0}, 0x0, {0x80dbea, 0x9}, {0xc0000d3a18, 0x1, 0x1})
        /home/bersace/.cache/gopath/pkg/mod/golang.org/x/[email protected]/slog/logger.go:230 +0x1fc
golang.org/x/exp/slog.Info({0x80dbea?, 0xc0001f0140?}, {0xc0000d3a18?, 0x0?, 0x0?})
        /home/bersace/.cache/gopath/pkg/mod/golang.org/x/[email protected]/slog/logger.go:265 +0x91
github.com/dalibo/ldap2pg/internal/config_test.Example()
        /home/bersace/src/dalibo/ldap2pg/internal/config/my_test.go:23 +0x2f8
testing.runExample({{0x80cdf5, 0x7}, 0x841bb8, {0x0, 0x0}, 0x0})
        /usr/local/lib/go-1.20/src/testing/run_example.go:63 +0x2d0
testing.runExamples(0xc0000d3dd8, {0xad20a0?, 0x1, 0x1?})
        /usr/local/lib/go-1.20/src/testing/example.go:44 +0x17d
testing.(*M).Run(0xc00009f540)
        /usr/local/lib/go-1.20/src/testing/testing.go:1908 +0x6ef
main.main()
        _testmain.go:51 +0x1aa

This is an error in my code. Still, I suggest to gracefully handle such case in appendTintError where err.Error() is called on nil.

What do you think of this ?

Review color and wording

Hi,

This is a matter of taste, but please allow me to open a discussion on the color and formatting of tint.

Here is the output on WezTerm using Twilight (base16) color scheme:

image

Here are some notes:

  • message and attributes values look dimmed.
  • It's hard to distinguish debug line from info lines.

I suggest to use journalctl output as a reference:

  • apply level color to message
  • use journalctl level colors
  • Use DEBUG, INFO , WARN and ERROR level string (note the padding space to always fit in 5 chars).
  • brigthen values

Here is a sample output (just from a simple dumb script) :

image

What do you think of this ?

Thanks for tint, it's a great module.

Regards,

More log levels please

Please consider adding more log levels by default, as I used to have more than one debug levels, just like we can use multiple -v for ssh to get different levels of debug info.

I'm proposing:

const (
	LevelTrace  = slog.Level(-8)
	LevelDbg3   = slog.Level(-7)
	LevelDbg2   = slog.Level(-6)
	LevelDbg1   = slog.Level(-5)
	LevelNotice = slog.Level(2)
	LevelFatal  = slog.Level(12)
)

var LevelNames = map[slog.Leveler]string{
	LevelTrace:  "TRC",
	LevelDbg3:   "D-3",
	LevelDbg2:   "D-2",
	LevelDbg1:   "D-1",
	LevelNotice: "NTC",
	LevelFatal:  "FTL",
}

. . .

		opts := &slog.HandlerOptions{
			Level: LevelTrace,
			ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
				if a.Key == slog.LevelKey {
					level := a.Value.Any().(slog.Level)
					levelLabel, exists := LevelNames[level]
					if !exists {
						levelLabel = level.String()
					}

					a.Value = slog.StringValue(levelLabel)
				}

				return a
			},
		}

		logger := slog.New(slog.NewTextHandler(os.Stdout, opts))

		ctx := context.Background()
		logger.Log(ctx, LevelDbg1, "Hidden debug message level1")
		logger.Log(ctx, LevelDbg2, "Hidden debug message level2")
		logger.Log(ctx, LevelDbg3, "Hidden debug message level3")
		logger.Log(ctx, LevelTrace, "Trace message")
		logger.Log(ctx, LevelNotice, "Notice message")
		logger.Log(ctx, LevelFatal, "Fatal level")

Which would give us:

time=... level=D-1 msg="Hidden debug message level1"
time=... level=D-2 msg="Hidden debug message level2"
time=... level=D-3 msg="Hidden debug message level3"
time=... level=TRC msg="Trace message"
time=... level=NTC msg="Notice message"
time=... level=FTL msg="Fatal level"

NB, I tried to put the above into tint, but got:

panic: interface conversion: interface {} is int64, not slog.Level

at my logger.Log(ctx, LevelDbg1 line.

Ref: https://betterstack.com/community/guides/logging/logging-in-go/#creating-custom-log-levels

Colorized attribute

Hi! I'm trying to add syntax highlighting for my SQL queries in logs.

There is a library that does it pretty well:

package main

import (
	"bytes"
	"os"

	"github.com/alecthomas/chroma/quick"
)

func main() {
	var q bytes.Buffer
	quick.Highlight(&q, "SELECT 1 + 1", "sql", "terminal16m", "monokai")
	os.Stderr.Write(q.Bytes()) // output will be colorized, as expected
}

But when I try to add colorized SQL as slog.Attr, it will print escaped sequence instead:

package main

import (
	"bytes"
	"log/slog"
	"os"
	"time"

	"github.com/alecthomas/chroma/quick"
	"github.com/lmittmann/tint"
)

func main() {
	slog.SetDefault(slog.New(
		tint.NewHandler(os.Stderr, &tint.Options{
			Level:      slog.LevelDebug,
			TimeFormat: time.TimeOnly,
		}),
	))

	var q bytes.Buffer
	quick.Highlight(&q, "SELECT 1 + 1", "sql", "terminal16m", "monokai")

	// prints something like
	// 18:50:29 INF SQL query query="\x1b[38;2;102;217;239mSELECT\x1b[0m\x1b[38;2;248;248;242m \x1b[0m\x1b[38;2;174;129;255m1\x1b[0m\x1b[38;2;24
	slog.Info("SQL query", "query", q.String())
}

I believe this is happening because of strconv.AppendQuote, which you are using to quote strings here. AppendQuote escapes all the colors sequences.

Seems like you're passing "quote=true" everywhere and I cannot disable this behavior using ReplaceAttrs or something like that.

Am I missing something? Is there a way to preserve colors for some attributes? Thanks!

TimeFormat can lead to misaligned output

E.g if I use

logOpts := &tint.Options{
	AddSource:  true,
	Level:      slog.LevelDebug,
	TimeFormat: time.Kitchen + " 05.999",
}

I can end up with logs like:

3:36PM 44.098 DBG labeler/indexer.go:244 indexed issue repo=coder/code-server num=3031
3:36PM 44.35 DBG labeler/indexer.go:244 indexed issue repo=coder/code-server num=3029

One possible solution is accepting the width of the time column via tint.Options. The better solution, I think, is allowing a callback to generate the time string. E.g.:

NowFn: func() string {
	timeFormatted := t.Format(time.Kitchen)
	milliseconds := t.Nanosecond() / 1e6
	return timeFormatted + fmt.Sprintf(" %03d", milliseconds)
} 

Adding a replace attribute function broke feature

I want to add a replace attribute function to handle custom log level & wording (FATAL, TRACE).

I've noticed that adding a ReplaceAttrs functions only handling custom level to string break the source parameters format and level colorization. A weird struct is displayed instead of the colored file:line and the levels are greyed out.

Does that mean that I have the re-implement the wheel if I wish to add more human friendly log level ?

Cheers

Custom log level names with color

It'd be great to be able to customize the log level names and keep the colors.

I think simple package level variables would work for most use cases.

var (
  InfoLevelName  = "INF"
  ErrorLevelName = "ERR"
)

Happy to submit a PR if you agree with the feature.

Skip time, replace level with icon, and some other differences

Hi! This is very cool. I really like how you differentiated the keys from the values. I was trying to figure out how to do something similar while still leaning on the TextHandler/JSONHandler

In my local console, I generally don't care much about the time of the log mesages, so I'd like to skip that field altogether. I chose to color the message per the level, and just use a unicode symbol for the level instead of "INF|DBG|WRN|ERR" string. I was curious about the source file and line number where the log messages came from, but I didn't want it particularly long.

Before I found this, I was making a similar thing inspired by apex/log cli, which is also colorful, but a bit more condensed. I'm not sure if it's interesting, but if you are interested, it looks like this:
Screen Shot 2023-03-26 at 5 56 49 PM
And the source code is here:
https://gist.github.com/StevenACoffman/d51ddab0d8af65e94ef88852f9fc1551

Test TestReplaceAttr fails sometimes

Each Log call internally performs a time.Now() call, so in case enough time passes on the testing machine, them will fail.

imagen

My proposal is call to time.Now on each testcase, and force-set it to the time key of the log params:

imagen

Option to cancel timestamp output

func (opts Options) NewHandler(w io.Writer) slog.Handler {
    // ...
    // if h.timeFormat == "" {
    //     h.timeFormat = defaultTimeFormat
    // }
    // ...
}

func (h *handler) Handle(_ context.Context, r slog.Record) error {
    // ...
    // write time, level, and message
    if h.timeFormat != "" {
        buf.WriteString("\033[2m")
        *buf = r.Time.AppendFormat(*buf, h.timeFormat)
        buf.WriteString("\033[0m ")
    }

Fields names are not printed for C structures

Here is the stock slog output:

slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})))
time=2024-01-05T02:18:08.689+07:00 level=DEBUG msg="XDGSurface SetData" x.data:=0x16e6cc0
time=2024-01-05T02:18:08.729+07:00 level=DEBUG msg=handleMapXDGToplevel topLevel={p:0x1786ba0}
time=2024-01-05T02:18:08.729+07:00 level=DEBUG msg=handleMapXDGToplevel s.topLevelList.Len()=0
time=2024-01-05T02:18:08.729+07:00 level=DEBUG msg=handleMapXDGToplevel s.topLevelList.Len()=1
time=2024-01-05T02:18:08.729+07:00 level=DEBUG msg=focusTopLevel "prev surface:"={p:<nil>}
time=2024-01-05T02:18:08.729+07:00 level=DEBUG msg=focusTopLevel "current surface:"={p:0x18598b0}
time=2024-01-05T02:18:08.729+07:00 level=DEBUG msg="XDGSurface SceneTree()" x.p="&{client:0x17c34c0 resource:0x16e75c0 surface:0x18598b0 link:{prev:0x17c34d8 next:0x17c34d8} role:1 role_resource:0x16e8a90 anon0:[160 107 120 1 0 0 0 0] popups:{prev:0x17c0680 next:0x17c0680} added:true configured:true configure_idle:<nil> scheduled_serial:1 configure_list:{prev:0x17c06a8 next:0x17c06a8} current:{configure_serial:1 geometry:{x:0 y:0 width:540 height:487}} pending:{configure_serial:1 geometry:{x:0 y:0 width:540 height:487}} initialized:true initial_commit:false events:{destroy:{listener_list:{prev:0x1574540 next:0x17c1430}} ping_timeout:{listener_list:{prev:0x17c06f8 next:0x17c06f8}} new_popup:{listener_list:{prev:0x17c0708 next:0x17c0708}} configure:{listener_list:{prev:0x17c0718 next:0x17c0718}} ack_configure:{listener_list:{prev:0x17c0728 next:0x17c0728}}} data:0x16e6cc0 role_resource_destroy:{link:{prev:0x16e8af0 next:0x16e8af0} notify:0x7fd40605ac90}}

This is output by stock tint

	slog.SetDefault(slog.New(
		tint.NewHandler(os.Stderr, &tint.Options{
			Level:      programLevel,
			TimeFormat: time.TimeOnly,
		}),
	))
02:21:46 DBG XDGSurface SetData x.data:=0x2cdc520
02:21:46 DBG handleMapXDGToplevel topLevel={0x2db68c0}
02:21:46 DBG handleMapXDGToplevel s.topLevelList.Len()=0
02:21:46 DBG handleMapXDGToplevel s.topLevelList.Len()=1
02:21:46 DBG focusTopLevel "prev surface:"={<nil>}
02:21:46 DBG focusTopLevel "current surface:"={0x2d7d5a0}
02:21:46 DBG XDGSurface SceneTree() x.p="&{0x2ce6c10 0x2cdc960 0x2d7d5a0 {0x2ce6c28 0x2ce6c28} 1 0x2cec230 [192 104 219 2 0 0 0 0] {0x2de2770 0x2de2770} true true <nil> 1 {0x2de2798 0x2de2798} {1 {0 0 540 487}} {1 {0 0 540 487}} true false {{{0x2b6ae00 0x2ce86d0}} {{0x2de27e8 0x2de27e8}} {{0x2de27f8 0x2de27f8}} {{0x2de2808 0x2de2808}} {{0x2de2818 0x2de2818}}} 0x2cdc520 {{0x2cec290 0x2cec290} 0x7fc9cb193c90}}"
02:21:46 DBG XDGSurface SceneTree() x.p.data=0x2cdc520

see the XDGSurface SceneTree() x.p= log line, in tint output there is no C structure fileds

Disable time

Is there a way to completely hide time from output?

Grafana not correctly identify the log level

Trying a lot of log libs, and @lmittmann yours looks the best. The only problem I have is that Grafana is not correctly reaing the levels of the logs. I am not sure what's the cause of it. Maybe it's the naming? (btw, is there a way to change INF -> INFO wihtout losing the colors?)

image

Bottom ones is another handler, top one is this one. Any ideas?

Suggestion: Option to use color emojis when no color is available

I'm using this package (thank you!) to log in an environment that doesn't support color (Xcode), but where I still want to easily glance at the error types.

Currently I've hacked this functionality into place:

// Injects emojis when standard levels are not colored
type emojiLevelWriter struct {
	w io.Writer
}

func (e *emojiLevelWriter) Write(b []byte) (int, error) {
	s := string(b)
	s = strings.Replace(s, " DBG", " โฌœ๏ธ DBG", 1)
	s = strings.Replace(s, " INF", " ๐ŸŸฉ INF", 1)
	s = strings.Replace(s, " WRN", " ๐ŸŸจ WRN", 1)
	s = strings.Replace(s, " ERR", " ๐ŸŸช๏ธ ERR", 1)
	_, err := e.w.Write([]byte(s))
	return len(s), err
}

// Usage
// (sidenote: github.com/jwalton/go-supportscolor more accurately detects color capabilities than isatty)
h := tint.NewHandler(&emojiLevelWriter{w}, &tint.Options{
	AddSource: false,
	Level:     level,
	NoColor:   !supportscolor.Stderr().SupportsColor,
}),

But it'd be nice if this was built-in functionality -- and it seems aligned with the spirit of this package.
I'd propose the Options struct having an Emoji property (default = false):

type Options struct {
	...

	// Disable color (Default: false)
	NoColor bool

	// Enable emoji with levels (Default: false)
	Emoji bool
}

Alternatively, exposing the level strings somewhere (e.g. as package vars) would also work.

Thoughts?

Enhancement for source attribute

Integrated Development Environments (IDEs) such as GoLand utilize relative file paths for referencing code lines. This functionality enhances the debugging process and facilitates efficient navigation through source files to locate specific lines of interest.
e.g. Watch this video

TintEnhancement.mp4

this make it easy for debugging and and moving through files to find a line you are looking for

currently I'm doing something like this

tintHandler := tint.NewHandler(os.Stderr, &tint.Options{
	AddSource: true,
	ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
		if attr.Key == slog.SourceKey {
			src := attr.Value.Any().(*slog.Source)
			return slog.Attr{
				Key:   attr.Key,
				Value: slog.AnyValue(fmt.Sprintf("%s:%d", path.Base(src.File), src.Line)),
			}
		}
		return attr
	},
})
slog.SetDefault(slog.New(tintHandler))

which logs in file:line format

CustomSourceAttr

We propose modifying this implementation to log source information using relative paths by default, following the format ./relative/filename:lineNumber. Alternatively, we suggest incorporating a mechanism to allow users to specify their preferred source formatting method.

Incorrect work with the log level option

During initialization in the tint.NewHandler, the minimum logging level is copied as a number and then used as an absolute value that cannot be changed.

And in the settings, not just the minimum level is set, but the slog.Leveler interface, which allows you to safely change this value in the future.

var programLevel = new(slog.LevelVar) // Info by default
h := tint.NewHandler(os.Stderr, &tint.HandlerOptions{
    Level: programLevel,
})
slog.SetDefault(slog.New(h))
// ...
programLevel.Set(slog.LevelDebug) // <- DON'T WORK!!!

FYI: https://pkg.go.dev/golang.org/x/exp/slog#hdr-Levels

Accordingly, in the internal handler you also need to replace level slog.Level with slog.Leveler and:

func (h *handler) Enabled(_ context.Context, level slog.Level) bool {
    return level >= h.level.Level()
}

Log level change

Is there any way to change log level after initialized and set as the default logger?

type HandlerWrapper struct {
	TintOpts *tint.Options
	slog.Handler
}

func (h *HandlerWrapper) SetLogLevel(levelStr string) error {
	if slogLevel, ok := h.TintOpts.Level.(*slog.Level); ok {
		if err := slogLevel.UnmarshalText([]byte(levelStr)); err != nil {
			return err
		}
	}

	return nil
}

type SetLeveler interface {
	SetLogLevel(levelStr string) error
}
tintOpts := &tint.Options{
	AddSource:  opt.Caller,
	Level:      &sloglevel,
	TimeFormat: timeFormat(opt.TimeFormat, pretty),
}

logger = slog.New(
	&HandlerWrapper{
		TintOpts: tintOpts,
		Handler: tint.NewHandler(
			opt.Writer,
			tintOpts,
		),
	},
)

And default function

func SetLevel(levelStr string) error {
	if wrapper, ok := slog.Default().Handler().(SetLeveler); ok {
		if err := wrapper.SetLogLevel(levelStr); err != nil {
			return err
		}
	}

	return nil
}

Is it good to have this kind of function directly?
This issue seems problem with slog not the tint handler but I don't know how people doing it.

bug: nested group name was wrong in `Err` attribute

	logger = logger.WithGroup("group0").With("reqId", "xxxx-xxx-xxxx-xxxx")
	logger.WithGroup("group1").Debug("Debug msg", "foo", "bar", "x", "y")
	logger.Info("This is a info msg", "foo", 1, "bar", true)
	logger.Warn("This is a warn msg", "foo", map[string]string{"aa": "bb", "cc": "dd"}, "bar", []string{"a", "b", "c"}, "null response", nil)
	logger.Error("This is a error msg", tint.Err(errors.New("i'm an error")))

the expected output should be group0.group1.err
image

Consideration of an alternative approach when printing structs

Hey there, thanks so much for your work on this handler, it's exactly what I was looking for.

One major difference between slog and zerolog which is reflected both in the slog.TextHandler and the tint handler, is the handling of structs whereby both handlers either use the MarshalText or essentially %v to format structs while also adding quotes if there's a space and escaping interior quotes within the string returned.

Meanwhile, zerolog would log the JSON representation of the struct:

e.g.

2023-05-07 16:37:41 INF Getting extract callback stream entry={"isDir":false,"modTime":"2020-10-25T16:38:06+11:00","path":"WinSpy-1.0.3.7z","size":807471}

This is created used a simple marshal function as follows:

func (a ArchiveEntry) MarshalZerologObject(e *zerolog.Event) {
	e.Str("path", a.Path)
	e.Bool("isDir", a.IsDir)
	e.Uint64("size", a.Size)
	if a.ModTime != nil {
		e.Time("modTime", *a.ModTime)
	}
}

I was wondering whether it may be worth consider an option which uses MarshalJSON instead and avoids the extra escaping and addition of quotes in the resultant output to provide similar output to zerolog?

I realise this would break the convention set out by slog and thus I'd understand if you were not a fan of this idea, but as it stands right now, I still am finding zerolog more practical.

Cheers
Fotis

Incorrect level handler

if h.level < slog.LevelDebug {
    h.level = defaultLevel
}

source code

How can I set the trace logging level (`slog.LevelDebug-4') with such restrictions?

Log files

Hi, i've noticed that it is not possible for tint to have a log file, due to the colors appearing in the log file. This is because io.MultiWriter is used for log.SetOutput, and if used, sends logs to stdout and a log file. Issue with that is colors will be retained and the log file will not look great.

Is it possible for tint to introduce a MultiLogger so that the Stderr can look nice, while the log file remains readable?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.