Import upstream version 1.0.0
Debian Janitor
2 years ago
0 | # CHANGELOG | |
1 | ||
2 | ## v1.0.0-rc1 | |
3 | ||
4 | This is the first logged release. Major changes (including breaking changes) | |
5 | have occurred since earlier tags. |
0 | # Contributing | |
1 | ||
2 | Logr is open to pull-requests, provided they fit within the intended scope of | |
3 | the project. Specifically, this library aims to be VERY small and minimalist, | |
4 | with no external dependencies. | |
5 | ||
6 | ## Compatibility | |
7 | ||
8 | This project intends to follow [semantic versioning](http://semver.org) and | |
9 | is very strict about compatibility. Any proposed changes MUST follow those | |
10 | rules. | |
11 | ||
12 | ## Performance | |
13 | ||
14 | As a logging library, logr must be as light-weight as possible. Any proposed | |
15 | code change must include results of running the [benchmark](./benchmark) | |
16 | before and after the change. |
0 | # A more minimal logging API for Go | |
0 | # A minimal logging API for Go | |
1 | ||
2 | logr offers an(other) opinion on how Go programs and libraries can do logging | |
3 | without becoming coupled to a particular logging implementation. This is not | |
4 | an implementation of logging - it is an API. In fact it is two APIs with two | |
5 | different sets of users. | |
6 | ||
7 | The `Logger` type is intended for application and library authors. It provides | |
8 | a relatively small API which can be used everywhere you want to emit logs. It | |
9 | defers the actual act of writing logs (to files, to stdout, or whatever) to the | |
10 | `LogSink` interface. | |
11 | ||
12 | The `LogSink` interface is intended for logging library implementers. It is a | |
13 | pure interface which can be implemented by to provide the actual logging | |
14 | functionality. | |
15 | ||
16 | This decoupling allows application and library developers to write code in | |
17 | terms of `logr.Logger` (which has very low dependency fan-out) while the | |
18 | implementation of logging is managed "up stack" (e.g. in or near `main()`.) | |
19 | Application developers can then switch out implementations as necessary. | |
20 | ||
21 | Many people assert that libraries should not be logging, and as such efforts | |
22 | like this are pointless. Those people are welcome to convince the authors of | |
23 | the tens-of-thousands of libraries that *DO* write logs that they are all | |
24 | wrong. In the meantime, logr takes a more practical approach. | |
25 | ||
26 | ## Typical usage | |
27 | ||
28 | Somewhere, early in an application's life, it will make a decision about which | |
29 | logging library (implementation) it actually wants to use. Something like: | |
30 | ||
31 | ``` | |
32 | func main() { | |
33 | // ... other setup code ... | |
34 | ||
35 | // Create the "root" logger. We have chosen the "logimpl" implementation, | |
36 | // which takes some initial parameters and returns a logr.Logger. | |
37 | logger := logimpl.New(param1, param2) | |
38 | ||
39 | // ... other setup code ... | |
40 | ``` | |
41 | ||
42 | Most apps will call into other libraries, create structures to govern the flow, | |
43 | etc. The `logr.Logger` object can be passed to these other libraries, stored | |
44 | in structs, or even used as a package-global variable, if needed. For example: | |
45 | ||
46 | ``` | |
47 | app := createTheAppObject(logger) | |
48 | app.Run() | |
49 | ``` | |
50 | ||
51 | Outside of this early setup, no other packages need to know about the choice of | |
52 | implementation. They write logs in terms of the `logr.Logger` that they | |
53 | received: | |
54 | ||
55 | ``` | |
56 | type appObject struct { | |
57 | // ... other fields ... | |
58 | logger logr.Logger | |
59 | // ... other fields ... | |
60 | } | |
61 | ||
62 | func (app *appObject) Run() { | |
63 | app.logger.Info("starting up", "timestamp", time.Now()) | |
64 | ||
65 | // ... app code ... | |
66 | ``` | |
67 | ||
68 | ## Background | |
69 | ||
70 | If the Go standard library had defined an interface for logging, this project | |
71 | probably would not be needed. Alas, here we are. | |
72 | ||
73 | ### Inspiration | |
1 | 74 | |
2 | 75 | Before you consider this package, please read [this blog post by the |
3 | inimitable Dave Cheney][warning-makes-no-sense]. I really appreciate what | |
4 | he has to say, and it largely aligns with my own experiences. Too many | |
5 | choices of levels means inconsistent logs. | |
6 | ||
7 | This package offers a purely abstract interface, based on these ideas but with | |
8 | a few twists. Code can depend on just this interface and have the actual | |
9 | logging implementation be injected from callers. Ideally only `main()` knows | |
10 | what logging implementation is being used. | |
11 | ||
12 | # Differences from Dave's ideas | |
76 | inimitable Dave Cheney][warning-makes-no-sense]. We really appreciate what | |
77 | he has to say, and it largely aligns with our own experiences. | |
78 | ||
79 | ### Differences from Dave's ideas | |
13 | 80 | |
14 | 81 | The main differences are: |
15 | 82 | |
16 | 1) Dave basically proposes doing away with the notion of a logging API in favor | |
17 | of `fmt.Printf()`. I disagree, especially when you consider things like output | |
18 | locations, timestamps, file and line decorations, and structured logging. I | |
19 | restrict the API to just 2 types of logs: info and error. | |
83 | 1. Dave basically proposes doing away with the notion of a logging API in favor | |
84 | of `fmt.Printf()`. We disagree, especially when you consider things like output | |
85 | locations, timestamps, file and line decorations, and structured logging. This | |
86 | package restricts the logging API to just 2 types of logs: info and error. | |
20 | 87 | |
21 | 88 | Info logs are things you want to tell the user which are not errors. Error |
22 | 89 | logs are, well, errors. If your code receives an `error` from a subordinate |
23 | 90 | function call and is logging that `error` *and not returning it*, use error |
24 | 91 | logs. |
25 | 92 | |
26 | 2) Verbosity-levels on info logs. This gives developers a chance to indicate | |
93 | 2. Verbosity-levels on info logs. This gives developers a chance to indicate | |
27 | 94 | arbitrary grades of importance for info logs, without assigning names with |
28 | semantic meaning such as "warning", "trace", and "debug". Superficially this | |
95 | semantic meaning such as "warning", "trace", and "debug." Superficially this | |
29 | 96 | may feel very similar, but the primary difference is the lack of semantics. |
30 | 97 | Because verbosity is a numerical value, it's safe to assume that an app running |
31 | 98 | with higher verbosity means more (and less important) logs will be generated. |
32 | 99 | |
33 | This is a BETA grade API. | |
100 | ## Implementations (non-exhaustive) | |
34 | 101 | |
35 | 102 | There are implementations for the following logging libraries: |
36 | 103 | |
104 | - **a function**: [funcr](https://github.com/go-logr/logr/funcr) | |
37 | 105 | - **github.com/google/glog**: [glogr](https://github.com/go-logr/glogr) |
38 | 106 | - **k8s.io/klog**: [klogr](https://git.k8s.io/klog/klogr) |
39 | 107 | - **go.uber.org/zap**: [zapr](https://github.com/go-logr/zapr) |
40 | - **log** (the Go standard library logger): | |
41 | [stdr](https://github.com/go-logr/stdr) | |
108 | - **log** (the Go standard library logger): [stdr](https://github.com/go-logr/stdr) | |
42 | 109 | - **github.com/sirupsen/logrus**: [logrusr](https://github.com/bombsimon/logrusr) |
43 | 110 | - **github.com/wojas/genericr**: [genericr](https://github.com/wojas/genericr) (makes it easy to implement your own backend) |
44 | 111 | - **logfmt** (Heroku style [logging](https://www.brandur.org/logfmt)): [logfmtr](https://github.com/iand/logfmtr) |
45 | 112 | |
46 | # FAQ | |
47 | ||
48 | ## Conceptual | |
49 | ||
50 | ## Why structured logging? | |
51 | ||
52 | - **Structured logs are more easily queriable**: Since you've got | |
113 | ## FAQ | |
114 | ||
115 | ### Conceptual | |
116 | ||
117 | #### Why structured logging? | |
118 | ||
119 | - **Structured logs are more easily queryable**: Since you've got | |
53 | 120 | key-value pairs, it's much easier to query your structured logs for |
54 | 121 | particular values by filtering on the contents of a particular key -- |
55 | 122 | think searching request logs for error codes, Kubernetes reconcilers for |
56 | the name and namespace of the reconciled object, etc | |
57 | ||
58 | - **Structured logging makes it easier to have cross-referencable logs**: | |
123 | the name and namespace of the reconciled object, etc. | |
124 | ||
125 | - **Structured logging makes it easier to have cross-referenceable logs**: | |
59 | 126 | Similarly to searchability, if you maintain conventions around your |
60 | 127 | keys, it becomes easy to gather all log lines related to a particular |
61 | 128 | concept. |
62 | ||
129 | ||
63 | 130 | - **Structured logs allow better dimensions of filtering**: if you have |
64 | 131 | structure to your logs, you've got more precise control over how much |
65 | 132 | information is logged -- you might choose in a particular configuration |
66 | 133 | to log certain keys but not others, only log lines where a certain key |
67 | matches a certain value, etc, instead of just having v-levels and names | |
134 | matches a certain value, etc., instead of just having v-levels and names | |
68 | 135 | to key off of. |
69 | 136 | |
70 | 137 | - **Structured logs better represent structured data**: sometimes, the |
71 | 138 | data that you want to log is inherently structured (think tuple-link |
72 | objects). Structured logs allow you to preserve that structure when | |
139 | objects.) Structured logs allow you to preserve that structure when | |
73 | 140 | outputting. |
74 | 141 | |
75 | ## Why V-levels? | |
142 | #### Why V-levels? | |
76 | 143 | |
77 | 144 | **V-levels give operators an easy way to control the chattiness of log |
78 | 145 | operations**. V-levels provide a way for a given package to distinguish |
79 | 146 | the relative importance or verbosity of a given log message. Then, if |
80 | 147 | a particular logger or package is logging too many messages, the user |
81 | of the package can simply change the v-levels for that library. | |
82 | ||
83 | ## Why not more named levels, like Warning? | |
148 | of the package can simply change the v-levels for that library. | |
149 | ||
150 | #### Why not named levels, like Info/Warning/Error? | |
84 | 151 | |
85 | 152 | Read [Dave Cheney's post][warning-makes-no-sense]. Then read [Differences |
86 | 153 | from Dave's ideas](#differences-from-daves-ideas). |
87 | 154 | |
88 | ## Why not allow format strings, too? | |
155 | #### Why not allow format strings, too? | |
89 | 156 | |
90 | 157 | **Format strings negate many of the benefits of structured logs**: |
91 | 158 | |
92 | 159 | - They're not easily searchable without resorting to fuzzy searching, |
93 | regular expressions, etc | |
160 | regular expressions, etc. | |
94 | 161 | |
95 | 162 | - They don't store structured data well, since contents are flattened into |
96 | a string | |
97 | ||
98 | - They're not cross-referencable | |
99 | ||
100 | - They don't compress easily, since the message is not constant | |
101 | ||
102 | (unless you turn positional parameters into key-value pairs with numerical | |
163 | a string. | |
164 | ||
165 | - They're not cross-referenceable. | |
166 | ||
167 | - They don't compress easily, since the message is not constant. | |
168 | ||
169 | (Unless you turn positional parameters into key-value pairs with numerical | |
103 | 170 | keys, at which point you've gotten key-value logging with meaningless |
104 | keys) | |
105 | ||
106 | ## Practical | |
107 | ||
108 | ## Why key-value pairs, and not a map? | |
171 | keys.) | |
172 | ||
173 | ### Practical | |
174 | ||
175 | #### Why key-value pairs, and not a map? | |
109 | 176 | |
110 | 177 | Key-value pairs are *much* easier to optimize, especially around |
111 | 178 | allocations. Zap (a structured logger that inspired logr's interface) has |
116 | 183 | potentially better performance, plus avoid making users type |
117 | 184 | `map[string]string{}` every time they want to log. |
118 | 185 | |
119 | ## What if my V-levels differ between libraries? | |
186 | #### What if my V-levels differ between libraries? | |
120 | 187 | |
121 | 188 | That's fine. Control your V-levels on a per-logger basis, and use the |
122 | `WithName` function to pass different loggers to different libraries. | |
189 | `WithName` method to pass different loggers to different libraries. | |
123 | 190 | |
124 | 191 | Generally, you should take care to ensure that you have relatively |
125 | 192 | consistent V-levels within a given logger, however, as this makes deciding |
126 | 193 | on what verbosity of logs to request easier. |
127 | 194 | |
128 | ## But I *really* want to use a format string! | |
195 | #### But I really want to use a format string! | |
129 | 196 | |
130 | 197 | That's not actually a question. Assuming your question is "how do |
131 | 198 | I convert my mental model of logging with format strings to logging with |
132 | 199 | constant messages": |
133 | 200 | |
134 | 1. figure out what the error actually is, as you'd write in a TL;DR style, | |
135 | and use that as a message | |
201 | 1. Figure out what the error actually is, as you'd write in a TL;DR style, | |
202 | and use that as a message. | |
136 | 203 | |
137 | 204 | 2. For every place you'd write a format specifier, look to the word before |
138 | it, and add that as a key value pair | |
205 | it, and add that as a key value pair. | |
139 | 206 | |
140 | 207 | For instance, consider the following examples (all taken from spots in the |
141 | 208 | Kubernetes codebase): |
149 | 216 | response when requesting url", "attempt", retries, "after |
150 | 217 | seconds", seconds, "url", url)` |
151 | 218 | |
152 | If you *really* must use a format string, place it as a key value, and | |
153 | call `fmt.Sprintf` yourself -- for instance, `log.Printf("unable to | |
219 | If you *really* must use a format string, use it in a key's value, and | |
220 | call `fmt.Sprintf` yourself. For instance: `log.Printf("unable to | |
154 | 221 | reflect over type %T")` becomes `logger.Info("unable to reflect over |
155 | 222 | type", "type", fmt.Sprintf("%T"))`. In general though, the cases where |
156 | 223 | this is necessary should be few and far between. |
157 | 224 | |
158 | ## How do I choose my V-levels? | |
225 | #### How do I choose my V-levels? | |
159 | 226 | |
160 | 227 | This is basically the only hard constraint: increase V-levels to denote |
161 | 228 | more verbose or more debug-y logs. |
162 | 229 | |
163 | 230 | Otherwise, you can start out with `0` as "you always want to see this", |
164 | 231 | `1` as "common logging that you might *possibly* want to turn off", and |
165 | `10` as "I would like to performance-test your log collection stack". | |
232 | `10` as "I would like to performance-test your log collection stack." | |
166 | 233 | |
167 | 234 | Then gradually choose levels in between as you need them, working your way |
168 | 235 | down from 10 (for debug and trace style logs) and up from 1 (for chattier |
169 | info-type logs). | |
170 | ||
171 | ## How do I choose my keys | |
172 | ||
173 | - make your keys human-readable | |
174 | - constant keys are generally a good idea | |
175 | - be consistent across your codebase | |
176 | - keys should naturally match parts of the message string | |
236 | info-type logs.) | |
237 | ||
238 | #### How do I choose my keys? | |
239 | ||
240 | - Make your keys human-readable. | |
241 | - Constant keys are generally a good idea. | |
242 | - Be consistent across your codebase. | |
243 | - Keys should naturally match parts of the message string. | |
177 | 244 | |
178 | 245 | While key names are mostly unrestricted (and spaces are acceptable), |
179 | 246 | it's generally a good idea to stick to printable ascii characters, or at |
180 | 247 | least match the general character set of your log lines. |
181 | 248 | |
249 | #### Why should keys be constant values? | |
250 | ||
251 | The point of structured logging is to make later log processing easier. Your | |
252 | keys are, effectively, the schema of each log message. If you use different | |
253 | keys across instances of the same log-line, you will make your structured logs | |
254 | much harder to use. `Sprintf()` is for values, not for keys! | |
255 | ||
256 | #### Why is this not a pure interface? | |
257 | ||
258 | The Logger type is implemented as a struct in order to allow the Go compiler to | |
259 | optimize things like high-V `Info` logs that are not triggered. Not all of | |
260 | these implementations are implemented yet, but this structure was suggested as | |
261 | a way to ensure they *can* be implemented. All of the real work is behind the | |
262 | `LogSink` interface. | |
263 | ||
182 | 264 | [warning-makes-no-sense]: http://dave.cheney.net/2015/11/05/lets-talk-about-logging |
0 | # Benchmarking logr | |
1 | ||
2 | Any major changes to the logr library must be benchmarked before and after the | |
3 | change. | |
4 | ||
5 | ## Running the benchmark | |
6 | ||
7 | ``` | |
8 | $ go test -bench='.' ./benchmark/ | |
9 | ``` | |
10 | ||
11 | ## Fixing the benchmark | |
12 | ||
13 | If you think this benchmark can be improved, you are probably correct! PRs are | |
14 | very welcome. |
0 | /* | |
1 | Copyright 2021 The logr Authors. | |
2 | ||
3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | you may not use this file except in compliance with the License. | |
5 | You may obtain a copy of the License at | |
6 | ||
7 | http://www.apache.org/licenses/LICENSE-2.0 | |
8 | ||
9 | Unless required by applicable law or agreed to in writing, software | |
10 | distributed under the License is distributed on an "AS IS" BASIS, | |
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | See the License for the specific language governing permissions and | |
13 | limitations under the License. | |
14 | */ | |
15 | ||
16 | package logr | |
17 | ||
18 | import ( | |
19 | "fmt" | |
20 | "testing" | |
21 | ||
22 | "github.com/go-logr/logr" | |
23 | "github.com/go-logr/logr/funcr" | |
24 | ) | |
25 | ||
26 | func noop(prefix, args string) { | |
27 | } | |
28 | ||
29 | //go:noinline | |
30 | func doInfoOneArg(b *testing.B, log logr.Logger) { | |
31 | for i := 0; i < b.N; i++ { | |
32 | log.Info("this is", "a", "string") | |
33 | } | |
34 | } | |
35 | ||
36 | //go:noinline | |
37 | func doInfoSeveralArgs(b *testing.B, log logr.Logger) { | |
38 | for i := 0; i < b.N; i++ { | |
39 | log.Info("multi", | |
40 | "bool", true, "string", "str", "int", 42, | |
41 | "float", 3.14, "struct", struct{ X, Y int }{93, 76}) | |
42 | } | |
43 | } | |
44 | ||
45 | //go:noinline | |
46 | func doV0Info(b *testing.B, log logr.Logger) { | |
47 | for i := 0; i < b.N; i++ { | |
48 | log.V(0).Info("multi", | |
49 | "bool", true, "string", "str", "int", 42, | |
50 | "float", 3.14, "struct", struct{ X, Y int }{93, 76}) | |
51 | } | |
52 | } | |
53 | ||
54 | //go:noinline | |
55 | func doV9Info(b *testing.B, log logr.Logger) { | |
56 | for i := 0; i < b.N; i++ { | |
57 | log.V(9).Info("multi", | |
58 | "bool", true, "string", "str", "int", 42, | |
59 | "float", 3.14, "struct", struct{ X, Y int }{93, 76}) | |
60 | } | |
61 | } | |
62 | ||
63 | //go:noinline | |
64 | func doError(b *testing.B, log logr.Logger) { | |
65 | err := fmt.Errorf("error message") | |
66 | for i := 0; i < b.N; i++ { | |
67 | log.Error(err, "multi", | |
68 | "bool", true, "string", "str", "int", 42, | |
69 | "float", 3.14, "struct", struct{ X, Y int }{93, 76}) | |
70 | } | |
71 | } | |
72 | ||
73 | //go:noinline | |
74 | func doWithValues(b *testing.B, log logr.Logger) { | |
75 | for i := 0; i < b.N; i++ { | |
76 | l := log.WithValues("k1", "v1", "k2", "v2") | |
77 | _ = l | |
78 | } | |
79 | } | |
80 | ||
81 | //go:noinline | |
82 | func doWithName(b *testing.B, log logr.Logger) { | |
83 | for i := 0; i < b.N; i++ { | |
84 | l := log.WithName("name") | |
85 | _ = l | |
86 | } | |
87 | } | |
88 | ||
89 | func BenchmarkDiscardInfoOneArg(b *testing.B) { | |
90 | var log logr.Logger = logr.Discard() | |
91 | doInfoOneArg(b, log) | |
92 | } | |
93 | ||
94 | func BenchmarkDiscardInfoSeveralArgs(b *testing.B) { | |
95 | var log logr.Logger = logr.Discard() | |
96 | doInfoSeveralArgs(b, log) | |
97 | } | |
98 | ||
99 | func BenchmarkDiscardV0Info(b *testing.B) { | |
100 | var log logr.Logger = logr.Discard() | |
101 | doV0Info(b, log) | |
102 | } | |
103 | ||
104 | func BenchmarkDiscardV9Info(b *testing.B) { | |
105 | var log logr.Logger = logr.Discard() | |
106 | doV9Info(b, log) | |
107 | } | |
108 | ||
109 | func BenchmarkDiscardError(b *testing.B) { | |
110 | var log logr.Logger = logr.Discard() | |
111 | doError(b, log) | |
112 | } | |
113 | ||
114 | func BenchmarkDiscardWithValues(b *testing.B) { | |
115 | var log logr.Logger = logr.Discard() | |
116 | doWithValues(b, log) | |
117 | } | |
118 | ||
119 | func BenchmarkDiscardWithName(b *testing.B) { | |
120 | var log logr.Logger = logr.Discard() | |
121 | doWithName(b, log) | |
122 | } | |
123 | ||
124 | func BenchmarkFuncrInfoOneArg(b *testing.B) { | |
125 | var log logr.Logger = funcr.New(noop, funcr.Options{}) | |
126 | doInfoOneArg(b, log) | |
127 | } | |
128 | ||
129 | func BenchmarkFuncrInfoSeveralArgs(b *testing.B) { | |
130 | var log logr.Logger = funcr.New(noop, funcr.Options{}) | |
131 | doInfoSeveralArgs(b, log) | |
132 | } | |
133 | ||
134 | func BenchmarkFuncrV0Info(b *testing.B) { | |
135 | var log logr.Logger = funcr.New(noop, funcr.Options{}) | |
136 | doV0Info(b, log) | |
137 | } | |
138 | ||
139 | func BenchmarkFuncrV9Info(b *testing.B) { | |
140 | var log logr.Logger = funcr.New(noop, funcr.Options{}) | |
141 | doV9Info(b, log) | |
142 | } | |
143 | ||
144 | func BenchmarkFuncrError(b *testing.B) { | |
145 | var log logr.Logger = funcr.New(noop, funcr.Options{}) | |
146 | doError(b, log) | |
147 | } | |
148 | ||
149 | func BenchmarkFuncrWithValues(b *testing.B) { | |
150 | var log logr.Logger = funcr.New(noop, funcr.Options{}) | |
151 | doWithValues(b, log) | |
152 | } | |
153 | ||
154 | func BenchmarkFuncrWithName(b *testing.B) { | |
155 | var log logr.Logger = funcr.New(noop, funcr.Options{}) | |
156 | doWithName(b, log) | |
157 | } |
15 | 15 | |
16 | 16 | package logr |
17 | 17 | |
18 | // Discard returns a valid Logger that discards all messages logged to it. | |
19 | // It can be used whenever the caller is not interested in the logs. | |
18 | // Discard returns a Logger that discards all messages logged to it. It can be | |
19 | // used whenever the caller is not interested in the logs. Logger instances | |
20 | // produced by this function always compare as equal. | |
20 | 21 | func Discard() Logger { |
21 | return DiscardLogger{} | |
22 | return Logger{ | |
23 | level: 0, | |
24 | sink: discardLogSink{}, | |
25 | } | |
22 | 26 | } |
23 | 27 | |
24 | // DiscardLogger is a Logger that discards all messages. | |
25 | type DiscardLogger struct{} | |
28 | // discardLogSink is a LogSink that discards all messages. | |
29 | type discardLogSink struct{} | |
26 | 30 | |
27 | func (l DiscardLogger) Enabled() bool { | |
31 | // Verify that it actually implements the interface | |
32 | var _ LogSink = discardLogSink{} | |
33 | ||
34 | func (l discardLogSink) Init(RuntimeInfo) { | |
35 | } | |
36 | ||
37 | func (l discardLogSink) Enabled(int) bool { | |
28 | 38 | return false |
29 | 39 | } |
30 | 40 | |
31 | func (l DiscardLogger) Info(msg string, keysAndValues ...interface{}) { | |
41 | func (l discardLogSink) Info(int, string, ...interface{}) { | |
32 | 42 | } |
33 | 43 | |
34 | func (l DiscardLogger) Error(err error, msg string, keysAndValues ...interface{}) { | |
44 | func (l discardLogSink) Error(error, string, ...interface{}) { | |
35 | 45 | } |
36 | 46 | |
37 | func (l DiscardLogger) V(level int) Logger { | |
47 | func (l discardLogSink) WithValues(...interface{}) LogSink { | |
38 | 48 | return l |
39 | 49 | } |
40 | 50 | |
41 | func (l DiscardLogger) WithValues(keysAndValues ...interface{}) Logger { | |
51 | func (l discardLogSink) WithName(string) LogSink { | |
42 | 52 | return l |
43 | 53 | } |
44 | ||
45 | func (l DiscardLogger) WithName(name string) Logger { | |
46 | return l | |
47 | } | |
48 | ||
49 | // Verify that it actually implements the interface | |
50 | var _ Logger = DiscardLogger{} |
17 | 17 | |
18 | 18 | import ( |
19 | 19 | "errors" |
20 | "reflect" | |
20 | 21 | "testing" |
21 | 22 | ) |
22 | 23 | |
23 | 24 | func TestDiscard(t *testing.T) { |
24 | 25 | l := Discard() |
25 | if _, ok := l.(DiscardLogger); !ok { | |
26 | if _, ok := l.sink.(discardLogSink); !ok { | |
26 | 27 | t.Error("did not return the expected underlying type") |
27 | 28 | } |
28 | 29 | // Verify that none of the methods panic, there is not more we can test. |
30 | 31 | l.Info("Hello world", "x", 1, "y", 2) |
31 | 32 | l.V(1).Error(errors.New("foo"), "a", 123) |
32 | 33 | if l.Enabled() { |
33 | t.Error("discard logger must always say it is disabled") | |
34 | t.Error("discardLogSink must always say it is disabled") | |
34 | 35 | } |
35 | 36 | } |
37 | ||
38 | func TestComparable(t *testing.T) { | |
39 | a := Discard() | |
40 | if !reflect.TypeOf(a).Comparable() { | |
41 | t.Fatal("discardLogSink must be comparable") | |
42 | } | |
43 | ||
44 | b := Discard() | |
45 | if a != b { | |
46 | t.Fatal("any two discardLogSink must be equal") | |
47 | } | |
48 | } |
23 | 23 | "github.com/go-logr/logr" |
24 | 24 | ) |
25 | 25 | |
26 | // TabLogger is a sample logr.Logger that logs to stderr. | |
27 | // It's terribly inefficient, and is *only* a basic example. | |
28 | type TabLogger struct { | |
26 | // tabLogSink is a sample logr.LogSink that logs to stderr. | |
27 | // It's terribly inefficient, and is only a basic example. | |
28 | type tabLogSink struct { | |
29 | 29 | name string |
30 | 30 | keyValues map[string]interface{} |
31 | ||
32 | writer *tabwriter.Writer | |
31 | writer *tabwriter.Writer | |
33 | 32 | } |
34 | 33 | |
35 | var _ logr.Logger = &TabLogger{} | |
34 | var _ logr.LogSink = &tabLogSink{} | |
36 | 35 | |
37 | func (l *TabLogger) Info(msg string, kvs ...interface{}) { | |
36 | // Note that Init usually takes a pointer so it can modify the receiver to save | |
37 | // runtime info. | |
38 | func (_ *tabLogSink) Init(info logr.RuntimeInfo) { | |
39 | } | |
40 | ||
41 | func (_ tabLogSink) Enabled(level int) bool { | |
42 | return true | |
43 | } | |
44 | ||
45 | func (l tabLogSink) Info(level int, msg string, kvs ...interface{}) { | |
38 | 46 | fmt.Fprintf(l.writer, "%s\t%s\t", l.name, msg) |
39 | 47 | for k, v := range l.keyValues { |
40 | 48 | fmt.Fprintf(l.writer, "%s: %+v ", k, v) |
46 | 54 | l.writer.Flush() |
47 | 55 | } |
48 | 56 | |
49 | func (_ *TabLogger) Enabled() bool { | |
50 | return true | |
57 | func (l tabLogSink) Error(err error, msg string, kvs ...interface{}) { | |
58 | kvs = append(kvs, "error", err) | |
59 | l.Info(0, msg, kvs...) | |
51 | 60 | } |
52 | 61 | |
53 | func (l *TabLogger) Error(err error, msg string, kvs ...interface{}) { | |
54 | kvs = append(kvs, "error", err) | |
55 | l.Info(msg, kvs...) | |
56 | } | |
57 | ||
58 | func (l *TabLogger) V(_ int) logr.Logger { | |
59 | return l | |
60 | } | |
61 | ||
62 | func (l *TabLogger) WithName(name string) logr.Logger { | |
63 | return &TabLogger{ | |
62 | func (l tabLogSink) WithName(name string) logr.LogSink { | |
63 | return &tabLogSink{ | |
64 | 64 | name: l.name + "." + name, |
65 | 65 | keyValues: l.keyValues, |
66 | 66 | writer: l.writer, |
67 | 67 | } |
68 | 68 | } |
69 | 69 | |
70 | func (l *TabLogger) WithValues(kvs ...interface{}) logr.Logger { | |
70 | func (l tabLogSink) WithValues(kvs ...interface{}) logr.LogSink { | |
71 | 71 | newMap := make(map[string]interface{}, len(l.keyValues)+len(kvs)/2) |
72 | 72 | for k, v := range l.keyValues { |
73 | 73 | newMap[k] = v |
75 | 75 | for i := 0; i < len(kvs); i += 2 { |
76 | 76 | newMap[kvs[i].(string)] = kvs[i+1] |
77 | 77 | } |
78 | return &TabLogger{ | |
78 | return &tabLogSink{ | |
79 | 79 | name: l.name, |
80 | 80 | keyValues: newMap, |
81 | 81 | writer: l.writer, |
82 | 82 | } |
83 | 83 | } |
84 | 84 | |
85 | // NewTabLogger is the main entry-point to this implementation. App developers | |
86 | // call this somewhere near main() and thenceforth only deal with logr.Logger. | |
85 | 87 | func NewTabLogger() logr.Logger { |
86 | return &TabLogger{ | |
88 | sink := &tabLogSink{ | |
87 | 89 | writer: tabwriter.NewWriter(os.Stderr, 40, 8, 2, '\t', 0), |
88 | 90 | } |
91 | return logr.New(sink) | |
89 | 92 | } |
0 | /* | |
1 | Copyright 2021 The logr Authors. | |
2 | ||
3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | you may not use this file except in compliance with the License. | |
5 | You may obtain a copy of the License at | |
6 | ||
7 | http://www.apache.org/licenses/LICENSE-2.0 | |
8 | ||
9 | Unless required by applicable law or agreed to in writing, software | |
10 | distributed under the License is distributed on an "AS IS" BASIS, | |
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | See the License for the specific language governing permissions and | |
13 | limitations under the License. | |
14 | */ | |
15 | ||
16 | // Package funcr implements github.com/go-logr/logr.Logger in terms of | |
17 | // an arbitrary "write" function. | |
18 | package funcr | |
19 | ||
20 | import ( | |
21 | "bytes" | |
22 | "fmt" | |
23 | "path/filepath" | |
24 | "reflect" | |
25 | "runtime" | |
26 | "strconv" | |
27 | "strings" | |
28 | ||
29 | "github.com/go-logr/logr" | |
30 | ) | |
31 | ||
32 | // New returns a logr.Logger which is implemented by an arbitrary function. | |
33 | func New(fn func(prefix, args string), opts Options) logr.Logger { | |
34 | return logr.New(newSink(fn, opts)) | |
35 | } | |
36 | ||
37 | func newSink(fn func(prefix, args string), opts Options) logr.LogSink { | |
38 | return &fnlogger{ | |
39 | prefix: "", | |
40 | values: nil, | |
41 | depth: 0, | |
42 | write: fn, | |
43 | logCaller: opts.LogCaller, | |
44 | verbosity: opts.Verbosity, | |
45 | } | |
46 | } | |
47 | ||
48 | // Options carries parameters which influence the way logs are generated. | |
49 | type Options struct { | |
50 | // LogCaller tells funcr to add a "caller" key to some or all log lines. | |
51 | // This has some overhead, so some users might not want it. | |
52 | LogCaller MessageClass | |
53 | ||
54 | // Verbosity tells funcr which V logs to be write. Higher values enable | |
55 | // more logs. | |
56 | Verbosity int | |
57 | } | |
58 | ||
59 | // MessageClass indicates which category or categories of messages to consider. | |
60 | type MessageClass int | |
61 | ||
62 | const ( | |
63 | None MessageClass = iota | |
64 | All | |
65 | Info | |
66 | Error | |
67 | ) | |
68 | ||
69 | type fnlogger struct { | |
70 | prefix string | |
71 | values []interface{} | |
72 | depth int | |
73 | write func(prefix, args string) | |
74 | logCaller MessageClass | |
75 | verbosity int | |
76 | } | |
77 | ||
78 | // Assert conformance to the interfaces. | |
79 | var _ logr.LogSink = &fnlogger{} | |
80 | var _ logr.CallDepthLogSink = &fnlogger{} | |
81 | ||
82 | func flatten(kvList ...interface{}) string { | |
83 | if len(kvList)%2 != 0 { | |
84 | kvList = append(kvList, "<no-value>") | |
85 | } | |
86 | // Empirically bytes.Buffer is faster than strings.Builder for this. | |
87 | buf := bytes.NewBuffer(make([]byte, 0, 1024)) | |
88 | for i := 0; i < len(kvList); i += 2 { | |
89 | k, ok := kvList[i].(string) | |
90 | if !ok { | |
91 | k = fmt.Sprintf("<non-string-key-%d>", i/2) | |
92 | } | |
93 | v := kvList[i+1] | |
94 | ||
95 | if i > 0 { | |
96 | buf.WriteRune(' ') | |
97 | } | |
98 | buf.WriteRune('"') | |
99 | buf.WriteString(k) | |
100 | buf.WriteRune('"') | |
101 | buf.WriteRune('=') | |
102 | buf.WriteString(pretty(v)) | |
103 | } | |
104 | return buf.String() | |
105 | } | |
106 | ||
107 | func pretty(value interface{}) string { | |
108 | return prettyWithFlags(value, 0) | |
109 | } | |
110 | ||
111 | const ( | |
112 | flagRawString = 0x1 | |
113 | ) | |
114 | ||
115 | // TODO: This is not fast. Most of the overhead goes here. | |
116 | func prettyWithFlags(value interface{}, flags uint32) string { | |
117 | // Handling the most common types without reflect is a small perf win. | |
118 | switch v := value.(type) { | |
119 | case bool: | |
120 | return strconv.FormatBool(v) | |
121 | case string: | |
122 | if flags&flagRawString > 0 { | |
123 | return v | |
124 | } | |
125 | // This is empirically faster than strings.Builder. | |
126 | return `"` + v + `"` | |
127 | case int: | |
128 | return strconv.FormatInt(int64(v), 10) | |
129 | case int8: | |
130 | return strconv.FormatInt(int64(v), 10) | |
131 | case int16: | |
132 | return strconv.FormatInt(int64(v), 10) | |
133 | case int32: | |
134 | return strconv.FormatInt(int64(v), 10) | |
135 | case int64: | |
136 | return strconv.FormatInt(int64(v), 10) | |
137 | case uint: | |
138 | return strconv.FormatUint(uint64(v), 10) | |
139 | case uint8: | |
140 | return strconv.FormatUint(uint64(v), 10) | |
141 | case uint16: | |
142 | return strconv.FormatUint(uint64(v), 10) | |
143 | case uint32: | |
144 | return strconv.FormatUint(uint64(v), 10) | |
145 | case uint64: | |
146 | return strconv.FormatUint(v, 10) | |
147 | case uintptr: | |
148 | return strconv.FormatUint(uint64(v), 10) | |
149 | case float32: | |
150 | return strconv.FormatFloat(float64(v), 'f', -1, 32) | |
151 | case float64: | |
152 | return strconv.FormatFloat(v, 'f', -1, 64) | |
153 | } | |
154 | ||
155 | buf := bytes.NewBuffer(make([]byte, 0, 256)) | |
156 | t := reflect.TypeOf(value) | |
157 | if t == nil { | |
158 | return "null" | |
159 | } | |
160 | v := reflect.ValueOf(value) | |
161 | switch t.Kind() { | |
162 | case reflect.Bool: | |
163 | return strconv.FormatBool(v.Bool()) | |
164 | case reflect.String: | |
165 | if flags&flagRawString > 0 { | |
166 | return v.String() | |
167 | } | |
168 | // This is empirically faster than strings.Builder. | |
169 | return `"` + v.String() + `"` | |
170 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |
171 | return strconv.FormatInt(int64(v.Int()), 10) | |
172 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: | |
173 | return strconv.FormatUint(uint64(v.Uint()), 10) | |
174 | case reflect.Float32: | |
175 | return strconv.FormatFloat(float64(v.Float()), 'f', -1, 32) | |
176 | case reflect.Float64: | |
177 | return strconv.FormatFloat(v.Float(), 'f', -1, 64) | |
178 | case reflect.Struct: | |
179 | buf.WriteRune('{') | |
180 | for i := 0; i < t.NumField(); i++ { | |
181 | f := t.Field(i) | |
182 | if f.PkgPath != "" { | |
183 | // reflect says this field is only defined for non-exported fields. | |
184 | continue | |
185 | } | |
186 | if i > 0 { | |
187 | buf.WriteRune(',') | |
188 | } | |
189 | buf.WriteRune('"') | |
190 | name := f.Name | |
191 | if tag, found := f.Tag.Lookup("json"); found { | |
192 | if comma := strings.Index(tag, ","); comma != -1 { | |
193 | name = tag[:comma] | |
194 | } else { | |
195 | name = tag | |
196 | } | |
197 | } | |
198 | buf.WriteString(name) | |
199 | buf.WriteRune('"') | |
200 | buf.WriteRune(':') | |
201 | buf.WriteString(pretty(v.Field(i).Interface())) | |
202 | } | |
203 | buf.WriteRune('}') | |
204 | return buf.String() | |
205 | case reflect.Slice, reflect.Array: | |
206 | buf.WriteRune('[') | |
207 | for i := 0; i < v.Len(); i++ { | |
208 | if i > 0 { | |
209 | buf.WriteRune(',') | |
210 | } | |
211 | e := v.Index(i) | |
212 | buf.WriteString(pretty(e.Interface())) | |
213 | } | |
214 | buf.WriteRune(']') | |
215 | return buf.String() | |
216 | case reflect.Map: | |
217 | buf.WriteRune('{') | |
218 | // This does not sort the map keys, for best perf. | |
219 | it := v.MapRange() | |
220 | i := 0 | |
221 | for it.Next() { | |
222 | if i > 0 { | |
223 | buf.WriteRune(',') | |
224 | } | |
225 | // JSON only does string keys. | |
226 | buf.WriteRune('"') | |
227 | buf.WriteString(prettyWithFlags(it.Key().Interface(), flagRawString)) | |
228 | buf.WriteRune('"') | |
229 | buf.WriteRune(':') | |
230 | buf.WriteString(pretty(it.Value().Interface())) | |
231 | i++ | |
232 | } | |
233 | buf.WriteRune('}') | |
234 | return buf.String() | |
235 | case reflect.Ptr, reflect.Interface: | |
236 | return pretty(v.Elem().Interface()) | |
237 | } | |
238 | return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String()) | |
239 | } | |
240 | ||
241 | type callerID struct { | |
242 | File string `json:"file"` | |
243 | Line int `json:"line"` | |
244 | } | |
245 | ||
246 | func (l fnlogger) caller() callerID { | |
247 | // +1 for this frame, +1 for Info/Error. | |
248 | _, file, line, ok := runtime.Caller(l.depth + 2) | |
249 | if !ok { | |
250 | return callerID{"<unknown>", 0} | |
251 | } | |
252 | return callerID{filepath.Base(file), line} | |
253 | } | |
254 | ||
255 | // Note that this receiver is a pointer, so depth can be saved. | |
256 | func (l *fnlogger) Init(info logr.RuntimeInfo) { | |
257 | l.depth += info.CallDepth | |
258 | } | |
259 | ||
260 | func (l fnlogger) Enabled(level int) bool { | |
261 | return level <= l.verbosity | |
262 | } | |
263 | ||
264 | func (l fnlogger) Info(level int, msg string, kvList ...interface{}) { | |
265 | args := make([]interface{}, 0, 64) // using a constant here impacts perf | |
266 | if l.logCaller == All || l.logCaller == Info { | |
267 | args = append(args, "caller", l.caller()) | |
268 | } | |
269 | args = append(args, "level", level, "msg", msg) | |
270 | args = append(args, l.values...) | |
271 | args = append(args, kvList...) | |
272 | argsStr := flatten(args...) | |
273 | l.write(l.prefix, argsStr) | |
274 | } | |
275 | ||
276 | func (l fnlogger) Error(err error, msg string, kvList ...interface{}) { | |
277 | args := make([]interface{}, 0, 64) // using a constant here impacts perf | |
278 | if l.logCaller == All || l.logCaller == Error { | |
279 | args = append(args, "caller", l.caller()) | |
280 | } | |
281 | args = append(args, "msg", msg) | |
282 | var loggableErr interface{} | |
283 | if err != nil { | |
284 | loggableErr = err.Error() | |
285 | } | |
286 | args = append(args, "error", loggableErr) | |
287 | args = append(args, l.values...) | |
288 | args = append(args, kvList...) | |
289 | argsStr := flatten(args...) | |
290 | l.write(l.prefix, argsStr) | |
291 | } | |
292 | ||
293 | // WithName returns a new Logger with the specified name appended. funcr | |
294 | // uses '/' characters to separate name elements. Callers should not pass '/' | |
295 | // in the provided name string, but this library does not actually enforce that. | |
296 | func (l fnlogger) WithName(name string) logr.LogSink { | |
297 | if len(l.prefix) > 0 { | |
298 | l.prefix = l.prefix + "/" | |
299 | } | |
300 | l.prefix += name | |
301 | return &l | |
302 | } | |
303 | ||
304 | func (l fnlogger) WithValues(kvList ...interface{}) logr.LogSink { | |
305 | // Three slice args forces a copy. | |
306 | n := len(l.values) | |
307 | l.values = append(l.values[:n:n], kvList...) | |
308 | return &l | |
309 | } | |
310 | ||
311 | func (l fnlogger) WithCallDepth(depth int) logr.LogSink { | |
312 | l.depth += depth | |
313 | return &l | |
314 | } |
0 | /* | |
1 | Copyright 2021 The logr Authors. | |
2 | ||
3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | you may not use this file except in compliance with the License. | |
5 | You may obtain a copy of the License at | |
6 | ||
7 | http://www.apache.org/licenses/LICENSE-2.0 | |
8 | ||
9 | Unless required by applicable law or agreed to in writing, software | |
10 | distributed under the License is distributed on an "AS IS" BASIS, | |
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | See the License for the specific language governing permissions and | |
13 | limitations under the License. | |
14 | */ | |
15 | ||
16 | package funcr | |
17 | ||
18 | import ( | |
19 | "encoding/json" | |
20 | "fmt" | |
21 | "path/filepath" | |
22 | "runtime" | |
23 | "testing" | |
24 | ||
25 | "github.com/go-logr/logr" | |
26 | ) | |
27 | ||
28 | type substr string | |
29 | ||
30 | func ptrint(i int) *int { | |
31 | return &i | |
32 | } | |
33 | func ptrstr(s string) *string { | |
34 | return &s | |
35 | } | |
36 | ||
37 | func TestPretty(t *testing.T) { | |
38 | cases := []interface{}{ | |
39 | "strval", | |
40 | substr("substrval"), | |
41 | true, | |
42 | false, | |
43 | int(93), | |
44 | int8(93), | |
45 | int16(93), | |
46 | int32(93), | |
47 | int64(93), | |
48 | int(-93), | |
49 | int8(-93), | |
50 | int16(-93), | |
51 | int32(-93), | |
52 | int64(-93), | |
53 | uint(93), | |
54 | uint8(93), | |
55 | uint16(93), | |
56 | uint32(93), | |
57 | uint64(93), | |
58 | uintptr(93), | |
59 | float32(93.76), | |
60 | float64(93.76), | |
61 | ptrint(93), | |
62 | ptrstr("pstrval"), | |
63 | []int{9, 3, 7, 6}, | |
64 | [4]int{9, 3, 7, 6}, | |
65 | struct { | |
66 | Int int | |
67 | String string | |
68 | }{ | |
69 | 93, "seventy-six", | |
70 | }, | |
71 | map[string]int{ | |
72 | "nine": 3, | |
73 | }, | |
74 | map[substr]int{ | |
75 | "nine": 3, | |
76 | }, | |
77 | fmt.Errorf("error"), | |
78 | struct { | |
79 | X int `json:"x"` | |
80 | Y int `json:"y"` | |
81 | }{ | |
82 | 93, 76, | |
83 | }, | |
84 | struct { | |
85 | X []int | |
86 | Y map[int]int | |
87 | Z struct{ P, Q int } | |
88 | }{ | |
89 | []int{9, 3, 7, 6}, | |
90 | map[int]int{9: 3}, | |
91 | struct{ P, Q int }{9, 3}, | |
92 | }, | |
93 | []struct{ X, Y string }{ | |
94 | {"nine", "three"}, | |
95 | {"seven", "six"}, | |
96 | }, | |
97 | } | |
98 | ||
99 | for i, tc := range cases { | |
100 | ours := pretty(tc) | |
101 | std, err := json.Marshal(tc) | |
102 | if err != nil { | |
103 | t.Errorf("[%d]: unexpected error: %v", i, err) | |
104 | } | |
105 | if ours != string(std) { | |
106 | t.Errorf("[%d]: expected %q, got %q", i, std, ours) | |
107 | } | |
108 | } | |
109 | } | |
110 | ||
111 | func makeKV(args ...interface{}) []interface{} { | |
112 | return args | |
113 | } | |
114 | ||
115 | func TestFlatten(t *testing.T) { | |
116 | testCases := []struct { | |
117 | name string | |
118 | kv []interface{} | |
119 | expect string | |
120 | }{{ | |
121 | name: "nil", | |
122 | kv: nil, | |
123 | expect: "", | |
124 | }, { | |
125 | name: "empty", | |
126 | kv: []interface{}{}, | |
127 | expect: "", | |
128 | }, { | |
129 | name: "primitives", | |
130 | kv: makeKV("int", 1, "str", "ABC", "bool", true), | |
131 | expect: `"int"=1 "str"="ABC" "bool"=true`, | |
132 | }, { | |
133 | name: "missing value", | |
134 | kv: makeKV("key"), | |
135 | expect: `"key"="<no-value>"`, | |
136 | }, { | |
137 | name: "non-string key", | |
138 | kv: makeKV(123, "val"), | |
139 | expect: `"<non-string-key-0>"="val"`, | |
140 | }} | |
141 | ||
142 | for _, tc := range testCases { | |
143 | t.Run(tc.name, func(t *testing.T) { | |
144 | r := flatten(tc.kv...) | |
145 | if r != tc.expect { | |
146 | t.Errorf("expected %q, got %q", tc.expect, r) | |
147 | } | |
148 | }) | |
149 | } | |
150 | } | |
151 | ||
152 | func TestEnabled(t *testing.T) { | |
153 | t.Run("default V", func(t *testing.T) { | |
154 | log := newSink(func(prefix, args string) {}, Options{}) | |
155 | if !log.Enabled(0) { | |
156 | t.Errorf("expected true") | |
157 | } | |
158 | if log.Enabled(1) { | |
159 | t.Errorf("expected false") | |
160 | } | |
161 | }) | |
162 | t.Run("V=9", func(t *testing.T) { | |
163 | log := newSink(func(prefix, args string) {}, Options{Verbosity: 9}) | |
164 | if !log.Enabled(8) { | |
165 | t.Errorf("expected true") | |
166 | } | |
167 | if !log.Enabled(9) { | |
168 | t.Errorf("expected true") | |
169 | } | |
170 | if log.Enabled(10) { | |
171 | t.Errorf("expected false") | |
172 | } | |
173 | }) | |
174 | } | |
175 | ||
176 | type capture struct { | |
177 | log string | |
178 | } | |
179 | ||
180 | func (c *capture) Func(prefix, args string) { | |
181 | c.log = prefix + " " + args | |
182 | } | |
183 | ||
184 | func TestInfo(t *testing.T) { | |
185 | testCases := []struct { | |
186 | name string | |
187 | args []interface{} | |
188 | expect string | |
189 | }{{ | |
190 | name: "just msg", | |
191 | args: makeKV(), | |
192 | expect: ` "level"=0 "msg"="msg"`, | |
193 | }, { | |
194 | name: "primitives", | |
195 | args: makeKV("int", 1, "str", "ABC", "bool", true), | |
196 | expect: ` "level"=0 "msg"="msg" "int"=1 "str"="ABC" "bool"=true`, | |
197 | }} | |
198 | ||
199 | for _, tc := range testCases { | |
200 | t.Run(tc.name, func(t *testing.T) { | |
201 | cap := &capture{} | |
202 | sink := newSink(cap.Func, Options{}) | |
203 | sink.Info(0, "msg", tc.args...) | |
204 | if cap.log != tc.expect { | |
205 | t.Errorf("\nexpected %q\n got %q", tc.expect, cap.log) | |
206 | } | |
207 | }) | |
208 | } | |
209 | } | |
210 | ||
211 | func TestInfoWithCaller(t *testing.T) { | |
212 | t.Run("LogCaller=All", func(t *testing.T) { | |
213 | cap := &capture{} | |
214 | sink := newSink(cap.Func, Options{LogCaller: All}) | |
215 | sink.Info(0, "msg") | |
216 | _, file, line, _ := runtime.Caller(0) | |
217 | expect := fmt.Sprintf(` "caller"={"file":%q,"line":%d} "level"=0 "msg"="msg"`, filepath.Base(file), line-1) | |
218 | if cap.log != expect { | |
219 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
220 | } | |
221 | }) | |
222 | t.Run("LogCaller=Info", func(t *testing.T) { | |
223 | cap := &capture{} | |
224 | sink := newSink(cap.Func, Options{LogCaller: Info}) | |
225 | sink.Info(0, "msg") | |
226 | _, file, line, _ := runtime.Caller(0) | |
227 | expect := fmt.Sprintf(` "caller"={"file":%q,"line":%d} "level"=0 "msg"="msg"`, filepath.Base(file), line-1) | |
228 | if cap.log != expect { | |
229 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
230 | } | |
231 | }) | |
232 | t.Run("LogCaller=Error", func(t *testing.T) { | |
233 | cap := &capture{} | |
234 | sink := newSink(cap.Func, Options{LogCaller: Error}) | |
235 | sink.Info(0, "msg") | |
236 | expect := ` "level"=0 "msg"="msg"` | |
237 | if cap.log != expect { | |
238 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
239 | } | |
240 | }) | |
241 | t.Run("LogCaller=None", func(t *testing.T) { | |
242 | cap := &capture{} | |
243 | sink := newSink(cap.Func, Options{LogCaller: None}) | |
244 | sink.Info(0, "msg") | |
245 | expect := ` "level"=0 "msg"="msg"` | |
246 | if cap.log != expect { | |
247 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
248 | } | |
249 | }) | |
250 | } | |
251 | ||
252 | func TestError(t *testing.T) { | |
253 | testCases := []struct { | |
254 | name string | |
255 | args []interface{} | |
256 | expect string | |
257 | }{{ | |
258 | name: "just msg", | |
259 | args: makeKV(), | |
260 | expect: ` "msg"="msg" "error"="err"`, | |
261 | }, { | |
262 | name: "primitives", | |
263 | args: makeKV("int", 1, "str", "ABC", "bool", true), | |
264 | expect: ` "msg"="msg" "error"="err" "int"=1 "str"="ABC" "bool"=true`, | |
265 | }} | |
266 | ||
267 | for _, tc := range testCases { | |
268 | t.Run(tc.name, func(t *testing.T) { | |
269 | cap := &capture{} | |
270 | sink := newSink(cap.Func, Options{}) | |
271 | sink.Error(fmt.Errorf("err"), "msg", tc.args...) | |
272 | if cap.log != tc.expect { | |
273 | t.Errorf("\nexpected %q\n got %q", tc.expect, cap.log) | |
274 | } | |
275 | }) | |
276 | } | |
277 | } | |
278 | ||
279 | func TestErrorWithCaller(t *testing.T) { | |
280 | t.Run("LogCaller=All", func(t *testing.T) { | |
281 | cap := &capture{} | |
282 | sink := newSink(cap.Func, Options{LogCaller: All}) | |
283 | sink.Error(fmt.Errorf("err"), "msg") | |
284 | _, file, line, _ := runtime.Caller(0) | |
285 | expect := fmt.Sprintf(` "caller"={"file":%q,"line":%d} "msg"="msg" "error"="err"`, filepath.Base(file), line-1) | |
286 | if cap.log != expect { | |
287 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
288 | } | |
289 | }) | |
290 | t.Run("LogCaller=Error", func(t *testing.T) { | |
291 | cap := &capture{} | |
292 | sink := newSink(cap.Func, Options{LogCaller: Error}) | |
293 | sink.Error(fmt.Errorf("err"), "msg") | |
294 | _, file, line, _ := runtime.Caller(0) | |
295 | expect := fmt.Sprintf(` "caller"={"file":%q,"line":%d} "msg"="msg" "error"="err"`, filepath.Base(file), line-1) | |
296 | if cap.log != expect { | |
297 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
298 | } | |
299 | }) | |
300 | t.Run("LogCaller=Info", func(t *testing.T) { | |
301 | cap := &capture{} | |
302 | sink := newSink(cap.Func, Options{LogCaller: Info}) | |
303 | sink.Error(fmt.Errorf("err"), "msg") | |
304 | expect := ` "msg"="msg" "error"="err"` | |
305 | if cap.log != expect { | |
306 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
307 | } | |
308 | }) | |
309 | t.Run("LogCaller=None", func(t *testing.T) { | |
310 | cap := &capture{} | |
311 | sink := newSink(cap.Func, Options{LogCaller: None}) | |
312 | sink.Error(fmt.Errorf("err"), "msg") | |
313 | expect := ` "msg"="msg" "error"="err"` | |
314 | if cap.log != expect { | |
315 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
316 | } | |
317 | }) | |
318 | } | |
319 | ||
320 | func TestInfoWithName(t *testing.T) { | |
321 | testCases := []struct { | |
322 | name string | |
323 | names []string | |
324 | args []interface{} | |
325 | expect string | |
326 | }{{ | |
327 | name: "one", | |
328 | names: []string{"pfx1"}, | |
329 | args: makeKV("k", "v"), | |
330 | expect: `pfx1 "level"=0 "msg"="msg" "k"="v"`, | |
331 | }, { | |
332 | name: "two", | |
333 | names: []string{"pfx1", "pfx2"}, | |
334 | args: makeKV("k", "v"), | |
335 | expect: `pfx1/pfx2 "level"=0 "msg"="msg" "k"="v"`, | |
336 | }} | |
337 | ||
338 | for _, tc := range testCases { | |
339 | t.Run(tc.name, func(t *testing.T) { | |
340 | cap := &capture{} | |
341 | sink := newSink(cap.Func, Options{}) | |
342 | for _, n := range tc.names { | |
343 | sink = sink.WithName(n) | |
344 | } | |
345 | sink.Info(0, "msg", tc.args...) | |
346 | if cap.log != tc.expect { | |
347 | t.Errorf("\nexpected %q\n got %q", tc.expect, cap.log) | |
348 | } | |
349 | }) | |
350 | } | |
351 | } | |
352 | ||
353 | func TestErrorWithName(t *testing.T) { | |
354 | testCases := []struct { | |
355 | name string | |
356 | names []string | |
357 | args []interface{} | |
358 | expect string | |
359 | }{{ | |
360 | name: "one", | |
361 | names: []string{"pfx1"}, | |
362 | args: makeKV("k", "v"), | |
363 | expect: `pfx1 "msg"="msg" "error"="err" "k"="v"`, | |
364 | }, { | |
365 | name: "two", | |
366 | names: []string{"pfx1", "pfx2"}, | |
367 | args: makeKV("k", "v"), | |
368 | expect: `pfx1/pfx2 "msg"="msg" "error"="err" "k"="v"`, | |
369 | }} | |
370 | ||
371 | for _, tc := range testCases { | |
372 | t.Run(tc.name, func(t *testing.T) { | |
373 | cap := &capture{} | |
374 | sink := newSink(cap.Func, Options{}) | |
375 | for _, n := range tc.names { | |
376 | sink = sink.WithName(n) | |
377 | } | |
378 | sink.Error(fmt.Errorf("err"), "msg", tc.args...) | |
379 | if cap.log != tc.expect { | |
380 | t.Errorf("\nexpected %q\n got %q", tc.expect, cap.log) | |
381 | } | |
382 | }) | |
383 | } | |
384 | } | |
385 | ||
386 | func TestInfoWithValues(t *testing.T) { | |
387 | testCases := []struct { | |
388 | name string | |
389 | values []interface{} | |
390 | args []interface{} | |
391 | expect string | |
392 | }{{ | |
393 | name: "zero", | |
394 | values: makeKV(), | |
395 | args: makeKV("k", "v"), | |
396 | expect: ` "level"=0 "msg"="msg" "k"="v"`, | |
397 | }, { | |
398 | name: "one", | |
399 | values: makeKV("one", 1), | |
400 | args: makeKV("k", "v"), | |
401 | expect: ` "level"=0 "msg"="msg" "one"=1 "k"="v"`, | |
402 | }, { | |
403 | name: "two", | |
404 | values: makeKV("one", 1, "two", 2), | |
405 | args: makeKV("k", "v"), | |
406 | expect: ` "level"=0 "msg"="msg" "one"=1 "two"=2 "k"="v"`, | |
407 | }} | |
408 | ||
409 | for _, tc := range testCases { | |
410 | t.Run(tc.name, func(t *testing.T) { | |
411 | cap := &capture{} | |
412 | sink := newSink(cap.Func, Options{}) | |
413 | sink = sink.WithValues(tc.values...) | |
414 | sink.Info(0, "msg", tc.args...) | |
415 | if cap.log != tc.expect { | |
416 | t.Errorf("\nexpected %q\n got %q", tc.expect, cap.log) | |
417 | } | |
418 | }) | |
419 | } | |
420 | } | |
421 | ||
422 | func TestErrorWithValues(t *testing.T) { | |
423 | testCases := []struct { | |
424 | name string | |
425 | values []interface{} | |
426 | args []interface{} | |
427 | expect string | |
428 | }{{ | |
429 | name: "zero", | |
430 | values: makeKV(), | |
431 | args: makeKV("k", "v"), | |
432 | expect: ` "msg"="msg" "error"="err" "k"="v"`, | |
433 | }, { | |
434 | name: "one", | |
435 | values: makeKV("one", 1), | |
436 | args: makeKV("k", "v"), | |
437 | expect: ` "msg"="msg" "error"="err" "one"=1 "k"="v"`, | |
438 | }, { | |
439 | name: "two", | |
440 | values: makeKV("one", 1, "two", 2), | |
441 | args: makeKV("k", "v"), | |
442 | expect: ` "msg"="msg" "error"="err" "one"=1 "two"=2 "k"="v"`, | |
443 | }} | |
444 | ||
445 | for _, tc := range testCases { | |
446 | t.Run(tc.name, func(t *testing.T) { | |
447 | cap := &capture{} | |
448 | sink := newSink(cap.Func, Options{}) | |
449 | sink = sink.WithValues(tc.values...) | |
450 | sink.Error(fmt.Errorf("err"), "msg", tc.args...) | |
451 | if cap.log != tc.expect { | |
452 | t.Errorf("\nexpected %q\n got %q", tc.expect, cap.log) | |
453 | } | |
454 | }) | |
455 | } | |
456 | } | |
457 | ||
458 | func TestInfoWithCallDepth(t *testing.T) { | |
459 | t.Run("one", func(t *testing.T) { | |
460 | cap := &capture{} | |
461 | sink := newSink(cap.Func, Options{LogCaller: All}) | |
462 | dSink := sink.(logr.CallDepthLogSink) | |
463 | sink = dSink.WithCallDepth(1) | |
464 | sink.Info(0, "msg") | |
465 | _, file, line, _ := runtime.Caller(1) | |
466 | expect := fmt.Sprintf(` "caller"={"file":%q,"line":%d} "level"=0 "msg"="msg"`, filepath.Base(file), line) | |
467 | if cap.log != expect { | |
468 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
469 | } | |
470 | }) | |
471 | } | |
472 | ||
473 | func TestErrorWithCallDepth(t *testing.T) { | |
474 | t.Run("one", func(t *testing.T) { | |
475 | cap := &capture{} | |
476 | sink := newSink(cap.Func, Options{LogCaller: All}) | |
477 | dSink := sink.(logr.CallDepthLogSink) | |
478 | sink = dSink.WithCallDepth(1) | |
479 | sink.Error(fmt.Errorf("err"), "msg") | |
480 | _, file, line, _ := runtime.Caller(1) | |
481 | expect := fmt.Sprintf(` "caller"={"file":%q,"line":%d} "msg"="msg" "error"="err"`, filepath.Base(file), line) | |
482 | if cap.log != expect { | |
483 | t.Errorf("\nexpected %q\n got %q", expect, cap.log) | |
484 | } | |
485 | }) | |
486 | } |
15 | 15 | |
16 | 16 | // This design derives from Dave Cheney's blog: |
17 | 17 | // http://dave.cheney.net/2015/11/05/lets-talk-about-logging |
18 | // | |
19 | // This is a BETA grade API. Until there is a significant 2nd implementation, | |
20 | // I don't really know how it will change. | |
21 | ||
22 | // Package logr defines abstract interfaces for logging. Packages can depend on | |
23 | // these interfaces and callers can implement logging in whatever way is | |
24 | // appropriate. | |
18 | ||
19 | // Package logr defines a general-purpose logging API and abstract interfaces | |
20 | // to back that API. Packages in the Go ecosystem can depend on this package, | |
21 | // while callers can implement logging with whatever backend is appropriate. | |
25 | 22 | // |
26 | 23 | // Usage |
27 | // | |
28 | // Logging is done using a Logger. Loggers can have name prefixes and named | |
29 | // values attached, so that all log messages logged with that Logger have some | |
30 | // base context associated. | |
31 | // | |
32 | // The term "key" is used to refer to the name associated with a particular | |
33 | // value, to disambiguate it from the general Logger name. | |
34 | // | |
35 | // For instance, suppose we're trying to reconcile the state of an object, and | |
36 | // we want to log that we've made some decision. | |
37 | // | |
38 | // With the traditional log package, we might write: | |
24 | // ----- | |
25 | // | |
26 | // Logging is done using a Logger instance. Logger is a concrete type with | |
27 | // methods, which defers the actual logging to a LogSink interface. The main | |
28 | // methods of Logger are Info() and Error(). Arguments to Info() and Error() | |
29 | // are key/value pairs rather than printf-style formatted strings, emphasizing | |
30 | // "structured logging". | |
31 | // | |
32 | // With Go's standard log package, we might write: | |
33 | // log.Printf("setting target value %s", targetValue) | |
34 | // | |
35 | // With logr's structured logging, we'd write: | |
36 | // logger.Info("setting target", "value", targetValue) | |
37 | // | |
38 | // Errors are much the same. Instead of: | |
39 | // log.Printf("failed to open the pod bay door for user %s: %v", user, err) | |
40 | // | |
41 | // We'd write: | |
42 | // logger.Error(err, "failed to open the pod bay door", "user", user) | |
43 | // | |
44 | // Info() and Error() are very similar, but they are separate methods so that | |
45 | // LogSink implementations can choose to do things like attach additional | |
46 | // information (such as stack traces) on calls to Error(). | |
47 | // | |
48 | // Verbosity | |
49 | // --------- | |
50 | // | |
51 | // Often we want to log information only when the application in "verbose | |
52 | // mode". To write log-lines that are more verbose, Logger has a V() method. | |
53 | // The higher the V-level of a log-line, the less critical it is considered. | |
54 | // Log-lines with V-levels that are not enabled (as per the LogSink) will not | |
55 | // be written. Level V(0) is the default, and logger.V(0).Info() has the same | |
56 | // meaning as logger.Info(). Negative V-levels have the same meaning as V(0). | |
57 | // | |
58 | // Where we might have written: | |
59 | // if flVerbose >= 2 { | |
60 | // log.Printf("an unusual thing happened") | |
61 | // } | |
62 | // | |
63 | // We can write: | |
64 | // logger.V(2).Info("an unusual thing happened") | |
65 | // | |
66 | // Logger Names | |
67 | // ------------ | |
68 | // | |
69 | // Logger instances can have name strings so that all messages logged through | |
70 | // that instance have additional context. For example, you might want to add | |
71 | // a subsystem name: | |
72 | // | |
73 | // logger.WithName("compactor").Info("started", "time", time.Now()) | |
74 | // | |
75 | // The WithName() method returns a new Logger, which can be passed to | |
76 | // constructors or other functions for further use. Repeated use of WithName() | |
77 | // will accumulate name "segments". These name segments will be joined in some | |
78 | // way by the LogSink implementation. It is strongly recommended that name | |
79 | // segments contain simple identifiers (letters, digits, and hyphen), and do | |
80 | // not contain characters that could muddle the log output or confuse the | |
81 | // joining operation (e.g. whitespace, commas, periods, slashes, brackets, | |
82 | // quotes, etc). | |
83 | // | |
84 | // Saved Values | |
85 | // ------------ | |
86 | // | |
87 | // Logger instances can store any number of key/value pairs, which will be | |
88 | // logged alongside all messages logged through that instance. For example, | |
89 | // you might want to create a Logger instance per managed object: | |
90 | // | |
91 | // With the standard log package, we might write: | |
39 | 92 | // log.Printf("decided to set field foo to value %q for object %s/%s", |
40 | 93 | // targetValue, object.Namespace, object.Name) |
41 | 94 | // |
42 | // With logr's structured logging, we'd write: | |
43 | // // elsewhere in the file, set up the logger to log with the prefix of | |
44 | // // "reconcilers", and the named value target-type=Foo, for extra context. | |
45 | // log := mainLogger.WithName("reconcilers").WithValues("target-type", "Foo") | |
95 | // With logr's we'd write: | |
96 | // // Elsewhere: set up the logger to log the object name. | |
97 | // obj.logger = mainLogger.WithValues( | |
98 | // "name", obj.name, "namespace", obj.namespace) | |
46 | 99 | // |
47 | 100 | // // later on... |
48 | // log.Info("setting foo on object", "value", targetValue, "object", object) | |
49 | // | |
50 | // Depending on our logging implementation, we could then make logging decisions | |
51 | // based on field values (like only logging such events for objects in a certain | |
52 | // namespace), or copy the structured information into a structured log store. | |
53 | // | |
54 | // For logging errors, Logger has a method called Error. Suppose we wanted to | |
55 | // log an error while reconciling. With the traditional log package, we might | |
56 | // write: | |
57 | // log.Errorf("unable to reconcile object %s/%s: %v", object.Namespace, object.Name, err) | |
58 | // | |
59 | // With logr, we'd instead write: | |
60 | // // assuming the above setup for log | |
61 | // log.Error(err, "unable to reconcile object", "object", object) | |
62 | // | |
63 | // This functions similarly to: | |
64 | // log.Info("unable to reconcile object", "error", err, "object", object) | |
65 | // | |
66 | // However, it ensures that a standard key for the error value ("error") is used | |
67 | // across all error logging. Furthermore, certain implementations may choose to | |
68 | // attach additional information (such as stack traces) on calls to Error, so | |
69 | // it's preferred to use Error to log errors. | |
70 | // | |
71 | // Parts of a log line | |
72 | // | |
73 | // Each log message from a Logger has four types of context: | |
74 | // logger name, log verbosity, log message, and the named values. | |
75 | // | |
76 | // The Logger name consists of a series of name "segments" added by successive | |
77 | // calls to WithName. These name segments will be joined in some way by the | |
78 | // underlying implementation. It is strongly recommended that name segments | |
79 | // contain simple identifiers (letters, digits, and hyphen), and do not contain | |
80 | // characters that could muddle the log output or confuse the joining operation | |
81 | // (e.g. whitespace, commas, periods, slashes, brackets, quotes, etc). | |
82 | // | |
83 | // Log verbosity represents how little a log matters. Level zero, the default, | |
84 | // matters most. Increasing levels matter less and less. Try to avoid lots of | |
85 | // different verbosity levels, and instead provide useful keys, logger names, | |
86 | // and log messages for users to filter on. It's illegal to pass a log level | |
87 | // below zero. | |
101 | // obj.logger.Info("setting foo", "value", targetValue) | |
102 | // | |
103 | // Best Practices | |
104 | // -------------- | |
105 | // | |
106 | // Logger has very few hard rules, with the goal that LogSink implementations | |
107 | // might have a lot of freedom to differentiate. There are, however, some | |
108 | // things to consider. | |
88 | 109 | // |
89 | 110 | // The log message consists of a constant message attached to the log line. |
90 | 111 | // This should generally be a simple description of what's occurring, and should |
91 | // never be a format string. | |
92 | // | |
93 | // Variable information can then be attached using named values (key/value | |
94 | // pairs). Keys are arbitrary strings, while values may be any Go value. | |
112 | // never be a format string. Variable information can then be attached using | |
113 | // named values. | |
114 | // | |
115 | // Keys are arbitrary strings, but should generally be constant values. Values | |
116 | // may be any Go value, but how the value is formatted is determined by the | |
117 | // LogSink implementation. | |
95 | 118 | // |
96 | 119 | // Key Naming Conventions |
120 | // ---------------------- | |
97 | 121 | // |
98 | 122 | // Keys are not strictly required to conform to any specification or regex, but |
99 | 123 | // it is recommended that they: |
123 | 147 | // above concepts, when necessary (for example, in a pure-JSON output form, it |
124 | 148 | // would be necessary to represent at least message and timestamp as ordinary |
125 | 149 | // named values). |
150 | // | |
151 | // Break Glass | |
152 | // ----------- | |
126 | 153 | // |
127 | 154 | // Implementations may choose to give callers access to the underlying |
128 | 155 | // logging implementation. The recommended pattern for this is: |
139 | 166 | "context" |
140 | 167 | ) |
141 | 168 | |
142 | // TODO: consider adding back in format strings if they're really needed | |
143 | // TODO: consider other bits of zap/zapcore functionality like ObjectMarshaller (for arbitrary objects) | |
144 | // TODO: consider other bits of glog functionality like Flush, OutputStats | |
145 | ||
146 | // Logger represents the ability to log messages, both errors and not. | |
147 | type Logger interface { | |
148 | // Enabled tests whether this Logger is enabled. For example, commandline | |
149 | // flags might be used to set the logging verbosity and disable some info | |
150 | // logs. | |
151 | Enabled() bool | |
152 | ||
153 | // Info logs a non-error message with the given key/value pairs as context. | |
154 | // | |
155 | // The msg argument should be used to add some constant description to | |
156 | // the log line. The key/value pairs can then be used to add additional | |
157 | // variable information. The key/value pairs should alternate string | |
158 | // keys and arbitrary values. | |
159 | Info(msg string, keysAndValues ...interface{}) | |
160 | ||
161 | // Error logs an error, with the given message and key/value pairs as context. | |
162 | // It functions similarly to calling Info with the "error" named value, but may | |
163 | // have unique behavior, and should be preferred for logging errors (see the | |
164 | // package documentations for more information). | |
165 | // | |
166 | // The msg field should be used to add context to any underlying error, | |
167 | // while the err field should be used to attach the actual error that | |
168 | // triggered this log line, if present. | |
169 | Error(err error, msg string, keysAndValues ...interface{}) | |
170 | ||
171 | // V returns an Logger value for a specific verbosity level, relative to | |
172 | // this Logger. In other words, V values are additive. V higher verbosity | |
173 | // level means a log message is less important. It's illegal to pass a log | |
174 | // level less than zero. | |
175 | V(level int) Logger | |
176 | ||
177 | // WithValues adds some key-value pairs of context to a logger. | |
178 | // See Info for documentation on how key/value pairs work. | |
179 | WithValues(keysAndValues ...interface{}) Logger | |
180 | ||
181 | // WithName adds a new element to the logger's name. | |
182 | // Successive calls with WithName continue to append | |
183 | // suffixes to the logger's name. It's strongly recommended | |
184 | // that name segments contain only letters, digits, and hyphens | |
185 | // (see the package documentation for more information). | |
186 | WithName(name string) Logger | |
187 | } | |
188 | ||
189 | // InfoLogger provides compatibility with code that relies on the v0.1.0 | |
190 | // interface. | |
191 | // | |
192 | // Deprecated: InfoLogger is an artifact of early versions of this API. New | |
193 | // users should never use it and existing users should use Logger instead. This | |
194 | // will be removed in a future release. | |
195 | type InfoLogger = Logger | |
196 | ||
169 | // New returns a new Logger instance. This is primarily used by libraries | |
170 | // implementing LogSink, rather than end users. | |
171 | func New(sink LogSink) Logger { | |
172 | logger := Logger{ | |
173 | sink: sink, | |
174 | } | |
175 | if withCallDepth, ok := sink.(CallDepthLogSink); ok { | |
176 | logger.withCallDepth = withCallDepth | |
177 | } | |
178 | sink.Init(runtimeInfo) | |
179 | return logger | |
180 | } | |
181 | ||
182 | // Logger is an interface to an abstract logging implementation. This is a | |
183 | // concrete type for performance reasons, but all the real work is passed on | |
184 | // to a LogSink. Implementations of LogSink should provide their own | |
185 | // constructors that return Logger, not LogSink. | |
186 | type Logger struct { | |
187 | level int | |
188 | sink LogSink | |
189 | withCallDepth CallDepthLogSink | |
190 | } | |
191 | ||
192 | // Enabled tests whether this Logger is enabled. For example, commandline | |
193 | // flags might be used to set the logging verbosity and disable some info | |
194 | // logs. | |
195 | func (l Logger) Enabled() bool { | |
196 | return l.sink.Enabled(l.level) | |
197 | } | |
198 | ||
199 | // Info logs a non-error message with the given key/value pairs as context. | |
200 | // | |
201 | // The msg argument should be used to add some constant description to | |
202 | // the log line. The key/value pairs can then be used to add additional | |
203 | // variable information. The key/value pairs must alternate string | |
204 | // keys and arbitrary values. | |
205 | func (l Logger) Info(msg string, keysAndValues ...interface{}) { | |
206 | if l.Enabled() { | |
207 | l.sink.Info(l.level, msg, keysAndValues...) | |
208 | } | |
209 | } | |
210 | ||
211 | // Error logs an error, with the given message and key/value pairs as context. | |
212 | // It functions similarly to Info, but may have unique behavior, and should be | |
213 | // preferred for logging errors (see the package documentations for more | |
214 | // information). | |
215 | // | |
216 | // The msg argument should be used to add context to any underlying error, | |
217 | // while the err argument should be used to attach the actual error that | |
218 | // triggered this log line, if present. | |
219 | func (l Logger) Error(err error, msg string, keysAndValues ...interface{}) { | |
220 | l.sink.Error(err, msg, keysAndValues...) | |
221 | } | |
222 | ||
223 | // V returns a new Logger instance for a specific verbosity level, relative to | |
224 | // this Logger. In other words, V-levels are additive. A higher verbosity | |
225 | // level means a log message is less important. Negative V-levels are treated | |
226 | // as 0. | |
227 | func (l Logger) V(level int) Logger { | |
228 | if level < 0 { | |
229 | level = 0 | |
230 | } | |
231 | l.level += level | |
232 | return l | |
233 | } | |
234 | ||
235 | // WithValues returns a new Logger instance with additional key/value pairs. | |
236 | // See Info for documentation on how key/value pairs work. | |
237 | func (l Logger) WithValues(keysAndValues ...interface{}) Logger { | |
238 | l.sink = l.sink.WithValues(keysAndValues...) | |
239 | return l | |
240 | } | |
241 | ||
242 | // WithName returns a new Logger instance with the specified name element added | |
243 | // to the Logger's name. Successive calls with WithName append additional | |
244 | // suffixes to the Logger's name. It's strongly recommended that name segments | |
245 | // contain only letters, digits, and hyphens (see the package documentation for | |
246 | // more information). | |
247 | func (l Logger) WithName(name string) Logger { | |
248 | l.sink = l.sink.WithName(name) | |
249 | return l | |
250 | } | |
251 | ||
252 | // WithCallDepth returns a Logger instance that offsets the call stack by the | |
253 | // specified number of frames when logging call site information, if possible. | |
254 | // This is useful for users who have helper functions between the "real" call | |
255 | // site and the actual calls to Logger methods. If depth is 0 the attribution | |
256 | // should be to the direct caller of this function. If depth is 1 the | |
257 | // attribution should skip 1 call frame, and so on. Successive calls to this | |
258 | // are additive. | |
259 | // | |
260 | // If the underlying log implementation supports a WithCallDepth(int) method, | |
261 | // it will be called and the result returned. If the implementation does not | |
262 | // support CallDepthLogSink, the original Logger will be returned. | |
263 | func (l Logger) WithCallDepth(depth int) Logger { | |
264 | if l.withCallDepth == nil { | |
265 | return l | |
266 | } | |
267 | l.sink = l.withCallDepth.WithCallDepth(depth) | |
268 | return l | |
269 | } | |
270 | ||
271 | // contextKey is how we find Loggers in a context.Context. | |
197 | 272 | type contextKey struct{} |
198 | 273 | |
199 | // FromContext returns a Logger constructed from ctx or nil if no | |
200 | // logger details are found. | |
201 | func FromContext(ctx context.Context) Logger { | |
274 | // FromContext returns a Logger from ctx or an error if no Logger is found. | |
275 | func FromContext(ctx context.Context) (Logger, error) { | |
202 | 276 | if v, ok := ctx.Value(contextKey{}).(Logger); ok { |
203 | return v | |
204 | } | |
205 | ||
206 | return nil | |
207 | } | |
208 | ||
209 | // FromContextOrDiscard returns a Logger constructed from ctx or a Logger | |
210 | // that discards all messages if no logger details are found. | |
277 | return v, nil | |
278 | } | |
279 | ||
280 | return Logger{}, notFoundError{} | |
281 | } | |
282 | ||
283 | // notFoundError exists to carry an IsNotFound method. | |
284 | type notFoundError struct{} | |
285 | ||
286 | func (notFoundError) Error() string { | |
287 | return "no logr.Logger was present" | |
288 | } | |
289 | ||
290 | func (notFoundError) IsNotFound() bool { | |
291 | return true | |
292 | } | |
293 | ||
294 | // FromContextOrDiscard returns a Logger from ctx. If no Logger is found, this | |
295 | // returns a Logger that discards all log messages. | |
211 | 296 | func FromContextOrDiscard(ctx context.Context) Logger { |
212 | 297 | if v, ok := ctx.Value(contextKey{}).(Logger); ok { |
213 | 298 | return v |
216 | 301 | return Discard() |
217 | 302 | } |
218 | 303 | |
219 | // NewContext returns a new context derived from ctx that embeds the Logger. | |
220 | func NewContext(ctx context.Context, l Logger) context.Context { | |
221 | return context.WithValue(ctx, contextKey{}, l) | |
222 | } | |
223 | ||
224 | // CallDepthLogger represents a Logger that knows how to climb the call stack | |
304 | // NewContext returns a new Context, derived from ctx, which carries the | |
305 | // provided Logger. | |
306 | func NewContext(ctx context.Context, logger Logger) context.Context { | |
307 | return context.WithValue(ctx, contextKey{}, logger) | |
308 | } | |
309 | ||
310 | // RuntimeInfo holds information that the logr "core" library knows which | |
311 | // LogSinks might want to know. | |
312 | type RuntimeInfo struct { | |
313 | // CallDepth is the number of call frames the logr library adds between the | |
314 | // end-user and the LogSink. LogSink implementations which choose to print | |
315 | // the original logging site (e.g. file & line) should climb this many | |
316 | // additional frames to find it. | |
317 | CallDepth int | |
318 | } | |
319 | ||
320 | // runtimeInfo is a static global. It must not be changed at run time. | |
321 | var runtimeInfo = RuntimeInfo{ | |
322 | CallDepth: 1, | |
323 | } | |
324 | ||
325 | // LogSink represents a logging implementation. End-users will generally not | |
326 | // interact with this type. | |
327 | type LogSink interface { | |
328 | // Init receives optional information about the logr library for LogSink | |
329 | // implementations that need it. | |
330 | Init(info RuntimeInfo) | |
331 | ||
332 | // Enabled tests whether this LogSink is enabled at the specified V-level. | |
333 | // For example, commandline flags might be used to set the logging | |
334 | // verbosity and disable some info logs. | |
335 | Enabled(level int) bool | |
336 | ||
337 | // Info logs a non-error message with the given key/value pairs as context. | |
338 | // The level argument is provided for optional logging. This method will | |
339 | // only be called when Enabled(level) is true. See Logger.Info for more | |
340 | // details. | |
341 | Info(level int, msg string, keysAndValues ...interface{}) | |
342 | ||
343 | // Error logs an error, with the given message and key/value pairs as | |
344 | // context. See Logger.Error for more details. | |
345 | Error(err error, msg string, keysAndValues ...interface{}) | |
346 | ||
347 | // WithValues returns a new LogSink with additional key/value pairs. See | |
348 | // Logger.WithValues for more details. | |
349 | WithValues(keysAndValues ...interface{}) LogSink | |
350 | ||
351 | // WithName returns a new LogSink with the specified name appended. See | |
352 | // Logger.WithName for more details. | |
353 | WithName(name string) LogSink | |
354 | } | |
355 | ||
356 | // CallDepthLogSink represents a Logger that knows how to climb the call stack | |
225 | 357 | // to identify the original call site and can offset the depth by a specified |
226 | 358 | // number of frames. This is useful for users who have helper functions |
227 | 359 | // between the "real" call site and the actual calls to Logger methods. |
231 | 363 | // |
232 | 364 | // This is an optional interface and implementations are not required to |
233 | 365 | // support it. |
234 | type CallDepthLogger interface { | |
235 | Logger | |
236 | ||
366 | type CallDepthLogSink interface { | |
237 | 367 | // WithCallDepth returns a Logger that will offset the call stack by the |
238 | 368 | // specified number of frames when logging call site information. If depth |
239 | 369 | // is 0 the attribution should be to the direct caller of this method. If |
240 | 370 | // depth is 1 the attribution should skip 1 call frame, and so on. |
241 | 371 | // Successive calls to this are additive. |
242 | WithCallDepth(depth int) Logger | |
243 | } | |
244 | ||
245 | // WithCallDepth returns a Logger that will offset the call stack by the | |
246 | // specified number of frames when logging call site information, if possible. | |
247 | // This is useful for users who have helper functions between the "real" call | |
248 | // site and the actual calls to Logger methods. If depth is 0 the attribution | |
249 | // should be to the direct caller of this function. If depth is 1 the | |
250 | // attribution should skip 1 call frame, and so on. Successive calls to this | |
251 | // are additive. | |
252 | // | |
253 | // If the underlying log implementation supports the CallDepthLogger interface, | |
254 | // the WithCallDepth method will be called and the result returned. If the | |
255 | // implementation does not support CallDepthLogger, the original Logger will be | |
256 | // returned. | |
257 | // | |
258 | // Callers which care about whether this was supported or not should test for | |
259 | // CallDepthLogger support themselves. | |
260 | func WithCallDepth(logger Logger, depth int) Logger { | |
261 | if decorator, ok := logger.(CallDepthLogger); ok { | |
262 | return decorator.WithCallDepth(depth) | |
263 | } | |
264 | return logger | |
265 | } | |
372 | WithCallDepth(depth int) LogSink | |
373 | } |
17 | 17 | |
18 | 18 | import ( |
19 | 19 | "context" |
20 | "fmt" | |
21 | "reflect" | |
20 | 22 | "testing" |
21 | 23 | ) |
22 | 24 | |
23 | // testLogger is a Logger just for testing that does nothing. | |
24 | type testLogger struct{} | |
25 | ||
26 | func (l *testLogger) Enabled() bool { | |
25 | // testLogSink is a Logger just for testing that calls optional hooks on each method. | |
26 | type testLogSink struct { | |
27 | fnInit func(ri RuntimeInfo) | |
28 | fnEnabled func(lvl int) bool | |
29 | fnInfo func(lvl int, msg string, kv ...interface{}) | |
30 | fnError func(err error, msg string, kv ...interface{}) | |
31 | fnWithValues func(kv ...interface{}) | |
32 | fnWithName func(name string) | |
33 | } | |
34 | ||
35 | var _ LogSink = &testLogSink{} | |
36 | ||
37 | func (l *testLogSink) Init(ri RuntimeInfo) { | |
38 | if l.fnInit != nil { | |
39 | l.fnInit(ri) | |
40 | } | |
41 | } | |
42 | ||
43 | func (l *testLogSink) Enabled(lvl int) bool { | |
44 | if l.fnEnabled != nil { | |
45 | return l.fnEnabled(lvl) | |
46 | } | |
27 | 47 | return false |
28 | 48 | } |
29 | 49 | |
30 | func (l *testLogger) Info(msg string, keysAndValues ...interface{}) { | |
31 | } | |
32 | ||
33 | func (l *testLogger) Error(err error, msg string, keysAndValues ...interface{}) { | |
34 | } | |
35 | ||
36 | func (l *testLogger) V(level int) Logger { | |
37 | return l | |
38 | } | |
39 | ||
40 | func (l *testLogger) WithValues(keysAndValues ...interface{}) Logger { | |
41 | return l | |
42 | } | |
43 | ||
44 | func (l *testLogger) WithName(name string) Logger { | |
45 | return l | |
46 | } | |
47 | ||
48 | // Verify that it actually implements the interface | |
49 | var _ Logger = &testLogger{} | |
50 | func (l *testLogSink) Info(lvl int, msg string, kv ...interface{}) { | |
51 | if l.fnInfo != nil { | |
52 | l.fnInfo(lvl, msg, kv...) | |
53 | } | |
54 | } | |
55 | ||
56 | func (l *testLogSink) Error(err error, msg string, kv ...interface{}) { | |
57 | if l.fnError != nil { | |
58 | l.fnError(err, msg, kv...) | |
59 | } | |
60 | } | |
61 | ||
62 | func (l *testLogSink) WithValues(kv ...interface{}) LogSink { | |
63 | if l.fnWithValues != nil { | |
64 | l.fnWithValues(kv...) | |
65 | } | |
66 | out := *l | |
67 | return &out | |
68 | } | |
69 | ||
70 | func (l *testLogSink) WithName(name string) LogSink { | |
71 | if l.fnWithName != nil { | |
72 | l.fnWithName(name) | |
73 | } | |
74 | out := *l | |
75 | return &out | |
76 | } | |
77 | ||
78 | type testCallDepthLogSink struct { | |
79 | testLogSink | |
80 | fnWithCallDepth func(depth int) | |
81 | } | |
82 | ||
83 | var _ CallDepthLogSink = &testCallDepthLogSink{} | |
84 | ||
85 | func (l *testCallDepthLogSink) WithCallDepth(depth int) LogSink { | |
86 | if l.fnWithCallDepth != nil { | |
87 | l.fnWithCallDepth(depth) | |
88 | } | |
89 | out := *l | |
90 | return &out | |
91 | } | |
92 | ||
93 | func TestNew(t *testing.T) { | |
94 | calledInit := 0 | |
95 | ||
96 | sink := &testLogSink{} | |
97 | sink.fnInit = func(ri RuntimeInfo) { | |
98 | if ri.CallDepth != 1 { | |
99 | t.Errorf("expected runtimeInfo.CallDepth = 1, got %d", ri.CallDepth) | |
100 | } | |
101 | calledInit++ | |
102 | } | |
103 | logger := New(sink) | |
104 | ||
105 | if logger.sink == nil { | |
106 | t.Errorf("expected sink to be set, got %v", logger.sink) | |
107 | } | |
108 | if calledInit != 1 { | |
109 | t.Errorf("expected sink.Init() to be called once, got %d", calledInit) | |
110 | } | |
111 | if logger.withCallDepth != nil { | |
112 | t.Errorf("expected withCallDepth to be nil, got %v", logger.withCallDepth) | |
113 | } | |
114 | } | |
115 | ||
116 | func TestNewCachesCallDepthInterface(t *testing.T) { | |
117 | sink := &testCallDepthLogSink{} | |
118 | logger := New(sink) | |
119 | ||
120 | if logger.withCallDepth == nil { | |
121 | t.Errorf("expected withCallDepth to be set") | |
122 | } | |
123 | } | |
124 | ||
125 | func TestEnabled(t *testing.T) { | |
126 | calledEnabled := 0 | |
127 | ||
128 | sink := &testLogSink{} | |
129 | sink.fnEnabled = func(lvl int) bool { | |
130 | calledEnabled++ | |
131 | return true | |
132 | } | |
133 | logger := New(sink) | |
134 | ||
135 | if en := logger.Enabled(); en != true { | |
136 | t.Errorf("expected true") | |
137 | } | |
138 | if calledEnabled != 1 { | |
139 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) | |
140 | } | |
141 | } | |
142 | ||
143 | func TestError(t *testing.T) { | |
144 | calledError := 0 | |
145 | errInput := fmt.Errorf("error") | |
146 | msgInput := "msg" | |
147 | kvInput := []interface{}{0, 1, 2} | |
148 | ||
149 | sink := &testLogSink{} | |
150 | sink.fnError = func(err error, msg string, kv ...interface{}) { | |
151 | calledError++ | |
152 | if err != errInput { | |
153 | t.Errorf("unexpected err input, got %v", err) | |
154 | } | |
155 | if msg != msgInput { | |
156 | t.Errorf("unexpected msg input, got %q", msg) | |
157 | } | |
158 | if !reflect.DeepEqual(kv, kvInput) { | |
159 | t.Errorf("unexpected kv input, got %v", kv) | |
160 | } | |
161 | } | |
162 | logger := New(sink) | |
163 | ||
164 | logger.Error(errInput, msgInput, kvInput...) | |
165 | if calledError != 1 { | |
166 | t.Errorf("expected sink.Error() to be called once, got %d", calledError) | |
167 | } | |
168 | } | |
169 | ||
170 | func TestV(t *testing.T) { | |
171 | sink := &testLogSink{} | |
172 | logger := New(sink) | |
173 | ||
174 | if l := logger.V(0); l.level != 0 { | |
175 | t.Errorf("expected level 0, got %d", l.level) | |
176 | } | |
177 | if l := logger.V(93); l.level != 93 { | |
178 | t.Errorf("expected level 93, got %d", l.level) | |
179 | } | |
180 | if l := logger.V(70).V(6); l.level != 76 { | |
181 | t.Errorf("expected level 76, got %d", l.level) | |
182 | } | |
183 | if l := logger.V(-1); l.level != 0 { | |
184 | t.Errorf("expected level 0, got %d", l.level) | |
185 | } | |
186 | if l := logger.V(1).V(-1); l.level != 1 { | |
187 | t.Errorf("expected level 1, got %d", l.level) | |
188 | } | |
189 | } | |
190 | ||
191 | func TestInfo(t *testing.T) { | |
192 | calledEnabled := 0 | |
193 | calledInfo := 0 | |
194 | lvlInput := 0 | |
195 | msgInput := "msg" | |
196 | kvInput := []interface{}{0, 1, 2} | |
197 | ||
198 | sink := &testLogSink{} | |
199 | sink.fnEnabled = func(lvl int) bool { | |
200 | calledEnabled++ | |
201 | return lvl < 100 | |
202 | } | |
203 | sink.fnInfo = func(lvl int, msg string, kv ...interface{}) { | |
204 | calledInfo++ | |
205 | if lvl != lvlInput { | |
206 | t.Errorf("unexpected lvl input, got %v", lvl) | |
207 | } | |
208 | if msg != msgInput { | |
209 | t.Errorf("unexpected msg input, got %q", msg) | |
210 | } | |
211 | if !reflect.DeepEqual(kv, kvInput) { | |
212 | t.Errorf("unexpected kv input, got %v", kv) | |
213 | } | |
214 | } | |
215 | logger := New(sink) | |
216 | ||
217 | calledEnabled = 0 | |
218 | calledInfo = 0 | |
219 | lvlInput = 0 | |
220 | logger.Info(msgInput, kvInput...) | |
221 | if calledEnabled != 1 { | |
222 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) | |
223 | } | |
224 | if calledInfo != 1 { | |
225 | t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) | |
226 | } | |
227 | ||
228 | calledEnabled = 0 | |
229 | calledInfo = 0 | |
230 | lvlInput = 0 | |
231 | logger.V(0).Info(msgInput, kvInput...) | |
232 | if calledEnabled != 1 { | |
233 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) | |
234 | } | |
235 | if calledInfo != 1 { | |
236 | t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) | |
237 | } | |
238 | ||
239 | calledEnabled = 0 | |
240 | calledInfo = 0 | |
241 | lvlInput = 93 | |
242 | logger.V(93).Info(msgInput, kvInput...) | |
243 | if calledEnabled != 1 { | |
244 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) | |
245 | } | |
246 | if calledInfo != 1 { | |
247 | t.Errorf("expected sink.Info() to be called once, got %d", calledInfo) | |
248 | } | |
249 | ||
250 | calledEnabled = 0 | |
251 | calledInfo = 0 | |
252 | lvlInput = 100 | |
253 | logger.V(100).Info(msgInput, kvInput...) | |
254 | if calledEnabled != 1 { | |
255 | t.Errorf("expected sink.Enabled() to be called once, got %d", calledEnabled) | |
256 | } | |
257 | if calledInfo != 0 { | |
258 | t.Errorf("expected sink.Info() to not be called, got %d", calledInfo) | |
259 | } | |
260 | } | |
261 | ||
262 | func TestWithValues(t *testing.T) { | |
263 | calledWithValues := 0 | |
264 | kvInput := []interface{}{"zero", 0, "one", 1, "two", 2} | |
265 | ||
266 | sink := &testLogSink{} | |
267 | sink.fnWithValues = func(kv ...interface{}) { | |
268 | calledWithValues++ | |
269 | if !reflect.DeepEqual(kv, kvInput) { | |
270 | t.Errorf("unexpected kv input, got %v", kv) | |
271 | } | |
272 | } | |
273 | logger := New(sink) | |
274 | ||
275 | out := logger.WithValues(kvInput...) | |
276 | if calledWithValues != 1 { | |
277 | t.Errorf("expected sink.WithValues() to be called once, got %d", calledWithValues) | |
278 | } | |
279 | if p := out.sink.(*testLogSink); p == sink { | |
280 | t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) | |
281 | } | |
282 | } | |
283 | ||
284 | func TestWithName(t *testing.T) { | |
285 | calledWithName := 0 | |
286 | nameInput := "name" | |
287 | ||
288 | sink := &testLogSink{} | |
289 | sink.fnWithName = func(name string) { | |
290 | calledWithName++ | |
291 | if name != nameInput { | |
292 | t.Errorf("unexpected name input, got %q", name) | |
293 | } | |
294 | } | |
295 | logger := New(sink) | |
296 | ||
297 | out := logger.WithName(nameInput) | |
298 | if calledWithName != 1 { | |
299 | t.Errorf("expected sink.WithName() to be called once, got %d", calledWithName) | |
300 | } | |
301 | if p := out.sink.(*testLogSink); p == sink { | |
302 | t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) | |
303 | } | |
304 | } | |
305 | ||
306 | func TestWithCallDepthNotImplemented(t *testing.T) { | |
307 | depthInput := 7 | |
308 | ||
309 | sink := &testLogSink{} | |
310 | logger := New(sink) | |
311 | ||
312 | out := logger.WithCallDepth(depthInput) | |
313 | if p := out.sink.(*testLogSink); p != sink { | |
314 | t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) | |
315 | } | |
316 | } | |
317 | ||
318 | func TestWithCallDepthImplemented(t *testing.T) { | |
319 | calledWithCallDepth := 0 | |
320 | depthInput := 7 | |
321 | ||
322 | sink := &testCallDepthLogSink{} | |
323 | sink.fnWithCallDepth = func(depth int) { | |
324 | calledWithCallDepth++ | |
325 | if depth != depthInput { | |
326 | t.Errorf("unexpected depth input, got %d", depth) | |
327 | } | |
328 | } | |
329 | logger := New(sink) | |
330 | ||
331 | out := logger.WithCallDepth(depthInput) | |
332 | if calledWithCallDepth != 1 { | |
333 | t.Errorf("expected sink.WithCallDepth() to be called once, got %d", calledWithCallDepth) | |
334 | } | |
335 | if p := out.sink.(*testCallDepthLogSink); p == sink { | |
336 | t.Errorf("expected output to be different from input, got in=%p, out=%p", sink, p) | |
337 | } | |
338 | } | |
50 | 339 | |
51 | 340 | func TestContext(t *testing.T) { |
52 | 341 | ctx := context.TODO() |
53 | 342 | |
54 | if out := FromContext(ctx); out != nil { | |
55 | t.Errorf("expected nil logger, got %#v", out) | |
56 | } | |
57 | if out := FromContextOrDiscard(ctx); out == nil { | |
58 | t.Errorf("expected non-nil logger") | |
59 | } else if _, ok := out.(DiscardLogger); !ok { | |
60 | t.Errorf("expected a DiscardLogger, got %#v", out) | |
61 | } | |
62 | ||
63 | logger := &testLogger{} | |
343 | if out, err := FromContext(ctx); err == nil { | |
344 | t.Errorf("expected error, got %#v", out) | |
345 | } else if _, ok := err.(notFoundError); !ok { | |
346 | t.Errorf("expected a notFoundError, got %#v", err) | |
347 | } | |
348 | ||
349 | out := FromContextOrDiscard(ctx) | |
350 | if _, ok := out.sink.(discardLogSink); !ok { | |
351 | t.Errorf("expected a discardLogSink, got %#v", out) | |
352 | } | |
353 | ||
354 | sink := &testLogSink{} | |
355 | logger := New(sink) | |
64 | 356 | lctx := NewContext(ctx, logger) |
65 | if out := FromContext(lctx); out == nil { | |
66 | t.Errorf("expected non-nil logger") | |
67 | } else if out.(*testLogger) != logger { | |
68 | t.Errorf("expected output to be the same as input: got in=%p, out=%p", logger, out) | |
69 | } | |
70 | if out := FromContextOrDiscard(lctx); out == nil { | |
71 | t.Errorf("expected non-nil logger") | |
72 | } else if out.(*testLogger) != logger { | |
73 | t.Errorf("expected output to be the same as input: got in=%p, out=%p", logger, out) | |
74 | } | |
75 | } | |
76 | ||
77 | // testCallDepthLogger is a Logger just for testing that does nothing. | |
78 | type testCallDepthLogger struct { | |
79 | *testLogger | |
80 | depth int | |
81 | } | |
82 | ||
83 | func (l *testCallDepthLogger) WithCallDepth(depth int) Logger { | |
84 | return &testCallDepthLogger{l.testLogger, l.depth + depth} | |
85 | } | |
86 | ||
87 | // Verify that it actually implements the interface | |
88 | var _ CallDepthLogger = &testCallDepthLogger{} | |
89 | ||
90 | func TestWithCallDepth(t *testing.T) { | |
91 | // Test an impl that does not support it. | |
92 | t.Run("not supported", func(t *testing.T) { | |
93 | in := &testLogger{} | |
94 | out := WithCallDepth(in, 42) | |
95 | if out.(*testLogger) != in { | |
96 | t.Errorf("expected output to be the same as input: got in=%p, out=%p", in, out) | |
97 | } | |
98 | }) | |
99 | ||
100 | // Test an impl that does support it. | |
101 | t.Run("supported", func(t *testing.T) { | |
102 | in := &testCallDepthLogger{&testLogger{}, 0} | |
103 | out := WithCallDepth(in, 42) | |
104 | if out.(*testCallDepthLogger) == in { | |
105 | t.Errorf("expected output to be different than input: got in=out=%p", in) | |
106 | } | |
107 | if cdl := out.(*testCallDepthLogger); cdl.depth != 42 { | |
108 | t.Errorf("expected depth=42, got %d", cdl.depth) | |
109 | } | |
110 | }) | |
111 | } | |
357 | if out, err := FromContext(lctx); err != nil { | |
358 | t.Errorf("unexpected error: %v", err) | |
359 | } else if p := out.sink.(*testLogSink); p != sink { | |
360 | t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) | |
361 | } | |
362 | out = FromContextOrDiscard(lctx) | |
363 | if p := out.sink.(*testLogSink); p != sink { | |
364 | t.Errorf("expected output to be the same as input, got in=%p, out=%p", sink, p) | |
365 | } | |
366 | } |
0 | /* | |
1 | Copyright 2019 The logr Authors. | |
2 | ||
3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | you may not use this file except in compliance with the License. | |
5 | You may obtain a copy of the License at | |
6 | ||
7 | http://www.apache.org/licenses/LICENSE-2.0 | |
8 | ||
9 | Unless required by applicable law or agreed to in writing, software | |
10 | distributed under the License is distributed on an "AS IS" BASIS, | |
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | See the License for the specific language governing permissions and | |
13 | limitations under the License. | |
14 | */ | |
15 | ||
16 | package testing | |
17 | ||
18 | import "github.com/go-logr/logr" | |
19 | ||
20 | // NullLogger is a logr.Logger that does nothing. | |
21 | // | |
22 | // Deprecated: NullLogger is idenitcal to logr.DiscardLogger. It is retained | |
23 | // for backwards compatibility, but new users should use logr.DiscardLogger | |
24 | // instead. | |
25 | type NullLogger = logr.DiscardLogger |
19 | 19 | "testing" |
20 | 20 | |
21 | 21 | "github.com/go-logr/logr" |
22 | "github.com/go-logr/logr/funcr" | |
22 | 23 | ) |
23 | 24 | |
24 | // TestLogger is a logr.Logger that prints through a testing.T object. | |
25 | // Only error logs will have any effect. | |
26 | type TestLogger struct { | |
27 | T *testing.T | |
25 | // NewTestLogger returns a logr.Logger that prints through a testing.T object. | |
26 | // Info logs are only enabled at V(0). | |
27 | func NewTestLogger(t *testing.T) logr.Logger { | |
28 | fn := func(prefix, args string) { | |
29 | t.Logf("%s: %s", prefix, args) | |
30 | } | |
31 | return funcr.New(fn, funcr.Options{}) | |
28 | 32 | } |
29 | ||
30 | var _ logr.Logger = TestLogger{} | |
31 | ||
32 | func (_ TestLogger) Info(_ string, _ ...interface{}) { | |
33 | // Do nothing. | |
34 | } | |
35 | ||
36 | func (_ TestLogger) Enabled() bool { | |
37 | return false | |
38 | } | |
39 | ||
40 | func (log TestLogger) Error(err error, msg string, args ...interface{}) { | |
41 | log.T.Logf("%s: %v -- %v", msg, err, args) | |
42 | } | |
43 | ||
44 | func (log TestLogger) V(v int) logr.Logger { | |
45 | return log | |
46 | } | |
47 | ||
48 | func (log TestLogger) WithName(_ string) logr.Logger { | |
49 | return log | |
50 | } | |
51 | ||
52 | func (log TestLogger) WithValues(_ ...interface{}) logr.Logger { | |
53 | return log | |
54 | } |
0 | /* | |
1 | Copyright 2021 The logr Authors. | |
2 | ||
3 | Licensed under the Apache License, Version 2.0 (the "License"); | |
4 | you may not use this file except in compliance with the License. | |
5 | You may obtain a copy of the License at | |
6 | ||
7 | http://www.apache.org/licenses/LICENSE-2.0 | |
8 | ||
9 | Unless required by applicable law or agreed to in writing, software | |
10 | distributed under the License is distributed on an "AS IS" BASIS, | |
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 | See the License for the specific language governing permissions and | |
13 | limitations under the License. | |
14 | */ | |
15 | ||
16 | package testing | |
17 | ||
18 | import ( | |
19 | "fmt" | |
20 | "testing" | |
21 | ) | |
22 | ||
23 | func TestTestLogger(t *testing.T) { | |
24 | logger := NewTestLogger(t) | |
25 | logger.Info("info") | |
26 | logger.V(0).Info("V(0).info") | |
27 | logger.V(1).Info("v(1).info") | |
28 | logger.Error(fmt.Errorf("error"), "error") | |
29 | } |