{"id":20355884,"url":"https://github.com/hedzr/logg","last_synced_at":"2026-02-14T02:30:09.367Z","repository":{"id":214826401,"uuid":"737456799","full_name":"hedzr/logg","owner":"hedzr","description":"colored logger with log/slog like api","archived":false,"fork":false,"pushed_at":"2025-06-29T02:15:58.000Z","size":2695,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-29T03:38:43.599Z","etag":null,"topics":["color-logging","golang","logging","logging-library","slog","slog-handler","structured-logging"],"latest_commit_sha":null,"homepage":"https://docs.hedzr.com/docs/logg/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hedzr.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-12-31T05:13:03.000Z","updated_at":"2025-06-29T02:15:56.000Z","dependencies_parsed_at":"2024-01-16T03:52:34.843Z","dependency_job_id":"5ecb11c0-e6d0-4299-a020-3b7201a6419e","html_url":"https://github.com/hedzr/logg","commit_stats":null,"previous_names":["hedzr/logg"],"tags_count":53,"template":false,"template_full_name":null,"purl":"pkg:github/hedzr/logg","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Flogg","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Flogg/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Flogg/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Flogg/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hedzr","download_url":"https://codeload.github.com/hedzr/logg/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hedzr%2Flogg/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263502339,"owners_count":23476570,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["color-logging","golang","logging","logging-library","slog","slog-handler","structured-logging"],"created_at":"2024-11-14T23:14:23.150Z","updated_at":"2026-02-14T02:30:09.345Z","avatar_url":"https://github.com/hedzr.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# hedzr/logg\n\n![Go](https://github.com/hedzr/logg/workflows/Go/badge.svg)\n![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/hedzr/logg)\n[![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/hedzr/logg.svg?label=release)](https://github.com/hedzr/logg/releases)\n[![Go Dev](https://img.shields.io/badge/go-dev-green)](https://pkg.go.dev/github.com/hedzr/logg)\n[![Go Report Card](https://goreportcard.com/badge/github.com/hedzr/logg)](https://goreportcard.com/report/github.com/hedzr/logg)\n[![Coverage Status](https://coveralls.io/repos/github/hedzr/logg/badge.svg?branch=master\u0026.9)](https://coveralls.io/github/hedzr/logg?branch=master)\n[![deps.dev](https://img.shields.io/badge/deps-dev-green)](https://deps.dev/go/github.com%2Fhedzr%2Flogg)\n\nA golang logging library with log/slog style APIs, with colorful console output.\n\n## Features\n\nThe abilities are:\n\n- fast enough: performance is not our unique aim, but quick enough.\n- colorful console output by default.\n- switch to logging format (eg. color, logfmt or json) dynamically.\n- interfaces and abilities similar with `log/slog`.\n- adapted into `log/slog` to enable color logging out of the box. note that some our verbs (such as Fatal, Panic) can't work at this time.\n- manage a cascade of child loggers and dump attrs of parent recursively optionally (need enable `LattrsR`).\n- super lightweight child loggers.\n- user-defined levels, writers, and value stringers.\n- privacy enough: hardening filepath(s), shortening package name(s) (by `Lprivacypath` and `Lprivacypathregexp`); and implementing `LogObjectMarshaller` or `LogArrayMarshaller` to safe guard the sensitive fields.\n- better multiline outputs.\n\nSince v0.8.60, we need go toolchain 1.24(.5) and above.\nSince v0.8.0, we need go toolchain 1.23 and above.\n\nSince v0.7.3, a locking version of printOut added. It would give more safeties to splitted writer (if your writer had implemented `LevelSettable` interface to compliant with logg/slog's log level).\n\n![image-20231107091609707](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/uPic/image-20231107091609707.png)\n\n### Changes\n\nSee also [CHANGELOG](CHANGELOG).\n\n\u003e Since v1 (soon), the streaming calls changed their behaviour: `.WithXXX` will make a new instance as a child logger and apply the settings; `.SetXXX` will take the effects on the place.\n\u003e \u003e NOTE: v0.7.x is pre-release version of v1.\n\u003e\n\u003e Since v0.5.7, `logg/slog` enables privacy hardening flags by default now.\n\u003e\n\u003e As to v0.8.53, `logg/slog` renders the text without truncating the leading spaces by default.\n\u003e And we don't give a way to roll back this feature currently.\n\u003e\n\u003e As to v0.8.55, the top level f-functions (such as `Debugf`) can be used. In the earlier release we had added similar f-functions to `Entry`, now they are satisfactory.\n\u003e In this release, we also upgraded golang toolchain to 1.24.5.\n\n## Motivation\n\nAs an opt-in copy of `log/slog`, we provide an out-of-the-box colored text outputting logger with more verbs (fatal, trace, verbose, ...).\n\nAnd an auto-optimized Verb: `Verbose(msg, args...)` would print logging contents only if build tag `verbose` defined.\nThe point is it will be optimized completely in a default build.\n\n## Guide\n\n### Basics\n\n`logg/slog` has package-level functions for logging: `Info`, `Error`, and so on. They will be mapped to the builtin default logger (can be accessed by `Default()`).\n\n`Default()` returns the default logger and `SetDefult()` replaces it.\n\nThe basic usages are:\n\n```go\npackage main\n\nimport (\n \"context\"\n \"errors\"\n \"fmt\"\n \"log/slog\"\n \"os\"\n \"sync\"\n \"time\"\n\n \"github.com/hedzr/is\"\n \"github.com/hedzr/is/basics\"\n \"github.com/hedzr/is/term\"\n \"github.com/hedzr/is/term/color\"\n\n logz \"github.com/hedzr/logg/slog\"\n)\n\nfunc main() {\n defer basics.Close()\n\n ctx, cancel := context.WithCancel(context.Background())\n defer cancel()\n\n testIs(ctx)\n testLogz(ctx)\n\n catcher := is.Signals().Catch()\n catcher.\n  WithPrompt(\"Press CTRL-C to quit...\").\n  WithOnLoopFunc(dbStarter, cacheStarter, mqStarter).\n  WithOnSignalCaught(func(sig os.Signal, wg *sync.WaitGroup) {\n    println()\n    slog.Info(\"signal caught\", \"sig\", sig)\n    cancel() // cancel user's loop, see \u003c-ctx.Done() in Wait(...)\n  }).\n  WaitFor(func(closer func()) {\n    slog.Debug(\"entering looper's loop...\")\n    defer closer()\n    // to terminate this app after a while automatically:\n    time.Sleep(10 * time.Second)\n    // stopChan \u003c- os.Interrupt\n    \u003c-ctx.Done() // waiting for main program stop\n  })\n}\n\nfunc testIs(ctx context.Context) {\n is.RegisterStateGetter(\"custom\", func() bool { return is.InVscodeTerminal() })\n\n println(\"state.InTesting:   \", is.InTesting())\n println(\"state.in-testing:  \", is.State(\"in-testing\"))\n println(\"state.custom:      \", is.State(\"custom\")) // detects a state with custom detector\n println(\"env.GetDebugLevel: \", is.Env().GetDebugLevel())\n if is.InDebugMode() {\n  slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, \u0026slog.HandlerOptions{AddSource: true, Level: slog.LevelDebug})))\n }\n\n fmt.Printf(\"\\n%v\", color.GetCPT().Translate(`\u003ccode\u003ecode\u003c/code\u003e | \u003ckbd\u003eCTRL\u003c/kbd\u003e\n  \u003cb\u003ebold / strong / em\u003c/b\u003e\n  \u003ci\u003eitalic / cite\u003c/i\u003e\n  \u003cu\u003eunderline\u003c/u\u003e\n  \u003cmark\u003einverse mark\u003c/mark\u003e\n  \u003cdel\u003estrike / del \u003c/del\u003e\n  \u003cfont color=\"green\"\u003egreen text\u003c/font\u003e\n`, color.FgDefault))\n\n println(\"term.IsTerminal:               \", term.IsTerminal(int(os.Stdout.Fd())))\n println(\"term.IsAnsiEscaped:            \", term.IsAnsiEscaped(color.GetCPT().Translate(`\u003ccode\u003ecode\u003c/code\u003e`, color.FgDefault)))\n println(\"term.IsCharDevice(stdout):     \", term.IsCharDevice(os.Stdout))\n rows, cols, err := term.GetFdSize(os.Stdout.Fd())\n println(\"term.GetFdSize(stdout):        \", rows, cols, err)\n rows, cols, err = term.GetTtySizeByFd(os.Stdout.Fd())\n println(\"term.GetTtySizeByFd(stdout):   \", rows, cols, err)\n rows, cols, err = term.GetTtySizeByFile(os.Stdout)\n println(\"term.GetTtySizeByFile(stdout): \", rows, cols, err)\n println(\"term.IsStartupByDoubleClick:   \", term.IsStartupByDoubleClick())\n\n logz.InfoContext(ctx, \"pre-forecasting\") // by default this line shall not be displayed since logz (logg/slog) is in WarnLevel.\n}\n\nfunc testLogz(ctx context.Context) {\n logz.SetLevel(logz.DebugLevel)\n\n logz.InfoContext(ctx, \"Hello, world!\")\n\n logz.InfoContext(ctx, \"Hello\", \"target\", \"world\")\n logz.Default().SetColorMode(false)\n logz.InfoContext(ctx, \"Hello\", \"target\", \"world\")\n logz.Default().SetJSONMode(true)\n logz.InfoContext(ctx, \"Hello\", \"target\", \"world\")\n\n testLogzAdapter(ctx)\n\n testLogz1(ctx)\n}\n\nfunc testLogz1(ctx context.Context) {\n msg := \"A message\"\n args := []any{\n  \"attr1\", 0,\n  \"attr2\", false,\n  \"attr3\", 3.13,\n  \"attr4\", errors.New(\"simple\"),\n  // ,,,\n  logz.Group(\"group1\",\n   \"attr1\", 0,\n   \"attr2\", false,\n   logz.NewAttr(\"attr3\", \"any styles what u prefer\"),\n   \"attrn\", // unpaired key can work here\n   logz.Group(\"more\", \"group\", []byte(\"more subgroup here\")),\n  ),\n  // ...\n  logz.Int(\"id\", 23123),\n  logz.Group(\"properties\",\n   logz.Int(\"width\", 4000),\n   logz.Int(\"height\", 3000),\n   logz.String(\"format\", \"jpeg\"),\n  ),\n }\n\n // disable unconditional termination inside logz.Panic/Fatal() calls.\n logz.AddFlags(logz.LnoInterrupt)\n\n logz.Print(\"\")   // logging a clean newline without decorations\n logz.Println(\"\") // logging a clean newline without decorations\n logz.Println()   // logging a clean newline without decorations\n logz.Print(msg, args...)\n logz.Println(msg) // synosym of Print\n logz.Fatal(msg, args...)\n logz.Panic(msg, args...)\n logz.Error(msg, args...)\n logz.Warn(msg, args...)\n logz.Info(msg, args...)\n logz.Debug(msg, args...)\n logz.Trace(msg, args...)\n\n // only print the logging contents while built with `-tags verbose`\n logz.Verbose(msg, args...) //\n\n // some verbs with more meanings\n logz.OK(\"ok\")\n logz.OK(\"ok\", args...)\n logz.Success(msg, args...)\n logz.Fail(msg, args...)\n\n // Contextual logging\n logz.InfoContext(ctx, \"info msg\", args...)\n\n logName := \"child1\"\n log := logz.New(logName)\n defer log.Close() // when you added file writer into `log`\n\n log.Print(\"\")   // logging a clean newline without decorations\n log.Println(\"\") // logging a clean newline without decorations\n log.Println()   // logging a clean newline without decorations\n log.Print(msg, args...)\n log.Println(msg) // synosym of Print\n log.Fatal(msg, args...)\n log.Panic(msg, args...)\n log.Error(msg, args...)\n log.Warn(msg, args...)\n log.Info(msg, args...)\n log.Debug(msg, args...)\n log.Trace(msg, args...)\n\n log.LogAttrs(ctx, logz.DebugLevel, \"debug msg\", args...)\n}\n\nfunc testLogzAdapter(ctx context.Context) {\n l := logz.New(\"standalone-logger-for-app\",\n  logz.NewAttr(\"attr1\", 2),\n  logz.NewAttrs(\"attr2\", 3, \"attr3\", 4.1),\n  \"attr4\", true, \"attr3\", \"string\",\n  logz.WithLevel(logz.AlwaysLevel),\n )\n defer l.Close()\n\n sub1 := l.New(\"sub1\").With(\"logger\", \"sub1\")\n sub2 := l.New(\"sub2\").With(\"logger\", \"sub2\").\n  WithLevel(logz.InfoLevel) // a new child instance here\n\n // create a log/slog logger HERE\n logger := slog.New(logz.NewSlogHandler(l, \u0026logz.HandlerOptions{\n  NoColor:  false,\n  NoSource: true,\n  JSON:     false,\n  Level:    logz.DebugLevel,\n }))\n\n l.Infof(\"logger: %v\", logger)\n l.Infof(\"l: %v\", l)\n\n // and logging with log/slog\n logger.DebugContext(ctx, \"hi debug\", \"AA\", 1.23456789)\n logger.InfoContext(ctx,\n  \"incoming request\",\n  slog.String(\"method\", \"GET\"),\n  slog.String(\"path\", \"/api/user\"),\n  slog.Int(\"status\", 200),\n )\n\n // now using our logg/slog interface\n sub1.DebugContext(ctx, \"hi debug\", \"AA\", 1.23456789)\n sub2.DebugContext(ctx, \"hi debug\", \"AA\", 1.23456789)\n sub2.InfoContext(ctx, \"hi info\", \"AA\", 1.23456789)\n}\n\nfunc dbStarter(closer func()) {\n defer closer()\n // initializing database connections...\n // ...\n}\n\nfunc cacheStarter(closer func()) {\n defer closer()\n // initializing redis cache connections...\n // ...\n}\n\nfunc mqStarter(closer func()) {\n defer closer()\n // initializing message queue connections...\n // ...\n}\n```\n\nThe sub-loggers are also supported, see [SubLogger](#sublogger).\n\n#### Println\n\nA `Println(args ...any)` has slight differences with other verbs like `Print(msg string, args ...any)`. But you are using it with same form like others. That is, the first of the args passing to Println is indeed a msg string.\n\nJust a little benefit, you can pass nothing to Println. If you're doing with this way, a complete blank line printed by `slog.Println()`, no timestamp, no serverity, and no caller info. `Println(\"\")` has same behaviour.\n\n```go\nslog.Info(\"1\")\nslog.Println() // this makes a real blank line, for colorful and logfmt formats\nslog.Info(\"2\")\n```\n\nIn a large long logging outputs, one and more blank line(s) maybe help your focus.\n\n### Customizing Your Verbs\n\nYou could wrap `logg/slog` package-level predicates/verbs to provide more features, just like what to do with  `OK`, `Success` and `Fail`. For example, the following sample demonstrates how to implement a `Tip` verb:\n\n```go\nfunc Tip(msg string, args...any) {\n  if myConditionSatisfied {\n    tipper.OK(msg, args...)\n  }\n}\n\nvar tipper logz.Logger // import \"logz\" \"github.com/hedzr/logg/slog\"\nvar onceTipper sync.Once\n\nfunc init(){\n  onceTipper.Do(func(){\n    tipper = slog.WithSkip(1)\n  })\n}\n```\n\n`WithSkip(n)` tells log/slog to strip extra n frames from caller stack so the logged message can hook to the caller of Tip(), rather than Line 3 in `Tip()`.\n\nAlso, using a custom level is a not-bad idea. See the [Customizing the Level](#customizing-the-level).\n\n### Builtin Output Formats\n\nlogg/slog has tree builtin optput formats: logfmt, json and colorful mode.\n\nThe default output is colorful to fit for debug console. But you can switch to the other two easily:\n\n```go\nslog.SetJSONMode()       // to get JSON format\nslog.SetColorMode(false) // to get logfmt format\nslog.SetColorMode()      // return to colorful mode\n```\n\n~~The above settings modify and apply effects to all loggers globally~~.\n\n~~`logg/slog` has no way to modify formats for certain a logger or sub-logger. We do believe that's normal action to keep a uniform output format~~.\n\nEach sub logger or detached loggers keep their own output formats, which can be changed dynamically.\n\nThe outputs:\n\n```bash\n{\"time\":\"14:53:10.907238+08:00\",\"level\":\"debug\",\"msg\":\"Debug message\",\"source\":{\"function\":\"github.com/hedzr/logg/slog_test.TestSlogJSON\",\"file\":\"./i_test.go\",\"line\":42}}\n\ntime=\"14:52:50.083343+08:00\" level=\"debug\" msg=\"Debug message\" source.function=\"github.com/hedzr/logg/slog_test.TestSlogLogfmt\" source.file=\"./i_test.go\" source.line=68\n\n```\n\nand,\n\n![image-20231028145416343](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/uPic/image-20231028145416343.png)\n\n### Set Level\n\nThe default is `WarnLevel` for a released app. If debugger detected the `DebugLevel` will be assumed.\n\nAlso the executable path will be tested for looking up if runing in test mode.\n\n```go\nslog.SetLevel(slog.InfoLevel)\nprintln(slog.Level)\n```\n\nEach sub-logger can hold its own level different with their parents or default logger.\n\nTo restore old level on Default logger, `SaveLevelAndSet` is available:\n\n```go\nfunc TestSlogBasic2(t *testing.T) {\n    defer slog.SaveLevelAndSet(slog.TraceLevel)()\n    slog.Debug(\"Debug message\") // should be enabled now\n    slog.Info(\"Info message\")   // should be enabled now\n    slog.Warn(\"Warning message\")\n    slog.Error(\"Error message\")\n}\n```\n\n### Sublogger\n\n`logger.WithXXX` can return a new sub logger, or get it by `New`.\n\n```go\nimport logz \"github.com/hedzr/logg/slog\"\n\nsub1 := logz.New(\"sub1\")\nsub2 := logz.Default().WithLevel(logz.TraceLevel)\nsub3 := logz.Default().New(\"sub3\")\nsub4 := logz.New(\n  WithJSONMode(false, false),\n  WithColorMode(false),\n  WithUTCMode(false, true, false),\n  WithTimeFormat(\"\", \"\", time.RFC3339Nano),\n  WithAttrs(Int(\"a\", 1)),\n  WithAttrs1(NewAttrs(\"a\", 1)),\n  With(\"b\", 2),\n)\n\nss1 := sub3.New(\"sub3-ss1\")\nss2 := sub3.New(\"sub3-ss2\")\n\nss3 := sub4.WithSkip(1)\n\n_ = `\n............. tree .............\n- (Default)\n  - sub2\n  - sub3\n    - sub3-ss1\n    - sub3-ss2\n- sub1 (detached logger)\n- sub4 (detached logger)\n  - ss3\n`\n```\n\nPassing a sublogger name as first arg to `New()` is useful. It is optional.\n\n#### Detached Logger\n\nA detached logger means that is splitted from `Default()` tree. `Default().New()` will make a tree of its child sub loggers. But package-level `New()` can build a clean sub logger detached with `Default()`.\n\n```go\nlogger := slog.New() // colorful logger, detached\nlogger := slog.New().SetJSONMode() // json format logger, detached\nlogger := slog.New().SetColorMode(false) // logfmt logger, detached\nlogger := slog.New().Set(\"attr1\", v1, \"attr2\", v2)) // detached\n\n// sublogger name is optional:\nlogger := slog.New(\"name\")\nlogger := slog.New(\"name\", slog.WithAttrs(args...))\nlogger := slog.New(\"name\", slog.NewAttr(\"attr1\", v1))\nlogger := slog.New(\"name\", slog.Int(\"attr1\", i1))\nlogger := slog.New(\"name\", slog.Group(\"group1\", slog.Int(\"attr1\", i1)))\nlogger := slog.New(\"name\", \"attr1\", v1, \"attr2\", v2).SetAttrs(args...)\n\n// child of child\nsl := logger.New()\nsl1 = logger.New(\"grandson\")\nsl2 = logger.WithLevel(slog.InfoLevel) // another child, since v0.7,x and v1\n\n// since v0.7 and v1, WithXXX can get a new sublogger, SetXXX not.\n\n// as a compasion, children of Default()\nlogger = slog.Default().New()\nsl1 := logger.New()\nsl2 := logger.WithLevel(slog.InfoLevel)\n```\n\nSublogger is lightweight,\n\n```go\nlogger := slog.New(args...).SetLevel(slog.InfoLevel)\n\n// .New() makes a child logger,\nsl1 := logger.New().SetLevel(slog.TraceLevel)\nsl2 := Default().New() // keep the parent's level\n// .WithXXX() makes a child logger\nsl3 := logger.WithLevel(slog.TraceLevel)\n```\n\nBy default, parent shares his features (level and other settings) to children, so `sl2` get `InfoLevel` same with `logger`.\n\n#### Inheritance\n\nIf `LattrsR` is set, the parent's attributes will be inherited to. For performance reason, it isn't enabled by default.\n\n```go\nlogger := slog.New(\"parent-logger\").Set(\"attr\", \"parent\").SetLevel(slog.InfoLevel)\nsl := logger.New(\"child-logger\")\n\nslog.AddFlags(slog.LattrsR)\nsl.Info(\"info\", \"attr1\", 1)    // also dumping \"attr=parent\" from logger\nslog.RemoveFlags(slog.LattrsR)\nsl.Info(\"info\", \"attr1\", 1)    // just dumping attrs in sl.\n```\n\nThe outputs looks like:\n\n```bash\n13:35:31.524263+08:00 child-logger [INF] info                                                    attr=parent attr1=1 /Volumes/VolHack/work/godev/cmdr.v2/libs.logg/slog/i_test.go:454 slog_test.TestSlogAttrsR\n13:35:31.524276+08:00 child-logger [INF] info                                                    attr1=1 /Volumes/VolHack/work/godev/cmdr.v2/libs.logg/slog/i_test.go:456 slog_test.TestSlogAttrsR\n```\n\nA logger or a sublogger could be identify by a unique name. Passing a string as first parameter to `New(...)`, it's assumed the logger name. Be careful that New() with a existed name will get the existed logger, sometimes it perhaps bad although we still kept this feature. Foutunately, `Sublogger(name)` can query a sub logger by name, this would be a guard when you really need many sub loggers.\n\nPassing common attributes and WithOpts following the sublogger name to `New()`, which will parse and interpret all of them. For examples:\n\n```go\n    l := slog.New(\"standalone-logger-for-app\",\n        slog.NewAttr(\"attr1\", 2),\n        slog.NewAttrs(\"attr2\", 3, \"attr3\", 4.1),\n        \"attr4\", true, \"attr3\", \"string\",\n        slog.WithLevel(slog.AlwaysLevel),\n    )\n    defer l.Close()\n\n    sub1 := l.New(\"sub1\").Set(\"logger\", \"sub1\")\n    sub2 := l.New(\"sub2\").Set(\"logger\", \"sub2\").SetLevel(slog.InfoLevel)\n\n    sub1.Debug(\"hi debug\", \"AA\", 1.23456789)\n```\n\nMaking children loggers is low-cost.\n\n### Holding a Logger\n\nBy creating and managing a sublogger, making your own logger might be dead simple:\n\n```go\nfunc newMyLogger2() *mylogger2 {\n    l := slog.New(\"mylogger\").SetLevel(slog.InfoLevel)\n    s := \u0026mylogger2{\n        l, // Provides basic Logger interface such as Info, Debug, etc.\n    true, // enable Infof()\n        l.WithSkip(1), // A sublogger created here. specially for Infof\n    }\n    return s\n}\n\ntype mylogger2 struct {\n    slog.Logger\n    SprintfLikeLoggingIsEnabled bool\n    sl                          slog.Logger\n}\n\nfunc (s *mylogger2) Close() { s.Logger.Close() }\n\nfunc (s *mylogger2) Infof(msg string, args ...any) {\n    if s.SprintfLikeLoggingIsEnabled {\n        if len(args) \u003e 0 {\n            // msg = fmt.Sprintf(msg, args...)\n\n            var data []byte\n            data = fmt.Appendf(data, msg, args...)\n            s.sl.Info(string(data))\n        }\n        s.sl.Info(msg)\n    }\n}\n\nfunc TestSlogBasic4(t *testing.T) {\n    l := newMyLogger2()\n    l.Infof(\"what's wrong with %v\", \"him\")\n    l.Info(\"no matter\")\n    // l.Infof(\"what's wrong with %v, %v, %v, %v, %v, %v\", m1AttrsAsAnySlice()...)\n}\n```\n\nThat is it.\n\n### Logging with Attributes\n\nThe attributes can be prepared or passed in several forms.\n\n#### Plain form\n\n```go\n    logger.Info(\"info message\",\n        \"attr1\", 0,\n        \"attr2\", false,\n        \"attr3\", 3.13,\n    ) // plain key and value pairs\n```\n\n#### NewAttr and NewGroupedAttr\n\n```go\nlogger.Info(\"info message\",\n        \"attr1\", 0,\n        \"attr4\", errors.New(\"simple\"),\n        \"attr3\", 3.13,\n        slog.NewAttr(\"attr3\", \"any styles what u prefer\"),\n        slog.NewGroupedAttrEasy(\"group1\", \"attr1\", 13, \"attr2\", false),\n    ) // use NewAttr, NewGroupedAttrs\n```\n\n#### Int, Float, String, Any, ..., and Group\n\nlogg/slog supports strong typed attributes:\n\n```go\nlogger.Info(\"image uploaded\",\n        slog.Int(\"id\", 23123),\n        slog.Group(\"properties\",\n            slog.Int(\"width\", 4000),\n            slog.Int(\"height\", 3000),\n            slog.String(\"format\", \"jpeg\"),\n        ),\n    ) // use Int, Float, String, Any, ..., and Group\n```\n\nThese interfaces are very similar with standard log/slog.\n\n#### Mixes all above forms\n\nThe above forms can be mixed in any order together.\n\n```go\nlogger.Info(\"image uploaded\",\n        \"attr1\", 0,\n        \"attr2\", false,\n        \"attr3\", 3.13,\n        \"attr4\", errors.New(\"simple\"),\n        // ,,,\n        slog.Group(\"group1\", \n            \"attr1\", 0,\n            \"attr2\", false,\n            slog.NewAttr(\"attr3\", \"any styles what u prefer\"),\n            slog.Group(\"more\", \"group\", []byte(\"more sub-attrs\")),\n            \"attrN\", // unpaired key can work here\n        ),\n        // ...\n        slog.Int(\"id\", 23123),\n        slog.Group(\"properties\",\n            slog.Int(\"width\", 4000),\n            slog.Int(\"height\", 3000),\n            slog.String(\"format\", \"jpeg\"),\n        ),\n    )\n```\n\n#### Work with common Attributes\n\nWhile creating a sublogger, you could specify some common attributes. They are no more effects for performance reason by default. `log.Info` and others printers will check out and print all of parents' common attributes while `LattrsR` is set.\n\nBoth of the following forms are valid:\n\n```go\n// available forms\nlogger := slog.New(\"logger-name\")\nlogger := slog.New(\"logger-name\", slog.WithAttrs(args...))\nlogger := slog.New(\"logger-name\", slog.NewAttr(\"attr1\", v1))\nlogger := slog.New(\"logger-name\", slog.Int(\"attr1\", i1))\nlogger := slog.New(\"logger-name\", slog.Group(\"group1\", slog.Int(\"attr1\", i1)))\nlogger := slog.New(\"logger-name\", \"attr1\", v1, \"attr2\", v2).WithAttrs(args...)\nlogger := slog.New(\"attr1\", v1, \"attr2\", v2).WithAttrs(args...) // no name is ok\n\n// the attributes will be printed out if LattrsR set,\nslog.AddFlags(slog.LattrsR)\nlogger.Info(\"message\", \"type\", \"int\") // Out: ,,, A message    attr1=v1 attr2=v2 ... type=int\n```\n\n#### Grouping attributes\n\nSee above of above.\n\n### Logging contextual attrs\n\nSame to standard `log/slog`, `logg/slog` has LogAttrs() to log attributes contextually.\n\n```go\n    logger := slog.New().SetAttrs(slog.String(\"app-version\", \"v0.0.1-beta\"))\n    ctx := context.Background()\n    logger.InfoContext(ctx, \"info msg\", \"attr1\", 111333,\n        slog.Group(\"memory\",\n            slog.Int(\"current\", 50),\n            slog.Int(\"min\", 20),\n            slog.Int(\"max\", 80)),\n        slog.Int(\"cpu\", 10),\n    )\n```\n\n#### Extracting attrs from context\n\nSometimes the attributes can be extracted from context.Context.\n\n```go\nfunc TestSlogWithContext(t *testing.T) {\n    logger := slog.New().SetAttrs(slog.String(\"app-version\", \"v0.0.1-beta\"))\n    ctx := context.WithValue(context.Background(), \"ctx\", \"oh,oh,oh\")\n    logger.SetContextKeys(\"ctx\").\n      InfoContext(ctx, \"info msg\",\n        \"attr1\", 111333,\n        slog.Group(\"memory\",\n            slog.Int(\"current\", 50),\n            slog.Int(\"min\", 20),\n            slog.Int(\"max\", 80)),\n        slog.Int(\"cpu\", 10),\n    )\n}\n```\n\nThe result:\n\n![image-20231106074712683](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/uPic/image-20231106074712683.png)\n\nAs you seen, the value in context was been extracted and printed out.\n\n### Set Writer\n\n`logg/slog` uses a internal `dualWriter` to serialize the logging contents.\n\nA `dualWriter` holds two output devices: `Normal`, and `Error`. `dualWriter` sends contents to stdout or stderr in accord to the requesting logging level. For example, a `Info(...)` calling will be dispatched to stdout and a `Warn`, `Error`, `Panic`, and `Fatal` to stderr.\n\nNot only for those, the dualWriter allows you stack mutiple writers as its `Normal` or `Error` output devices. That means, a console `os.Stdout` and a file writer can be combined into `Normal` at once. How to do it? It's simple:\n\n```go\nlogger := New(\"tty+file\").AddWriter(slog.NewFileWriter(\"/tmp/app-stdout.log\"))\n```\n\nCalling AddWriter on `Default()` would enable the above assembly into the default logger.\n\nIn shortly, `AddWriter(w)`/`RemoveWriter`/`ResetWriter` can append/remove/reset the stdout device. `AddErrorWriter(w)`/`RemoveErrorWriter`/`ResetErrorWriter` can append/remove/reset to the stderr device.\n\nAlso, `ResetWriters` works so we can always back to default state.\n\n#### io.Writer\n\nA standard `io.Writer` is needed when using AddWriter/WithWriter:\n\n```go\n// Simple file\ntf, _ := os.CreateTempFile(\"\", \"stdout.log\")\nlogger.AddWriter(tf)\n```\n\n### Leveled Writers\n\nYour writer can implement `LevelSettable` to handling the requesting logging level.\n\n```go\npackage mywriter\nimport (\n \"logz\" \"github.com/hedzr/logg/slog\"\n)\ntype myWriter struct {\n level logz.Level\n}\nfunc (s *myWriter) SetLevel(level logz.Level) { s.level = level } // logz.LevelSettable\nfunc (s *myWriter) Write(data []byte) (n int, err error) {\n switch(s.level) {\n case logz.DebugLevel:\n // ...\n }\n return\n}\n```\n\nIn this scene, `logg/slog` will call `Write` following `SetLevel`. It's safe in many cases, but you can take more safety for it by using locked mechanism: go build tags `-tags=logglock` will enable a special version with wrapping the two calls in a sync.Mutex. See the source code in entry_lock.go.\n\nAlso we provides sub-feature to allow you specify special writer for a special logging level:\n\n```go\nfunc TestAddLevelWriter1(t *testing.T) {\n    logger := New().AddLevelWriter(InfoLevel, \u0026decorated{os.Stdout})\n    logger.Info(\"info msg\")\n    logger.Debug(getMessage(0))\n}\n\ntype decorated struct {\n    *os.File\n}\n\nfunc (s *decorated) Write(p []byte) (n int, err error) {\n    if s.File != nil {\n        if ni, e := s.File.WriteString(\"[decorated] \"); e != nil {\n            err = errors.Join(err, e)\n        } else {\n            n += ni\n        }\n        if ni, e := s.File.Write(p); e != nil {\n            err = errors.Join(err, e)\n        } else {\n            n += ni\n        }\n    }\n    return\n}\n```\n\nThe result is similar with:\n\n```bash\n[decorated] 10:57:22.986797+08:00 [INF] info msg                              ./new_test.go:170 slog.TestAddLevelWriter1\n10:57:22.987031+08:00 [DBG] Test logging, but use a somewhat realistic message length. (#0)  ./new_test.go:171 slog.TestAddLevelWriter1\n\n```\n\n#### Close() and Closables\n\nIn most cases, Logger's `Close()` has no more attentions to you.\n\nBut adding defer close codes is a best practice, like this:\n\n```go\npackage main\n\nimport \"github.com/hedzr/logg/slog\"\n\nfunc main() {\n  logger := slog.New(\"tty+file\").AddWriter(slog.NewFileWriter(\"/tmp/app-stdout.log\"))\n  defer logger.Close()\n  \n  logger.Info(\"info msg\")\n}\n```\n\nThe file writer will get a chance to shutdown itself gracefully.\n\n`Closables` is a concept from is/basics.Closables, see it at [hedzr/is/basics](https://github.com/hedzr/is/blob/master/basics/closers.go).\n\n### Set Handler\n\n..\n\n### Customizing the Level\n\nIn `logg/slog`, using your own logging level is enough simple\u003e The following fragments sample you how to make 3 new levels,\n\n```go\nconst (\n    NoticeLevel = slog.Level(17) // A custom level must have a value greater than slog.MaxLevel\n    HintLevel   = slog.Level(-8) // Or use a negative number\n    SwellLevel  = slog.Level(12) // Sometimes, you may use the value equal with slog.MaxLevel\n)\n\nfunc TestSlogCustomizedLevel(t *testing.T) {\n    checkerr(t, slog.RegisterLevel(NoticeLevel, \"NOTICE\",\n        slog.RegWithShortTags([6]string{\"\", \"N\", \"NT\", \"NTC\", \"NOTC\", \"NOTIC\"}),\n        slog.RegWithColor(color.FgWhite, color.BgUnderline),\n        slog.RegWithTreatedAsLevel(slog.InfoLevel),\n    ))\n\n    checkerr(t, slog.RegisterLevel(HintLevel, \"Hint\",\n        slog.RegWithShortTags([6]string{\"\", \"H\", \"HT\", \"HNT\", \"HINT\", \"HINT \"}),\n        slog.RegWithColor(color.NoColor, color.BgInverse),\n        slog.RegWithTreatedAsLevel(slog.InfoLevel),\n    ))\n\n    checkerr(t, slog.RegisterLevel(SwellLevel, \"SWELL\",\n        slog.RegWithShortTags([6]string{\"\", \"S\", \"SW\", \"SWL\", \"SWEL\", \"SWEEL\"}),\n        slog.RegWithColor(color.FgRed, color.BgBoldOrBright),\n        slog.RegWithTreatedAsLevel(slog.ErrorLevel),\n        slog.RegWithPrintToErrorDevice(),\n    ))\n\n    logger := slog.New()\n\n    logger.Debug(\"Debug message\")\n    logger.Info(\"Info message\")\n    logger.Warn(\"Warning message\")\n    logger.Error(\"Error message\")\n\n    slog.SetLevelOutputWidth(5)\n\n    ctx := context.Background()\n    logger.LogAttrs(ctx, NoticeLevel, \"Notice message\")\n    logger.LogAttrs(ctx, HintLevel, \"Hint message\")\n    logger.LogAttrs(ctx, SwellLevel, \"Swell level\")\n}\n\nfunc checkerr(t *testing.T, err error) {\n    if err != nil {\n        t.Error(err)\n    }\n}\n```\n\nIts outputs looks like\n\n![image-20231030144425448](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/uPic/image-20231030144425448.png)\n\nThe ansi color representations relyes on your terminal settings.\n\n`SetLevelOutputWidth(n)` lets you can control the level serverity's display width (from 1 to 5). At customizing you should pass an array (`[6]string`) for each levels. For example, HintLevel can be displayed as \"HINT\" when output width is 4, or as \"H\" when width is 1. You could change it globally at any time. Just like the snapshot above, it was changed to 5 at last time, so HintLevel has width 5.\n\n### Customizing the Colors\n\nDislike logg/slog's console colors of each levels? No matter, setup with yours:\n\n```go\nimport color \"github.com/hedzr/is/term/color\"\n\nslog.SetLevelColors(slog.PanicLevel, color.FgWhite, color.NoColor)\n```\n\nThese are standard ANSI escaped sequences, any of SGR fore-, background colors. A special `NoColor` is `-1`.\n\n```go\nconst NoColor = color.Color(-1)\n```\n\n[`hedzr/is`](https://github.com/hedzr/is) provides a color Translator to format your html-like string to ansi colored text for terminal outputting. For more information see hedzr/is doc.\n\n### Adapting into `log/slog`\n\nIf you are using unified `log/slog` interfaces, just put `logg/slog` into it:\n\n```go\nimport logslog \"log/slog\"\nimport logz \"github.com/hedzr/logg/slog\"\n\nfunc TestSlogUsedForLogSlog(t *testing.T) {\n    l := logz.New(\"standalone-logger-for-app\",\n        logz.NewAttr(\"attr1\", 2),\n        logz.NewAttrs(\"attr2\", 3, \"attr3\", 4.1),\n        \"attr4\", true, \"attr3\", \"string\",\n        logz.WithLevel(slog.AlwaysLevel),\n    )\n    defer l.Close()\n\n    sub1 := l.New(\"sub1\").With(\"logger\", \"sub1\")\n    sub2 := l.New(\"sub2\").With(\"logger\", \"sub2\").WithLevel(logz.InfoLevel)\n\n    // create a log/slog logger HERE\n    logger := logslog.New(logz.NewSlogHandler(l, nil))\n\n    t.Logf(\"logger: %v\", logger)\n    t.Logf(\"l: %v\", l)\n\n    // and logging with log/slog\n    logger.Debug(\"hi debug\", \"AA\", 1.23456789)\n    logger.Info(\n        \"incoming request\",\n        logslog.String(\"method\", \"GET\"),\n        logslog.String(\"path\", \"/api/user\"),\n        logslog.Int(\"status\", 200),\n    )\n\n    // now using our logg/slog interface\n    sub1.Debug(\"hi debug\", \"AA\", 1.23456789)\n    sub2.Debug(\"hi debug\", \"AA\", 1.23456789)\n    sub2.Info(\"hi info\", \"AA\", 1.23456789)\n}\n```\n\nThe result:\n\n![image-20231029100643178](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/uPic/image-20231029100643178.png)\n\nOur testing keeps logg/slog native outputs as the last three lines.\n\nIn this case, some features of our logg/slog cannot be used via log/slog APIs but it's still colorful.\n\n### Customizing `ValueStringer`\n\nlogg/slog allows you handle stringerize value with customizing `ValueStringer`. So you can pass yours.\n\nHere is a sample to pretty print the values in attributes:\n\n```go\nimport \"github.com/alecthomas/repr\"\n\nfunc NewSpewPrinter() *prettyPrinter { //nolint:revive // just a test\n    return \u0026prettyPrinter{\n        repr.New(os.Stdout, repr.Indent(\"  \")),\n    }\n}\n\ntype prettyPrinter struct {\n    *repr.Printer\n}\n\nfunc (p *prettyPrinter) SetWriter(w io.Writer) {\n    p.Printer = repr.New(w, repr.Indent(\"  \"))\n}\n\nfunc (p *prettyPrinter) WriteValue(value any) {\n    // p.reprValue(map[reflect.Value]bool{}, reflect.ValueOf(value), \"\", true, false)\n    p.Print(value)\n}\n\nfunc TestSlogCustomValueStringer(t *testing.T) {\n    // slog.SetLevel(slog.InfoLevel)\n    slog.AddFlags(slog.Lprivacypathregexp | slog.Lprivacypath)\n\n    defer slog.SaveFlagsAnd(func() { //nolint:revive // ok\n        slog.AddFlags(slog.Lprettyprint)\n    })()\n\n    printer := NewSpewPrinter()\n\n    for _, logger := range []slog.Logger{\n        slog.New(\" spew \").WithValueStringer(printer),\n        slog.New(\"normal\"),\n    } {\n        logger.Println()\n        logger.LogAttrs(\n            context.Background(),\n            slog.InfoLevel,\n            \"image uploaded\",\n            slog.Int(\"id\", 23123),\n            slog.Group(\"properties\",\n                slog.Int(\"width\", 4000),\n                slog.Int(\"height\", 3000),\n                slog.String(\"format\", \"jpeg\"),\n                slog.Any(\"Map\", map[int][]float64{3: {3.14, 2.72}, 5: {0.717, 1.732}}),\n            ),\n        )\n    }\n}\n```\n\nThe outputs are:\n\n![image-20231029170419385](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/uPic/image-20231029170419385.png)\n\nTo integrete `go-spew` is similar and simple.\n\n### Hide the sensitive fields\n\nYour struct can implement `LogObjectMashaller` or `LogArrayMashaller` so that the sensitive fields can be hardened.\n\n```go\ntype users []*user\n\nfunc (uu users) MarshalLogArray(enc *slog.PrintCtx) (err error) {\n    for i := range uu {\n        if i \u003e 0 {\n            enc.WriteRune(',')\n        }\n        if e := uu[i].MarshalLogObject(enc); e != nil {\n            err = errors.Join(err, e)\n        }\n    }\n    return\n}\n\ntype user struct {\n    Name      string    `json:\"name\"`\n    Email     string    `json:\"email\"`\n    CreatedAt time.Time `json:\"created_at\"`\n}\n\nfunc (u *user) MarshalLogObject(enc *slog.PrintCtx) (err error) {\n    enc.AddString(\"name\", u.Name)\n    enc.AddRune(',')\n    enc.AddString(\"email\", u.Email)\n    enc.AddRune(',')\n    enc.AddInt64(\"createdAt\", u.CreatedAt.UnixNano())\n    return\n}\n```\n\n`*slog.PrintCtx` is our value encoder.\n\n### Hardening filepath, shorten package name\n\nThe caller information could leak the user's names, disk volumes, directory structure and others sensitive contents.\n\n#### Hardening filepath(s)\n\nFor this test case:\n\n```go\nfunc TestLogOneTwoThree(t *testing.T) {\n    l := slogg.New().WithLevel(slogg.InfoLevel)\n    l.Info(\"info msg\", \"Aa\", 1, \"Bbb\", \"a string\", \"Cc\", 3.732, \"D\", 2.71828+5.3571i)\n}\n```\n\nIt may print out:\n\n```go\n20:20:01.697577+08:00 [INF] info msg                             Aa=1 Bbb=a string Cc=3.732 D=(2.71828+5.3571i) /Volumes/VolWork/go.work/libs.log/bench/logg_test.go:17 bench.TestLogOneTwoThree\n```\n\nNow we enable privacy flags, a builtin regexp rule (`/Volumes/.*/(.*)` -\u003e `~$1`) will take effects (see the [Tilde Directory](#tilde-directory):\n\n```bash\n20:22:33.688847+08:00 [INF] info msg                             Aa=1 Bbb=a string Cc=3.732 D=(2.71828+5.3571i) ~work/go.work/libs.log/bench/logg_test.go:17 bench.TestLogOneTwoThree\n`````\n\nThe code looks like:\n\n```go\nfunc init() {\n    slog.AddFlags(slog.Lprivacypathregexp | slog.Lprivacypath)\n}\n```\n\nThe builtin rules includes truncate homdir to `~`, disable absolute pathname, and using relative path, and so on.\n\nYou may make calls to `AddKnownPathMapping(path, repl)` and `AddKnownPathRegexpMapping(expr, repl)` to setup\nthem.\n\n##### Tilde Directory\n\nIn zsh/bash, you can create tilde directory as a folder alias. For a instance,\n\n```bash\nhash -d work=/Volumes/VolWork\nls -la ~work/go.work/\n```\n\nThe builtin rules of `logg/slog` can do this translate in call info in logging message.\n\nSee also `AddKnownPathRegexpMapping/RemoveKnownPathRegexpMapping` and `ResetKnownPathRegexpMapping`.\n\n#### Shortening package name(s)\n\nIn outputs, package name in caller information has form `github.com/user/repo/offset.object.function`, By enabling `Lcallerpackagename`, `github.com` will be shortened to `GH`. The other well-known code-hosting providers are converted too:\n\n- \"github.com\" -\u003e \"GH\"\n- \"gitlab.com\" -\u003e \"GL\"\n- \"gitee.com\" -\u003e \"GT\"\n- \"bitbucker.com\" -\u003e \"BB\"\n\n```go\nfunc init() {\n    slog.AddFlags(slog.Lcallerpackagename)\n}\n```\n\nIf `Lcallerpackagename` is not present (this is default behavior), the package name will be truncated simply.\n\n`AddCodeHostingProviders(provide, repl)` API can add more rules for shortening.\n\n#### More Rules\n\nYou can always append yours with `AddKnownPathMapping(pathname, repl string)` and `AddKnownPathRegexpMapping(pathnameRegexpExpr, repl string)`.\n\nIf `Lprivacypathregexp` and `Lprivacypath` is not present (this is default behavior), we try to truncate the pathname as possible as we can.\n\nAnd, `AddCodeHostingProviders(provider, repl string)` do similar things and need `Lcallerpackagename` is enabled.\n\nSee also `AddKnownPathRegexpMapping`. `RemoveKnownPathRegexpMapping` and `ResetKnownPathRegexpMapping`, `AddKnownPathMapping`. `RemoveKnownPathMapping` and `ResetKnownPathMapping`,\n\n### Other Helpers\n\nHere are two savers so that you can write codes easiler:\n\n```go\n// Save global Level and set to new, and restore original\ndefer slog.SaveLevelAndSet(slog.WarnLevel)()\n\n\n defer SaveFlagsAndMod(LnoInterrupt | LattrsR)()\n defer SaveLevelAndSet(TraceLevel)()\n\n// Save global Flags and modify it for local logic, and restore it after going back to up level\ndefer slog.SaveFlagsAnd(func() {\n    slog.AddFlags(slog.LattrsR) // add, remove, or set flags\n})()\n\n```\n\n## Others\n\n### What meaning is the Multiline print?\n\nIn colorful print mode, one of logging output line starts with timestamp and serverity, and ends with attributes and caller info. So the message is put at middle part of one line with limited width.\n\nA very long message will be wrapped to next line(s), right?\n\nThinking about another case, a logging line has title and details, what form output is better.\n\n```go\nfunc TestLogLongLine(t *testing.T) {\n    l := New()\n    l.Info(\"/ERROR/ Started at \"+time.Now().String()+\", this is a multi-line test\\nVersion: 2.0.1\\nPackage: hedzr/hilog\", \"Aa\", 1, \"Bbb\", \"a string\", \"Cc\", 3.732, \"D\", 2.71828+5.3571i)\n}\n```\n\nIt prints:\n\n![image-20231106234956844](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/uPic/image-20231106234956844.png)\n\nAs you seen, a message will be splitted to first line (as a title) and the rest lines (as a details text). So you can avoid write a long message. Instead, you write a title with some details to describe a logging line better clearly. By the way, its json or logfmt format is still a message field with key name \"msg\".\n\n### Error Objects\n\nWhen you're using [errors.v3](https://github.com/hedzr/errors) (`gopkg.in/hedzr/errors.v3`), the trace info will be embedded into all output formats:\n\n```go\nimport \"gopkg.in/hedzr/errors.v3\"\n\nerr := errors.New(\"fail sample\")\nslog.Info(\"error occurred\", \"err\", err)\n```\n\nThe similar test codes get the output like this:\n\n![Screenshot 2024-11-18 at 07.58.11](https://cdn.jsdelivr.net/gh/hzimg/blog-pics@master/upgit/2024/11/20241118_1731888090.png)\n\nFor others errors, we used `fmt.Sprintf(\"%+v\", err)`, it's heavy but effective.\n\nWe do not support the log/slog way with `slog.Any(\"error\", err)` and `replaceAttr`.\n\n## LICENSE\n\nApache 2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhedzr%2Flogg","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhedzr%2Flogg","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhedzr%2Flogg/lists"}