bumping google.golang.org/genproto bbf56f3...83a465c: > 83a465c chore(all): update all (# 1070) > 419f025 chore(all): update actions/github-script action to v7 (# 1071) bumping google.golang.org/genproto/googleapis/api bbf56f3...83a465c: > 83a465c chore(all): update all (# 1070) > 419f025 chore(all): update actions/github-script action to v7 (# 1071) bumping github.com/google/uuid 8de8764...4d47f8e: > 4d47f8e chore(master): release 1.5.0 (# 145) > 9ee7366 feat: Validate UUID without creating new UUID (# 141) > b35aa6a add uuid version 6 and 7 (# 139) bumping knative.dev/pkg 9386ad6...03265f7: > 03265f7 Bump golang.org/x/tools from 0.16.0 to 0.16.1 (# 2917) > 155eba4 Bump google.golang.org/api from 0.153.0 to 0.154.0 (# 2920) > d71ca9a Bump cloud.google.com/go/storage from 1.35.1 to 1.36.0 (# 2918) > 5413322 Bump github.com/google/uuid from 1.4.0 to 1.5.0 (# 2916) > 4914c47 Bump google.golang.org/api from 0.152.0 to 0.153.0 (# 2914) > 9ad79ff Bump actions/setup-go from 4 to 5 (# 2913) bumping golang.org/x/tools a9ef4cf...2acb2e6: > 2acb2e6 gopls/internal/test/marker: minor clean up of marker test doc > 28b92af internal/typeparams: eliminate remainining compatibility shims > ee35f8e gopls/internal/lsp/source: hovering over broken packages is not an error > 67611a1 internal/typeparams: eliminate type aliases > 23c86e8 internal/typeparams: delete const Enabled=true and simplify > e46688f gopls/internal/analysis/fillstruct: don't panic with invalid fields > 8bd7553 gopls/internal/util/goversion: warn about EOL for Go 1.18 > bc9cd15 gopls/internal/settings: remove MemoryMode option > bbc30f1 gopls/protocol: Allow AnnotatedTextEdits > f40889d gopls/internal/analysis/stubmethods: fix OOB panic in fromValueSpec > 113a081 gopls/internal/lsp/protocol: report panics in RPC goroutines > 0640701 gopls/internal: don't bug.Report when given an invalid position > 90c60a2 gopls/internal/lsp/source: forgive inliner failures in ill-typed code > 6823dc4 go/ssa: tidy the docs > f580f77 cmd/deadcode: publish internal/cmd/deadcode > 7590aeb gopls/internal/lsp/source: improve method hover > 8e821ab gopls/internal/server: tidy imports, fix build > 1b28e5d gopls/internal/lsp/protocol: eliminate NonstandardRequest > 1153928 go/packages: improve docs of 'go list' and GOPACKAGESDRIVER > c4e846a gopls/internal/analysis: improve analyzer docs > 003c81d internal/diff: eliminate duplicate code with generics > 5e672e2 gopls/internal/lsp/source: add test for rename spurious shadowing > 9dce7c1 Revert "gopls/internal/server: rewrite the server diagnostic tracking" > c8cb6a3 gopls/doc: add a note updating the original design doc > 184d2a7 gopls/internal/server: rewrite the server diagnostic tracking > 625a693 gopls/internal/lsp/cache: move gc_details toggle state to the snapshot > 7e4a1ff internal/lsp/command: allow //go:generate command access network > c7cadca gopls/internal/cmd: remove gopls.run_vulncheck example > 79d4e32 gopls/internal/lsp/cache/parsego: clamp positions when fixing statements > 58556d1 gopls/internal/lsp/protocol/generate: restore indentation > c4f958a gopls/internal/cmd: add 'execute' command > d146c60 gopls/internal/test/marker: move ExitIfSmallMachine back to TestMain > b74ea80 gopls/internal/lsp/source/completion: support more postfix completion for map and slice (len, for, forr) > ca243bd gopls/internal/server: simplify snapshot diagnostics > c0305a8 gopls/internal/lsp/server: simplify logic for diagnosing changed files > ee21c63 gopls/internal/test: move the tests > 83512c4 internal/lsp/source/completion/snippet: move from lsp/snippet > c21f267 gopls/internal/lsp: remove lsp path segment > 5995d31 gopls/internal/util: more renamings > 8c1b662 gopls/internal/analysis: rename lsp/analysis -> analysis > d599a86 gopls: move marker tests to internal/test/marker > 6dd3b2b gopls/internal/server: superficial clean up of diagnostics > 5fb3132 gopls/internal/lsp: delete dead code > 1b1e4da internal/imports: remove duplicate unused imports > 42c4470 gopls/protocol/generate: files to generate lsp stubs > f291bf8 gopls: update lsp stubs > db5acf6 gopls/internal/util: move lru,frob,bug,{ast,types,path}util here > 147b88d internal/server: factor protocol.NonNilSlice > 6e052bb gopls/internal/server: rename from lsp > 30c5d24 gopls/internal/lsp/cache/metadata: rename Metadata to Package > 8cd8121 gopls/internal/regtest: avoid file segments ending in a period > c8019b9 gopls/internal/lsp/cache: move ignoreFilter onto View > f930faf gopls/internal/lsp: remove analysisMode, and other unused params > c04fbc0 gopls/internal/lsp: cleanups to view logic > 64f8a68 gopls: remove unnecessary conversions > 12b4de1 gopls/internal/lsp/source: fix hover for objects in unsafe > 4cbc053 gopls/internal/lsp: invert the import between cache and source > ad85a67 gopls/internal/lsp/source: eliminate AnalyzerErrorKind > 2c02506 gopls/internal/lsp/cache: clean up more references to the source package > 0454d64 gopls/internal/lsp/source/typerefs: move to ../cache/typerefs > d2ba90b gopls/internal/lsp/source/methodsets: move to ../cache/methodsets > e3785e8 gopls/internal/lsp/source/xrefs: move to ../cache/xrefs > ef6cba8 gopls/internal/lsp/cache: move parsing types / algorithm to parsego > b9a0354 gopls/internal/lsp/cache: split metadata related types into a package > 87aa71c gopls/internal/lsp/cache: move load diagnostics to packageHandle > b405134 gopls/internal/telemetry/cmd/stacks: skip gopls@devel > 73e70de gopls/internal/lsp: factor showMessage calls > 1896879 gopls: enable telemetry uploading only with 'serve' > eee280c gopls/internal/lsp/protocol: reject non-'file' scheme DocumentURIs > 72ee4f8 gopls/internal/lsp/source/extract: don't assume Scope.Pos > 781b218 gopls/internal/lsp/source: move NodeContains into the astutil pkg > 2d18617 gopls/internal/lsp/cache: clean up unnecessary type checking indirection > 98d026b go/analysis/analysistest: allow End=0 (meaning End=Pos) in RunWithSuggestedFixes > 67f59a0 gopls/internal/lsp/cache: don't rebuild filters for each modcache dir bumping google.golang.org/api c18c15e...08bc733: > 08bc733 chore(main): release 0.154.0 (# 2292) > 861bc79 chore(all): update module github.com/google/go-github/v52 to v57 (# 2289) > be0daf0 feat(all): auto-regenerate discovery clients (# 2300) > 432541d chore(all): update all (# 2288) > d27a8e0 feat(all): auto-regenerate discovery clients (# 2298) > e5e778a feat(all): auto-regenerate discovery clients (# 2297) > 161a8e5 feat(all): auto-regenerate discovery clients (# 2295) > c059038 feat(all): auto-regenerate discovery clients (# 2294) > cefa0cd feat(all): auto-regenerate discovery clients (# 2293) > 46421d4 feat(transport): add OpenTelemetry context propagation (# 2127) > 81cfcf1 chore(main): release 0.153.0 (# 2277) > 4aaad57 feat: update workloadmanager api manually (# 2282) > 0a6ebe4 feat(all): auto-regenerate discovery clients (# 2291) > 9f55397 feat(all): auto-regenerate discovery clients (# 2290) > 4148872 feat(all): auto-regenerate discovery clients (# 2287) > 7897739 feat(all): auto-regenerate discovery clients (# 2286) > bdc87de feat(all): auto-regenerate discovery clients (# 2283) > e648a9d feat: expose experimental universe-related options (# 2264) > e8e2895 feat(all): auto-regenerate discovery clients (# 2280) > d07fe32 feat(all): auto-regenerate discovery clients (# 2279) > 510f0f9 feat(all): auto-regenerate discovery clients (# 2276) bumping github.com/go-logr/logr 4da5305...8adefbe: > 8adefbe docs: interoperability with slog > ebabbb9 build(deps): bump github/codeql-action from 2.22.3 to 2.22.4 > 9c361f0 build(deps): bump actions/checkout from 4.1.0 to 4.1.1 > d9b2b78 Merge pull request # 229 from go-logr/dependabot/github_actions/github/codeql-action-2.22.3 > 2ea8628 Merge pull request # 228 from go-logr/dependabot/github_actions/github/codeql-action-2.22.0 > 91cec29 build(deps): bump github/codeql-action from 2.22.0 to 2.22.3 > 37a4f55 Merge pull request # 227 from go-logr/dependabot/github_actions/ossf/scorecard-action-2.3.0 > ecf310c build(deps): bump github/codeql-action from 2.21.9 to 2.22.0 > 1d1c415 Merge pull request # 226 from go-logr/dependabot/github_actions/github/codeql-action-2.21.9 > d73e05e build(deps): bump ossf/scorecard-action from 2.2.0 to 2.3.0 > 2472ace Merge pull request # 225 from go-logr/dependabot/github_actions/actions/checkout-4.1.0 > 83882e3 build(deps): bump github/codeql-action from 2.21.8 to 2.21.9 > 152caf3 Merge pull request # 224 from go-logr/dependabot/github_actions/github/codeql-action-2.21.8 > 2c41c0d build(deps): bump actions/checkout from 4.0.0 to 4.1.0 > 9e1091d Merge pull request # 223 from go-logr/dependabot/github_actions/github/codeql-action-2.21.7 > 148c77e build(deps): bump github/codeql-action from 2.21.7 to 2.21.8 > b75243e Merge pull request # 221 from go-logr/dependabot/github_actions/actions/upload-artifact-3.1.3 > cfad582 build(deps): bump github/codeql-action from 2.21.5 to 2.21.7 > 3ad2e7a Merge pull request # 220 from go-logr/dependabot/github_actions/actions/checkout-4.0.0 > 91fb740 build(deps): bump actions/upload-artifact from 3.1.2 to 3.1.3 > 5dda621 Merge pull request # 219 from thockin/master > 459a36c build(deps): bump actions/checkout from 3.6.0 to 4.0.0 > eb5bf2d Merge pull request # 218 from thockin/calldepth_enabled_consistent > 314b74e test: eliminate helper func > d0d35d7 Merge pull request # 217 from go-logr/dependabot/github_actions/github/codeql-action-2.21.5 > af7b2b7 Use same call depth for Enabled, Info, Error > eea184c Merge pull request # 214 from go-logr/dependabot/github_actions/actions/checkout-3.6.0 > b937b2a build(deps): bump github/codeql-action from 2.21.4 to 2.21.5 > 7bf6c82 slogr: add SlogSink > a42871c build(deps): bump actions/checkout from 3.5.3 to 3.6.0 > 358130c slogr: restore original backend when converting back and forth > b785b9f slogr: add glue code for logging to slog.Handler and with slog.Logger > 50e47a6 add Logger.GetV > ad8352a build(deps): bump github/codeql-action from 2.21.3 to 2.21.4 > 533f1c5 build(deps): bump golangci/golangci-lint-action from 3.6.0 to 3.7.0 > bde80be Merge pull request # 206 from go-logr/dependabot/github_actions/actions/setup-go-4.1.0 > 043bb14 Merge pull request # 207 from go-logr/dependabot/github_actions/github/codeql-action-2.21.3 > 9bd8235 build(deps): bump actions/setup-go from 4.0.1 to 4.1.0 > 4c4e60e Merge pull request # 203 from thockin/bump_go_1_18 > 7ad7fbc build(deps): bump github/codeql-action from 2.21.2 to 2.21.3 > b8912c3 build(deps): bump github/codeql-action from 2.21.0 to 2.21.2 > 10d92fc Search and replace interface{} -> any > 4e7199f build(deps): bump github/codeql-action from 2.20.4 to 2.21.0 > 7078b55 Bump go versions to 1.18+ > ff91da8 build(deps): bump github/codeql-action from 2.20.3 to 2.20.4 > 9cbcf50 Merge pull request # 191 from go-logr/dependabot/github_actions/github/codeql-action-2.20.3 > 7ece47f Merge pull request # 190 from go-logr/dependabot/github_actions/actions/setup-go-4.0.1 > a548a7e build(deps): bump github/codeql-action from 2.20.1 to 2.20.3 > 0ad2c4b Merge pull request # 189 from pnacht/pinned-deps > dac64b6 build(deps): bump actions/setup-go from 3.5.0 to 4.0.1 > 73d5d25 README: show of OpenSSF Scorecard badge > dc28fa9 Update scorecard workflow Actions > 4e0403a Create scorecard.yml > 3372189 Add dependabot to monitor Actions > 6134c6a docs: explain relationship between Logger{} and Discard() > 9e8bbfc Hash-pin workflow Actions > 5d57712 Update security email > c331f48 Add a security policy (with placeholder email) > 4058b9a Add write permissions to assign.yaml > fef7432 Add top-level read-only token permissions > 82f9555 Merge pull request # 173 from thockin/master > d1187da funcr: better tests for JSON output > 930897a Fix lint failures > b95cdeb funcr: Fix wrong prefix in JSON mode bumping google.golang.org/genproto/googleapis/rpc 83a465c...3a041ad: > 3a041ad chore(all): update all to 83a465c (# 1072) Signed-off-by: Knative Automation <automation@knative.team> |
||
---|---|---|
.. | ||
.golangci.yaml | ||
CHANGELOG.md | ||
CONTRIBUTING.md | ||
LICENSE | ||
README.md | ||
SECURITY.md | ||
discard.go | ||
logr.go |
README.md
A minimal logging API for Go
logr offers an(other) opinion on how Go programs and libraries can do logging without becoming coupled to a particular logging implementation. This is not an implementation of logging - it is an API. In fact it is two APIs with two different sets of users.
The Logger
type is intended for application and library authors. It provides
a relatively small API which can be used everywhere you want to emit logs. It
defers the actual act of writing logs (to files, to stdout, or whatever) to the
LogSink
interface.
The LogSink
interface is intended for logging library implementers. It is a
pure interface which can be implemented by logging frameworks to provide the actual logging
functionality.
This decoupling allows application and library developers to write code in
terms of logr.Logger
(which has very low dependency fan-out) while the
implementation of logging is managed "up stack" (e.g. in or near main()
.)
Application developers can then switch out implementations as necessary.
Many people assert that libraries should not be logging, and as such efforts like this are pointless. Those people are welcome to convince the authors of the tens-of-thousands of libraries that DO write logs that they are all wrong. In the meantime, logr takes a more practical approach.
Typical usage
Somewhere, early in an application's life, it will make a decision about which logging library (implementation) it actually wants to use. Something like:
func main() {
// ... other setup code ...
// Create the "root" logger. We have chosen the "logimpl" implementation,
// which takes some initial parameters and returns a logr.Logger.
logger := logimpl.New(param1, param2)
// ... other setup code ...
Most apps will call into other libraries, create structures to govern the flow,
etc. The logr.Logger
object can be passed to these other libraries, stored
in structs, or even used as a package-global variable, if needed. For example:
app := createTheAppObject(logger)
app.Run()
Outside of this early setup, no other packages need to know about the choice of
implementation. They write logs in terms of the logr.Logger
that they
received:
type appObject struct {
// ... other fields ...
logger logr.Logger
// ... other fields ...
}
func (app *appObject) Run() {
app.logger.Info("starting up", "timestamp", time.Now())
// ... app code ...
Background
If the Go standard library had defined an interface for logging, this project probably would not be needed. Alas, here we are.
When the Go developers started developing such an interface with slog, they adopted some of the logr design but also left out some parts and changed others:
Feature | logr | slog |
---|---|---|
High-level API | Logger (passed by value) |
Logger (passed by pointer) |
Low-level API | LogSink |
Handler |
Stack unwinding | done by LogSink |
done by Logger |
Skipping helper functions | WithCallDepth , WithCallStackHelper |
not supported by Logger |
Generating a value for logging on demand | Marshaler |
LogValuer |
Log levels | >= 0, higher meaning "less important" | positive and negative, with 0 for "info" and higher meaning "more important" |
Error log entries | always logged, don't have a verbosity level | normal log entries with level >= LevelError |
Passing logger via context | NewContext , FromContext |
no API |
Adding a name to a logger | WithName |
no API |
Modify verbosity of log entries in a call chain | V |
no API |
Grouping of key/value pairs | not supported | WithGroup , GroupValue |
The high-level slog API is explicitly meant to be one of many different APIs
that can be layered on top of a shared slog.Handler
. logr is one such
alternative API, with interoperability provided by the slogr
package.
Inspiration
Before you consider this package, please read this blog post by the inimitable Dave Cheney. We really appreciate what he has to say, and it largely aligns with our own experiences.
Differences from Dave's ideas
The main differences are:
- Dave basically proposes doing away with the notion of a logging API in favor
of
fmt.Printf()
. We disagree, especially when you consider things like output locations, timestamps, file and line decorations, and structured logging. This package restricts the logging API to just 2 types of logs: info and error.
Info logs are things you want to tell the user which are not errors. Error
logs are, well, errors. If your code receives an error
from a subordinate
function call and is logging that error
and not returning it, use error
logs.
- Verbosity-levels on info logs. This gives developers a chance to indicate arbitrary grades of importance for info logs, without assigning names with semantic meaning such as "warning", "trace", and "debug." Superficially this may feel very similar, but the primary difference is the lack of semantics. Because verbosity is a numerical value, it's safe to assume that an app running with higher verbosity means more (and less important) logs will be generated.
Implementations (non-exhaustive)
There are implementations for the following logging libraries:
- a function (can bridge to non-structured libraries): funcr
- a testing.T (for use in Go tests, with JSON-like output): testr
- github.com/google/glog: glogr
- k8s.io/klog (for Kubernetes): klogr
- a testing.T (with klog-like text output): ktesting
- go.uber.org/zap: zapr
- log (the Go standard library logger): stdr
- github.com/sirupsen/logrus: logrusr
- github.com/wojas/genericr: genericr (makes it easy to implement your own backend)
- logfmt (Heroku style logging): logfmtr
- github.com/rs/zerolog: zerologr
- github.com/go-kit/log: gokitlogr (also compatible with github.com/go-kit/kit/log since v0.12.0)
- bytes.Buffer (writing to a buffer): bufrlogr (useful for ensuring values were logged, like during testing)
slog interoperability
Interoperability goes both ways, using the logr.Logger
API with a slog.Handler
and using the slog.Logger
API with a logr.LogSink
. slogr provides NewLogr
and
NewSlogHandler
API calls to convert between a logr.Logger
and a slog.Handler
.
As usual, slog.New
can be used to wrap such a slog.Handler
in the high-level
slog API. slogr
itself leaves that to the caller.
Using a logr.Sink
as backend for slog
Ideally, a logr sink implementation should support both logr and slog by
implementing both the normal logr interface(s) and slogr.SlogSink
. Because
of a conflict in the parameters of the common Enabled
method, it is not
possible to implement both slog.Handler and logr.Sink in the same
type.
If both are supported, log calls can go from the high-level APIs to the backend
without the need to convert parameters. NewLogr
and NewSlogHandler
can
convert back and forth without adding additional wrappers, with one exception:
when Logger.V
was used to adjust the verbosity for a slog.Handler
, then
NewSlogHandler
has to use a wrapper which adjusts the verbosity for future
log calls.
Such an implementation should also support values that implement specific
interfaces from both packages for logging (logr.Marshaler
, slog.LogValuer
,
slog.GroupValue
). logr does not convert those.
Not supporting slog has several drawbacks:
- Recording source code locations works correctly if the handler gets called
through
slog.Logger
, but may be wrong in other cases. That's because alogr.Sink
does its own stack unwinding instead of using the program counter provided by the high-level API. - slog levels <= 0 can be mapped to logr levels by negating the level without a
loss of information. But all slog levels > 0 (e.g.
slog.LevelWarning
as used byslog.Logger.Warn
) must be mapped to 0 before calling the sink because logr does not support "more important than info" levels. - The slog group concept is supported by prefixing each key in a key/value pair with the group names, separated by a dot. For structured output like JSON it would be better to group the key/value pairs inside an object.
- Special slog values and interfaces don't work as expected.
- The overhead is likely to be higher.
These drawbacks are severe enough that applications using a mixture of slog and logr should switch to a different backend.
Using a slog.Handler
as backend for logr
Using a plain slog.Handler
without support for logr works better than the
other direction:
- All logr verbosity levels can be mapped 1:1 to their corresponding slog level by negating them.
- Stack unwinding is done by the
slogr.SlogSink
and the resulting program counter is passed to theslog.Handler
. - Names added via
Logger.WithName
are gathered and recorded in an additional attribute withlogger
as key and the names separated by slash as value. Logger.Error
is turned into a log record withslog.LevelError
as level and an additional attribute witherr
as key, if an error was provided.
The main drawback is that logr.Marshaler
will not be supported. Types should
ideally support both logr.Marshaler
and slog.Valuer
. If compatibility
with logr implementations without slog support is not important, then
slog.Valuer
is sufficient.
Context support for slog
Storing a logger in a context.Context
is not supported by
slog. logr.NewContext
and logr.FromContext
can be used with slog like this
to fill this gap:
func HandlerFromContext(ctx context.Context) slog.Handler {
logger, err := logr.FromContext(ctx)
if err == nil {
return slogr.NewSlogHandler(logger)
}
return slog.Default().Handler()
}
func ContextWithHandler(ctx context.Context, handler slog.Handler) context.Context {
return logr.NewContext(ctx, slogr.NewLogr(handler))
}
The downside is that storing and retrieving a slog.Handler
needs more
allocations compared to using a logr.Logger
. Therefore the recommendation is
to use the logr.Logger
API in code which uses contextual logging.
FAQ
Conceptual
Why structured logging?
-
Structured logs are more easily queryable: Since you've got key-value pairs, it's much easier to query your structured logs for particular values by filtering on the contents of a particular key -- think searching request logs for error codes, Kubernetes reconcilers for the name and namespace of the reconciled object, etc.
-
Structured logging makes it easier to have cross-referenceable logs: Similarly to searchability, if you maintain conventions around your keys, it becomes easy to gather all log lines related to a particular concept.
-
Structured logs allow better dimensions of filtering: if you have structure to your logs, you've got more precise control over how much information is logged -- you might choose in a particular configuration to log certain keys but not others, only log lines where a certain key matches a certain value, etc., instead of just having v-levels and names to key off of.
-
Structured logs better represent structured data: sometimes, the data that you want to log is inherently structured (think tuple-link objects.) Structured logs allow you to preserve that structure when outputting.
Why V-levels?
V-levels give operators an easy way to control the chattiness of log operations. V-levels provide a way for a given package to distinguish the relative importance or verbosity of a given log message. Then, if a particular logger or package is logging too many messages, the user of the package can simply change the v-levels for that library.
Why not named levels, like Info/Warning/Error?
Read Dave Cheney's post. Then read Differences from Dave's ideas.
Why not allow format strings, too?
Format strings negate many of the benefits of structured logs:
-
They're not easily searchable without resorting to fuzzy searching, regular expressions, etc.
-
They don't store structured data well, since contents are flattened into a string.
-
They're not cross-referenceable.
-
They don't compress easily, since the message is not constant.
(Unless you turn positional parameters into key-value pairs with numerical keys, at which point you've gotten key-value logging with meaningless keys.)
Practical
Why key-value pairs, and not a map?
Key-value pairs are much easier to optimize, especially around allocations. Zap (a structured logger that inspired logr's interface) has performance measurements that show this quite nicely.
While the interface ends up being a little less obvious, you get
potentially better performance, plus avoid making users type
map[string]string{}
every time they want to log.
What if my V-levels differ between libraries?
That's fine. Control your V-levels on a per-logger basis, and use the
WithName
method to pass different loggers to different libraries.
Generally, you should take care to ensure that you have relatively consistent V-levels within a given logger, however, as this makes deciding on what verbosity of logs to request easier.
But I really want to use a format string!
That's not actually a question. Assuming your question is "how do I convert my mental model of logging with format strings to logging with constant messages":
-
Figure out what the error actually is, as you'd write in a TL;DR style, and use that as a message.
-
For every place you'd write a format specifier, look to the word before it, and add that as a key value pair.
For instance, consider the following examples (all taken from spots in the Kubernetes codebase):
-
klog.V(4).Infof("Client is returning errors: code %v, error %v", responseCode, err)
becomeslogger.Error(err, "client returned an error", "code", responseCode)
-
klog.V(4).Infof("Got a Retry-After %ds response for attempt %d to %v", seconds, retries, url)
becomeslogger.V(4).Info("got a retry-after response when requesting url", "attempt", retries, "after seconds", seconds, "url", url)
If you really must use a format string, use it in a key's value, and
call fmt.Sprintf
yourself. For instance: log.Printf("unable to reflect over type %T")
becomes logger.Info("unable to reflect over type", "type", fmt.Sprintf("%T"))
. In general though, the cases where
this is necessary should be few and far between.
How do I choose my V-levels?
This is basically the only hard constraint: increase V-levels to denote more verbose or more debug-y logs.
Otherwise, you can start out with 0
as "you always want to see this",
1
as "common logging that you might possibly want to turn off", and
10
as "I would like to performance-test your log collection stack."
Then gradually choose levels in between as you need them, working your way down from 10 (for debug and trace style logs) and up from 1 (for chattier info-type logs). For reference, slog pre-defines -4 for debug logs (corresponds to 4 in logr), which matches what is recommended for Kubernetes.
How do I choose my keys?
Keys are fairly flexible, and can hold more or less any string value. For best compatibility with implementations and consistency with existing code in other projects, there are a few conventions you should consider.
- Make your keys human-readable.
- Constant keys are generally a good idea.
- Be consistent across your codebase.
- Keys should naturally match parts of the message string.
- Use lower case for simple keys and lowerCamelCase for more complex ones. Kubernetes is one example of a project that has adopted that convention.
While key names are mostly unrestricted (and spaces are acceptable), it's generally a good idea to stick to printable ascii characters, or at least match the general character set of your log lines.
Why should keys be constant values?
The point of structured logging is to make later log processing easier. Your
keys are, effectively, the schema of each log message. If you use different
keys across instances of the same log line, you will make your structured logs
much harder to use. Sprintf()
is for values, not for keys!
Why is this not a pure interface?
The Logger type is implemented as a struct in order to allow the Go compiler to
optimize things like high-V Info
logs that are not triggered. Not all of
these implementations are implemented yet, but this structure was suggested as
a way to ensure they can be implemented. All of the real work is behind the
LogSink
interface.