From 396f078ffa8f9ea04f48d0436532e62368600c44 Mon Sep 17 00:00:00 2001 From: Erik Unger Date: Wed, 17 Jun 2026 11:19:16 +0200 Subject: [PATCH] fix: nil-safe Logger level methods; cut v1.1.1 changelog, docs + examples Fixed: - Guard all Logger level methods (Trace/Debug/Info/Warn/Error/Fatal and their At/Ctx/f/fCtx variants) against a nil receiver, returning a nil *Message instead of panicking, honoring the nil-safe contract documented in doc.go. Adds TestLogger_NilSafeLevelMethods. Added: - Runnable Example functions for the primary Logger API and the log subpackage (visible on pkg.go.dev, verified by go test); package doc comment for log. Changed: - logsentry: bump indirect golang.org/x/text v0.34.0 -> v0.37.0 (aligns with the tools submodule). Documentation: - Scope duplicate-key-prevention docs to inherited attributes in README; same-chain duplicates are not deduplicated. - Fix the doc.go context example to use golog.NewString (no golog.Str exists). Release prep: - Cut the [1.1.1] - 2026-06-17 CHANGELOG section from [Unreleased] (VERSION is already v1.1.1). - Ignore the .gstack/ browse-audit artifact in .gitignore. --- .gitignore | 3 ++ CHANGELOG.md | 33 ++++++++++++++++- README.md | 8 ++--- VERSION | 2 +- doc.go | 4 +-- example_test.go | 49 +++++++++++++++++++++++++ go.work.sum | 21 ++++++++++- log/example_test.go | 29 +++++++++++++++ log/log.go | 17 +++++++++ logger.go | 87 +++++++++++++++++++++++++++++++++++++++++++++ logger_test.go | 55 ++++++++++++++++++++++++++++ logsentry/go.mod | 2 +- logsentry/go.sum | 3 +- 13 files changed, 301 insertions(+), 12 deletions(-) create mode 100644 example_test.go create mode 100644 log/example_test.go diff --git a/.gitignore b/.gitignore index f55f4bf..c2dbb6a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out *.prof + +# Browse-audit output +.gstack/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e8e13f..af41f16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,36 @@ lockstep with the root module, e.g. `v1.0.7`, `logsentry/v1.0.7`, `goslog/v1.0.7 ## [Unreleased] +## [1.1.1] - 2026-06-17 + +### Added + +- Runnable `Example` functions for the primary `Logger` API (`Example`, + `ExampleLogger`) and the `log` subpackage, so the common usage paths appear + on pkg.go.dev and are verified by `go test`. +- Package documentation comment for the `log` subpackage. + +### Changed + +- **logsentry:** bump the indirect `golang.org/x/text` dependency from v0.34.0 + to v0.37.0, aligning it with the `tools` submodule. + +### Fixed + +- A nil `*Logger` no longer panics on the level methods (`Trace`, `Debug`, + `Info`, `Warn`, `Error`, `Fatal` and their `At`/`Ctx`/`f`/`fCtx` variants). + They now return a nil `*Message`, honoring the nil-safe contract documented + in `doc.go`. Previously they dereferenced `l.config` while reading the level + and crashed with a nil pointer dereference. + +### Documentation + +- Clarify that duplicate-key prevention applies to keys inherited from the + logger, a sub-logger, or the context (the inherited value wins), not to + duplicate keys added within a single message chain. +- Fix the `doc.go` context example to use `golog.NewString` (there is no + `golog.Str` constructor). + ## [1.1.0] - 2026-06-16 ### Changed @@ -109,7 +139,8 @@ lockstep with the root module, e.g. `v1.0.7`, `logsentry/v1.0.7`, `goslog/v1.0.7 attrib type for zero-allocation `time.Time` logging, and the `tag-release` versioning script. -[Unreleased]: https://github.com/domonda/golog/compare/v1.1.0...HEAD +[Unreleased]: https://github.com/domonda/golog/compare/v1.1.1...HEAD +[1.1.1]: https://github.com/domonda/golog/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/domonda/golog/compare/v1.0.7...v1.1.0 [1.0.7]: https://github.com/domonda/golog/compare/v1.0.6...v1.0.7 [1.0.6]: https://github.com/domonda/golog/compare/v1.0.5...v1.0.6 diff --git a/README.md b/README.md index fcda0b0..04a2a78 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Fast and feature-rich structured logging library for Go - **Terminal Auto-Detection**: Automatically switches between colored text (TTY) and JSON (non-TTY) - **Configurable Log Levels**: TRACE, DEBUG, INFO, WARN, ERROR, FATAL with flexible filtering - **Context Support**: Log attributes can be stored in and retrieved from context automatically -- **Duplicate Key Prevention**: Prevents accidental duplicate keys in log output +- **Duplicate Key Prevention**: A message attribute is dropped when the same key was already set by the logger, a sub-logger, or the context (the inherited value wins), keeping inherited structured data clean - **Colorized Output**: Beautiful colored console output with customizable colorizers - **Multi-Writer Architecture**: Log to multiple destinations with different formats and filters - **Rotating Log Files**: Automatic file rotation based on size thresholds @@ -730,7 +730,7 @@ golog is designed to strike a balance between performance and flexibility. While | Feature | zerolog | zap | golog | |------------------------------------------|------------------|------------------|-----------------------| | **Multi-writer support** | Single output | Limited | Native, unlimited | -| **Duplicate key prevention** | No | No | Yes | +| **Duplicate key prevention** (inherited) | No | No | Yes | | **Context attribute integration** | Manual | Manual | Automatic | | **Sub-logger with inherited attributes** | Basic | Basic | Full support with attrib recording | | **Zero allocations (simple message)** | Yes | Yes | Yes | @@ -755,7 +755,7 @@ golog is designed to strike a balance between performance and flexibility. While - **Native multi-writer architecture**: Log to console, files, and external services simultaneously with different formats and filters per destination - **Automatic context integration**: Attributes added to `context.Context` are automatically included in log messages without manual plumbing - **Sub-logger attribute recording**: The `With().SubLogger()` pattern creates child loggers that efficiently inherit and extend parent attributes -- **Duplicate key prevention**: Prevents accidental duplicate keys in log output, ensuring clean structured data +- **Duplicate key prevention**: When a message sets a key already provided by the logger, a sub-logger, or the context, the inherited value is kept and the message-level duplicate is dropped, ensuring clean inherited structured data - **Zero allocations for standard logging**: Despite the richer feature set, golog achieves zero allocations for JSON logging with fields - **Nil-safe design**: A nil logger is safe to use and won't panic, simplifying error handling @@ -766,7 +766,7 @@ golog is the right choice when you need: - **Multiple output destinations**: Log to stdout with colors for development and JSON files for production simultaneously - **Request-scoped logging**: Automatically propagate correlation IDs, user IDs, and other context through your application - **Sub-loggers with inherited context**: Create child loggers for specific components that include parent attributes -- **Clean structured data**: Prevent duplicate keys from appearing in your logs +- **Clean structured data**: Prevent keys inherited from sub-loggers or context from being duplicated by message-level attributes - **slog compatibility**: Use golog as a backend for Go's standard library logging interface - **Rotating log files**: Built-in support for size-based log rotation diff --git a/VERSION b/VERSION index 795460f..56130fb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.1.0 +v1.1.1 diff --git a/doc.go b/doc.go index 46ef279..b3de6e4 100644 --- a/doc.go +++ b/doc.go @@ -83,8 +83,8 @@ Store and retrieve log attributes in context: // Add attributes to context ctx = golog.ContextWithAttribs(ctx, - golog.Str("correlation_id", corrID), - golog.Str("user_id", userID), + golog.NewString("correlation_id", corrID), + golog.NewString("user_id", userID), ) // Create logger from context diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..4202445 --- /dev/null +++ b/example_test.go @@ -0,0 +1,49 @@ +package golog + +import ( + "errors" + "os" +) + +// Example shows the minimal logging setup: build a Config, create a Logger, +// and emit messages with the level methods. Passing a nil Format uses +// NewDefaultFormat, which prefixes each line with a timestamp; this example +// sets a Format without a TimestampKey so the output is reproducible. +func Example() { + config := NewConfig( + &DefaultLevels, + AllLevelsActive, + NewJSONWriterConfig(os.Stdout, &Format{LevelKey: "level", MessageKey: "message"}), + ) + log := NewLogger(config) + + log.Info("Application started").Log() + log.Error("Connection failed").Err(errors.New("timeout")).Log() + + // Output: + // {"level":"INFO","message":"Application started"} + // {"level":"ERROR","message":"Connection failed","error":"timeout"} +} + +// ExampleLogger demonstrates structured logging with typed field methods and a +// sub-logger whose attributes are inherited by every message it emits. The +// Format omits the TimestampKey so the output is reproducible. +func ExampleLogger() { + format := &Format{LevelKey: "level", MessageKey: "message"} + log := NewLogger(NewConfig(&DefaultLevels, AllLevelsActive, NewJSONWriterConfig(os.Stdout, format))) + + // Typed field methods avoid reflection and stay zero-allocation. + log.Info("user login"). + Str("user", "john_doe"). + Int("attempt", 2). + Bool("admin", false). + Log() + + // A sub-logger inherits attributes for every message it emits. + svc := log.With().Str("service", "auth").SubLogger() + svc.Warn("token expiring soon").Log() + + // Output: + // {"level":"INFO","message":"user login","user":"john_doe","attempt":2,"admin":false} + // {"level":"WARN","message":"token expiring soon","service":"auth"} +} diff --git a/go.work.sum b/go.work.sum index 675747d..5bf9e2d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -300,12 +300,14 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkY github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= @@ -345,10 +347,10 @@ github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -360,8 +362,10 @@ github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1Ig github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -378,11 +382,13 @@ github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9 github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -394,6 +400,7 @@ github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JS github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= @@ -455,6 +462,7 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -533,12 +541,14 @@ github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjF github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/modelcontextprotocol/go-sdk v1.3.1/go.mod h1:DgVX498dMD8UJlseK1S5i1T4tFz2fkBk4xogC3D15nw= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5/go.mod h1:FUqVoUPHSEdDR0MnFM3Dh8AU0pZHLXUD127SAJGER/s= @@ -558,6 +568,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= @@ -605,6 +617,7 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= @@ -625,6 +638,7 @@ go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVL go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= @@ -814,6 +828,7 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -937,6 +952,8 @@ golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -946,6 +963,7 @@ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -1104,6 +1122,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go. google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/bytestream v0.0.0-20251124214823-79d6a2a48846/go.mod h1:G3Q0qS3k/oFEmVMddPsSYcFnm2+Mq2XRmxujrtu5hr0= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:6TABGosqSqU2l1+fJ3jdvOYPPVryeKybxYF0cCZkTBE= google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= diff --git a/log/example_test.go b/log/example_test.go new file mode 100644 index 0000000..a049935 --- /dev/null +++ b/log/example_test.go @@ -0,0 +1,29 @@ +package log_test + +import ( + "context" + + "github.com/domonda/golog" + "github.com/domonda/golog/log" +) + +// Example shows the ready-to-use package logger. No setup is required: the +// logger is configured from the LOG_LEVEL environment variable and writes +// colored text on a terminal or JSON when the output is redirected. +func Example() { + log.Info("Application started").Log() + log.Error("Something went wrong"). + Str("component", "auth"). + Log() +} + +// Example_context attaches request-scoped attributes to a context; the *Ctx +// logging methods include them automatically, so correlation IDs propagate +// without manual plumbing. +func Example_context() { + ctx := golog.ContextWithAttribs(context.Background(), + golog.NewString("request_id", "req-123"), + golog.NewString("user_id", "user-456"), + ) + log.InfoCtx(ctx, "processing request").Log() +} diff --git a/log/log.go b/log/log.go index 98d4f97..6009acc 100644 --- a/log/log.go +++ b/log/log.go @@ -1,3 +1,20 @@ +// Package log provides a ready-to-use, package-level logger so applications and +// libraries can start logging without constructing a [golog.Logger] themselves. +// +// The package-level functions ([Info], [Warn], [Error], [Debug], [Trace], +// [Fatal] and their *Ctx and *f variants) delegate to the shared [Logger], +// which is built from [Config]. By default the minimum level is read from the +// LOG_LEVEL environment variable (defaulting to DEBUG) and the output format +// adapts to the environment: colorized text on a terminal, JSON when stdout is +// redirected (see [golog.DecideWriterConfigForTerminal]). +// +// Both [Config] and [Logger] are exported variables that may be reassigned at +// startup to customize formatting, levels, or writers. [Logger] uses a +// [golog.DerivedConfig] that references [Config], so changes to [Config] take +// effect immediately without recreating the logger. +// +// For request-scoped logging, [HTTPMiddlewareHandler] and [HTTPMiddlewareFunc] +// attach a UUID request ID that the *Ctx functions include automatically. package log import ( diff --git a/logger.go b/logger.go index cf96f80..3ba4e04 100644 --- a/logger.go +++ b/logger.go @@ -260,146 +260,233 @@ func (l *Logger) FatalAndPanic(p any) { // Fatal starts a new fatal level log message. func (l *Logger) Fatal(text string) *Message { + if l == nil { + return nil + } return l.NewMessage(context.Background(), l.config.FatalLevel(), text) } // FatalCtx starts a new fatal level log message with the given context. func (l *Logger) FatalCtx(ctx context.Context, text string) *Message { + if l == nil { + return nil + } return l.NewMessage(ctx, l.config.FatalLevel(), text) } // Fatalf starts a new fatal level log message formatted using fmt.Sprintf. func (l *Logger) Fatalf(format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(context.Background(), l.config.FatalLevel(), format, args...) } // FatalfCtx starts a new fatal level log message with context, formatted using fmt.Sprintf. func (l *Logger) FatalfCtx(ctx context.Context, format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(ctx, l.config.FatalLevel(), format, args...) } // Error starts a new error level log message. func (l *Logger) Error(text string) *Message { + if l == nil { + return nil + } return l.NewMessage(context.Background(), l.config.ErrorLevel(), text) } // ErrorAt starts a new error level log message with the given timestamp. func (l *Logger) ErrorAt(timestamp time.Time, text string) *Message { + if l == nil { + return nil + } return l.NewMessageAt(context.Background(), timestamp, l.config.ErrorLevel(), text) } // ErrorCtx starts a new error level log message with the given context. func (l *Logger) ErrorCtx(ctx context.Context, text string) *Message { + if l == nil { + return nil + } return l.NewMessage(ctx, l.config.ErrorLevel(), text) } // Errorf uses fmt.Errorf underneath to support Go 1.13 wrapped error formatting with %w func (l *Logger) Errorf(format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessage(context.Background(), l.config.ErrorLevel(), fmt.Errorf(format, args...).Error()) } // ErrorfCtx uses fmt.Errorf underneath to support Go 1.13 wrapped error formatting with %w func (l *Logger) ErrorfCtx(ctx context.Context, format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessage(ctx, l.config.ErrorLevel(), fmt.Errorf(format, args...).Error()) } // Warn starts a new warn level log message. func (l *Logger) Warn(text string) *Message { + if l == nil { + return nil + } return l.NewMessage(context.Background(), l.config.WarnLevel(), text) } // WarnAt starts a new warn level log message with the given timestamp. func (l *Logger) WarnAt(timestamp time.Time, text string) *Message { + if l == nil { + return nil + } return l.NewMessageAt(context.Background(), timestamp, l.config.WarnLevel(), text) } // WarnCtx starts a new warn level log message with the given context. func (l *Logger) WarnCtx(ctx context.Context, text string) *Message { + if l == nil { + return nil + } return l.NewMessage(ctx, l.config.WarnLevel(), text) } // Warnf starts a new warn level log message formatted using fmt.Sprintf. func (l *Logger) Warnf(format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(context.Background(), l.config.WarnLevel(), format, args...) } // WarnfCtx starts a new warn level log message with context, formatted using fmt.Sprintf. func (l *Logger) WarnfCtx(ctx context.Context, format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(ctx, l.config.WarnLevel(), format, args...) } // Info starts a new info level log message. func (l *Logger) Info(text string) *Message { + if l == nil { + return nil + } return l.NewMessage(context.Background(), l.config.InfoLevel(), text) } // InfoAt starts a new info level log message with the given timestamp. func (l *Logger) InfoAt(timestamp time.Time, text string) *Message { + if l == nil { + return nil + } return l.NewMessageAt(context.Background(), timestamp, l.config.InfoLevel(), text) } // InfoCtx starts a new info level log message with the given context. func (l *Logger) InfoCtx(ctx context.Context, text string) *Message { + if l == nil { + return nil + } return l.NewMessage(ctx, l.config.InfoLevel(), text) } // Infof starts a new info level log message formatted using fmt.Sprintf. func (l *Logger) Infof(format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(context.Background(), l.config.InfoLevel(), format, args...) } // InfofCtx starts a new info level log message with context, formatted using fmt.Sprintf. func (l *Logger) InfofCtx(ctx context.Context, format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(ctx, l.config.InfoLevel(), format, args...) } // Debug starts a new debug level log message. func (l *Logger) Debug(text string) *Message { + if l == nil { + return nil + } return l.NewMessage(context.Background(), l.config.DebugLevel(), text) } // DebugAt starts a new debug level log message with the given timestamp. func (l *Logger) DebugAt(timestamp time.Time, text string) *Message { + if l == nil { + return nil + } return l.NewMessageAt(context.Background(), timestamp, l.config.DebugLevel(), text) } // DebugCtx starts a new debug level log message with the given context. func (l *Logger) DebugCtx(ctx context.Context, text string) *Message { + if l == nil { + return nil + } return l.NewMessage(ctx, l.config.DebugLevel(), text) } // Debugf starts a new debug level log message formatted using fmt.Sprintf. func (l *Logger) Debugf(format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(context.Background(), l.config.DebugLevel(), format, args...) } // DebugfCtx starts a new debug level log message with context, formatted using fmt.Sprintf. func (l *Logger) DebugfCtx(ctx context.Context, format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(ctx, l.config.DebugLevel(), format, args...) } // Trace starts a new trace level log message. func (l *Logger) Trace(text string) *Message { + if l == nil { + return nil + } return l.NewMessage(context.Background(), l.config.TraceLevel(), text) } // TraceAt starts a new trace level log message with the given timestamp. func (l *Logger) TraceAt(timestamp time.Time, text string) *Message { + if l == nil { + return nil + } return l.NewMessageAt(context.Background(), timestamp, l.config.TraceLevel(), text) } // TraceCtx starts a new trace level log message with the given context. func (l *Logger) TraceCtx(ctx context.Context, text string) *Message { + if l == nil { + return nil + } return l.NewMessage(ctx, l.config.TraceLevel(), text) } // Tracef starts a new trace level log message formatted using fmt.Sprintf. func (l *Logger) Tracef(format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(context.Background(), l.config.TraceLevel(), format, args...) } // TracefCtx starts a new trace level log message with context, formatted using fmt.Sprintf. func (l *Logger) TracefCtx(ctx context.Context, format string, args ...any) *Message { + if l == nil { + return nil + } return l.NewMessagef(ctx, l.config.TraceLevel(), format, args...) } diff --git a/logger_test.go b/logger_test.go index bd5f208..8e51be8 100644 --- a/logger_test.go +++ b/logger_test.go @@ -564,3 +564,58 @@ func TestLogger_DuplicateKeyHandling(t *testing.T) { // Verify only one occurrence of the key assert.Equal(t, 1, strings.Count(output, "key="), "key should appear exactly once") } + +// TestLogger_NilSafeLevelMethods verifies the nil-safe contract documented in +// doc.go: a nil *Logger must not panic on the level shortcut methods. Each +// method reads the level from l.config, so a missing nil guard dereferences a +// nil pointer. See https://github.com/domonda/golog issue: nil logger panic. +func TestLogger_NilSafeLevelMethods(t *testing.T) { + var logger *Logger + ctx := context.Background() + now := time.Now() + err := context.Canceled + + assert.NotPanics(t, func() { + // Base level methods (the documented example: log.Info(...).Log()). + logger.Trace("msg").Log() + logger.Debug("msg").Log() + logger.Info("msg").Log() + logger.Warn("msg").Log() + logger.Error("msg").Err(err).Log() + logger.Fatal("msg").Log() + + // At variants. + logger.TraceAt(now, "msg").Log() + logger.DebugAt(now, "msg").Log() + logger.InfoAt(now, "msg").Log() + logger.WarnAt(now, "msg").Log() + logger.ErrorAt(now, "msg").Log() + + // Ctx variants. + logger.TraceCtx(ctx, "msg").Log() + logger.DebugCtx(ctx, "msg").Log() + logger.InfoCtx(ctx, "msg").Log() + logger.WarnCtx(ctx, "msg").Log() + logger.ErrorCtx(ctx, "msg").Log() + logger.FatalCtx(ctx, "msg").Log() + + // Formatted variants. + logger.Tracef("msg %d", 1).Log() + logger.Debugf("msg %d", 1).Log() + logger.Infof("msg %d", 1).Log() + logger.Warnf("msg %d", 1).Log() + logger.Errorf("msg %w", err).Log() + logger.Fatalf("msg %d", 1).Log() + + // Formatted context variants. + logger.TracefCtx(ctx, "msg %d", 1).Log() + logger.DebugfCtx(ctx, "msg %d", 1).Log() + logger.InfofCtx(ctx, "msg %d", 1).Log() + logger.WarnfCtx(ctx, "msg %d", 1).Log() + logger.ErrorfCtx(ctx, "msg %w", err).Log() + logger.FatalfCtx(ctx, "msg %d", 1).Log() + }) + + // A nil logger must return a nil Message so chained field methods stay safe. + assert.Nil(t, logger.Info("msg")) +} diff --git a/logsentry/go.mod b/logsentry/go.mod index 123f972..04374f8 100644 --- a/logsentry/go.mod +++ b/logsentry/go.mod @@ -18,5 +18,5 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.37.0 // indirect ) diff --git a/logsentry/go.sum b/logsentry/go.sum index ebb824b..f4bd45b 100644 --- a/logsentry/go.sum +++ b/logsentry/go.sum @@ -32,7 +32,6 @@ golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=