├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── benchmarks_test.go
├── checkers_test.go
├── config.go
├── config_test.go
├── context.go
├── context_test.go
├── doc.go
├── entry.go
├── example
├── first.go
├── main.go
└── second.go
├── export_test.go
├── formatter.go
├── formatter_test.go
├── global.go
├── global_test.go
├── go.mod
├── go.sum
├── helper.go
├── labels.go
├── level.go
├── level_test.go
├── logger.go
├── logger_test.go
├── logging_test.go
├── loggocolor
└── writer.go
├── module.go
├── package_test.go
├── testwriter.go
├── util_test.go
├── writer.go
└── writer_test.go
/.gitignore:
--------------------------------------------------------------------------------
1 | example/example
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | All files in this repository are licensed as follows. If you contribute
2 | to this repository, it is assumed that you license your contribution
3 | under the same license unless you state otherwise.
4 |
5 | All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file.
6 |
7 | This software is licensed under the LGPLv3, included below.
8 |
9 | As a special exception to the GNU Lesser General Public License version 3
10 | ("LGPL3"), the copyright holders of this Library give you permission to
11 | convey to a third party a Combined Work that links statically or dynamically
12 | to this Library without providing any Minimal Corresponding Source or
13 | Minimal Application Code as set out in 4d or providing the installation
14 | information set out in section 4e, provided that you comply with the other
15 | provisions of LGPL3 and provided that you meet, for the Application the
16 | terms and conditions of the license(s) which apply to the Application.
17 |
18 | Except as stated in this special exception, the provisions of LGPL3 will
19 | continue to comply in full to this Library. If you modify this Library, you
20 | may apply this exception to your version of this Library, but you are not
21 | obliged to do so. If you do not wish to do so, delete this exception
22 | statement from your version. This exception does not (and cannot) modify any
23 | license terms which apply to the Application, with which you must still
24 | comply.
25 |
26 |
27 | GNU LESSER GENERAL PUBLIC LICENSE
28 | Version 3, 29 June 2007
29 |
30 | Copyright (C) 2007 Free Software Foundation, Inc.
31 | Everyone is permitted to copy and distribute verbatim copies
32 | of this license document, but changing it is not allowed.
33 |
34 |
35 | This version of the GNU Lesser General Public License incorporates
36 | the terms and conditions of version 3 of the GNU General Public
37 | License, supplemented by the additional permissions listed below.
38 |
39 | 0. Additional Definitions.
40 |
41 | As used herein, "this License" refers to version 3 of the GNU Lesser
42 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
43 | General Public License.
44 |
45 | "The Library" refers to a covered work governed by this License,
46 | other than an Application or a Combined Work as defined below.
47 |
48 | An "Application" is any work that makes use of an interface provided
49 | by the Library, but which is not otherwise based on the Library.
50 | Defining a subclass of a class defined by the Library is deemed a mode
51 | of using an interface provided by the Library.
52 |
53 | A "Combined Work" is a work produced by combining or linking an
54 | Application with the Library. The particular version of the Library
55 | with which the Combined Work was made is also called the "Linked
56 | Version".
57 |
58 | The "Minimal Corresponding Source" for a Combined Work means the
59 | Corresponding Source for the Combined Work, excluding any source code
60 | for portions of the Combined Work that, considered in isolation, are
61 | based on the Application, and not on the Linked Version.
62 |
63 | The "Corresponding Application Code" for a Combined Work means the
64 | object code and/or source code for the Application, including any data
65 | and utility programs needed for reproducing the Combined Work from the
66 | Application, but excluding the System Libraries of the Combined Work.
67 |
68 | 1. Exception to Section 3 of the GNU GPL.
69 |
70 | You may convey a covered work under sections 3 and 4 of this License
71 | without being bound by section 3 of the GNU GPL.
72 |
73 | 2. Conveying Modified Versions.
74 |
75 | If you modify a copy of the Library, and, in your modifications, a
76 | facility refers to a function or data to be supplied by an Application
77 | that uses the facility (other than as an argument passed when the
78 | facility is invoked), then you may convey a copy of the modified
79 | version:
80 |
81 | a) under this License, provided that you make a good faith effort to
82 | ensure that, in the event an Application does not supply the
83 | function or data, the facility still operates, and performs
84 | whatever part of its purpose remains meaningful, or
85 |
86 | b) under the GNU GPL, with none of the additional permissions of
87 | this License applicable to that copy.
88 |
89 | 3. Object Code Incorporating Material from Library Header Files.
90 |
91 | The object code form of an Application may incorporate material from
92 | a header file that is part of the Library. You may convey such object
93 | code under terms of your choice, provided that, if the incorporated
94 | material is not limited to numerical parameters, data structure
95 | layouts and accessors, or small macros, inline functions and templates
96 | (ten or fewer lines in length), you do both of the following:
97 |
98 | a) Give prominent notice with each copy of the object code that the
99 | Library is used in it and that the Library and its use are
100 | covered by this License.
101 |
102 | b) Accompany the object code with a copy of the GNU GPL and this license
103 | document.
104 |
105 | 4. Combined Works.
106 |
107 | You may convey a Combined Work under terms of your choice that,
108 | taken together, effectively do not restrict modification of the
109 | portions of the Library contained in the Combined Work and reverse
110 | engineering for debugging such modifications, if you also do each of
111 | the following:
112 |
113 | a) Give prominent notice with each copy of the Combined Work that
114 | the Library is used in it and that the Library and its use are
115 | covered by this License.
116 |
117 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
118 | document.
119 |
120 | c) For a Combined Work that displays copyright notices during
121 | execution, include the copyright notice for the Library among
122 | these notices, as well as a reference directing the user to the
123 | copies of the GNU GPL and this license document.
124 |
125 | d) Do one of the following:
126 |
127 | 0) Convey the Minimal Corresponding Source under the terms of this
128 | License, and the Corresponding Application Code in a form
129 | suitable for, and under terms that permit, the user to
130 | recombine or relink the Application with a modified version of
131 | the Linked Version to produce a modified Combined Work, in the
132 | manner specified by section 6 of the GNU GPL for conveying
133 | Corresponding Source.
134 |
135 | 1) Use a suitable shared library mechanism for linking with the
136 | Library. A suitable mechanism is one that (a) uses at run time
137 | a copy of the Library already present on the user's computer
138 | system, and (b) will operate properly with a modified version
139 | of the Library that is interface-compatible with the Linked
140 | Version.
141 |
142 | e) Provide Installation Information, but only if you would otherwise
143 | be required to provide such information under section 6 of the
144 | GNU GPL, and only to the extent that such information is
145 | necessary to install and execute a modified version of the
146 | Combined Work produced by recombining or relinking the
147 | Application with a modified version of the Linked Version. (If
148 | you use option 4d0, the Installation Information must accompany
149 | the Minimal Corresponding Source and Corresponding Application
150 | Code. If you use option 4d1, you must provide the Installation
151 | Information in the manner specified by section 6 of the GNU GPL
152 | for conveying Corresponding Source.)
153 |
154 | 5. Combined Libraries.
155 |
156 | You may place library facilities that are a work based on the
157 | Library side by side in a single library together with other library
158 | facilities that are not Applications and are not covered by this
159 | License, and convey such a combined library under terms of your
160 | choice, if you do both of the following:
161 |
162 | a) Accompany the combined library with a copy of the same work based
163 | on the Library, uncombined with any other library facilities,
164 | conveyed under the terms of this License.
165 |
166 | b) Give prominent notice with the combined library that part of it
167 | is a work based on the Library, and explaining where to find the
168 | accompanying uncombined form of the same work.
169 |
170 | 6. Revised Versions of the GNU Lesser General Public License.
171 |
172 | The Free Software Foundation may publish revised and/or new versions
173 | of the GNU Lesser General Public License from time to time. Such new
174 | versions will be similar in spirit to the present version, but may
175 | differ in detail to address new problems or concerns.
176 |
177 | Each version is given a distinguishing version number. If the
178 | Library as you received it specifies that a certain numbered version
179 | of the GNU Lesser General Public License "or any later version"
180 | applies to it, you have the option of following the terms and
181 | conditions either of that published version or of any later version
182 | published by the Free Software Foundation. If the Library as you
183 | received it does not specify a version number of the GNU Lesser
184 | General Public License, you may choose any version of the GNU Lesser
185 | General Public License ever published by the Free Software Foundation.
186 |
187 | If the Library as you received it specifies that a proxy can decide
188 | whether future versions of the GNU Lesser General Public License shall
189 | apply, that proxy's public statement of acceptance of any version is
190 | permanent authorization for you to choose that version for the
191 | Library.
192 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | default: check
2 |
3 | check:
4 | go test
5 |
6 | docs:
7 | godoc2md github.com/juju/loggo > README.md
8 | sed -i 's|\[godoc-link-here\]|[](https://godoc.org/github.com/juju/loggo)|' README.md
9 |
10 |
11 | .PHONY: default check docs
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # loggo
3 | import "github.com/juju/loggo/v2"
4 |
5 | [](https://godoc.org/github.com/juju/loggo)
6 |
7 | ### Module level logging for Go
8 | This package provides an alternative to the standard library log package.
9 |
10 | The actual logging functions never return errors. If you are logging
11 | something, you really don't want to be worried about the logging
12 | having trouble.
13 |
14 | Modules have names that are defined by dotted strings.
15 |
16 |
17 | "first.second.third"
18 |
19 | There is a root module that has the name `""`. Each module
20 | (except the root module) has a parent, identified by the part of
21 | the name without the last dotted value.
22 | * the parent of "first.second.third" is "first.second"
23 | * the parent of "first.second" is "first"
24 | * the parent of "first" is "" (the root module)
25 |
26 | Each module can specify its own severity level. Logging calls that are of
27 | a lower severity than the module's effective severity level are not written
28 | out.
29 |
30 | Loggers are created through their Context. There is a default global context
31 | that is used if you just want simple use. Contexts are used where you may want
32 | different sets of writers for different loggers. Most use cases are fine with
33 | just using the default global context.
34 |
35 | Loggers are created using the GetLogger function.
36 |
37 |
38 | logger := loggo.GetLogger("foo.bar")
39 |
40 | The default global context has one writer registered, which will write to Stderr,
41 | and the root module, which will only emit warnings and above.
42 | If you want to continue using the default
43 | logger, but have it emit all logging levels you need to do the following.
44 |
45 |
46 | writer, err := loggo.RemoveWriter("default")
47 | // err is non-nil if and only if the name isn't found.
48 | loggo.RegisterWriter("default", writer)
49 |
50 | To make loggo produce colored output, you can do the following,
51 | having imported github.com/juju/loggo/loggocolor:
52 |
53 |
54 | loggo.ReplaceDefaultWriter(loggocolor.NewWriter(os.Stderr))
55 |
56 |
57 |
58 |
59 | ## Constants
60 | ``` go
61 | const DefaultWriterName = "default"
62 | ```
63 | DefaultWriterName is the name of the default writer for
64 | a Context.
65 |
66 |
67 | ## Variables
68 | ``` go
69 | var TimeFormat = initTimeFormat()
70 | ```
71 | TimeFormat is the time format used for the default writer.
72 | This can be set with the environment variable LOGGO_TIME_FORMAT.
73 |
74 |
75 | ## func ConfigureLoggers
76 | ``` go
77 | func ConfigureLoggers(specification string) error
78 | ```
79 | ConfigureLoggers configures loggers according to the given string
80 | specification, which specifies a set of modules and their associated
81 | logging levels. Loggers are colon- or semicolon-separated; each
82 | module is specified as =. White space outside of
83 | module names and levels is ignored. The root module is specified
84 | with the name "".
85 |
86 | An example specification:
87 |
88 |
89 | `=ERROR; foo.bar=WARNING`
90 |
91 |
92 | ## func DefaultFormatter
93 | ``` go
94 | func DefaultFormatter(entry Entry) string
95 | ```
96 | DefaultFormatter returns the parameters separated by spaces except for
97 | filename and line which are separated by a colon. The timestamp is shown
98 | to second resolution in UTC. For example:
99 |
100 |
101 | 2016-07-02 15:04:05
102 |
103 |
104 | ## func LoggerInfo
105 | ``` go
106 | func LoggerInfo() string
107 | ```
108 | LoggerInfo returns information about the configured loggers and their
109 | logging levels. The information is returned in the format expected by
110 | ConfigureLoggers. Loggers with UNSPECIFIED level will not
111 | be included.
112 |
113 |
114 | ## func RegisterWriter
115 | ``` go
116 | func RegisterWriter(name string, writer Writer) error
117 | ```
118 | RegisterWriter adds the writer to the list of writers in the DefaultContext
119 | that get notified when logging. If there is already a registered writer
120 | with that name, an error is returned.
121 |
122 |
123 | ## func ResetLogging
124 | ``` go
125 | func ResetLogging()
126 | ```
127 | ResetLogging iterates through the known modules and sets the levels of all
128 | to UNSPECIFIED, except for which is set to WARNING. The call also
129 | removes all writers in the DefaultContext and puts the original default
130 | writer back as the only writer.
131 |
132 |
133 | ## func ResetWriters
134 | ``` go
135 | func ResetWriters()
136 | ```
137 | ResetWriters puts the list of writers back into the initial state.
138 |
139 |
140 |
141 | ## type Config
142 | ``` go
143 | type Config map[string]Level
144 | ```
145 | Config is a mapping of logger module names to logging severity levels.
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | ### func ParseConfigString
156 | ``` go
157 | func ParseConfigString(specification string) (Config, error)
158 | ```
159 | ParseConfigString parses a logger configuration string into a map of logger
160 | names and their associated log level. This method is provided to allow
161 | other programs to pre-validate a configuration string rather than just
162 | calling ConfigureLoggers.
163 |
164 | Logging modules are colon- or semicolon-separated; each module is specified
165 | as =. White space outside of module names and levels is
166 | ignored. The root module is specified with the name "".
167 |
168 | As a special case, a log level may be specified on its own.
169 | This is equivalent to specifying the level of the root module,
170 | so "DEBUG" is equivalent to `=DEBUG`
171 |
172 | An example specification:
173 |
174 |
175 | `=ERROR; foo.bar=WARNING`
176 |
177 |
178 |
179 |
180 | ### func (Config) String
181 | ``` go
182 | func (c Config) String() string
183 | ```
184 | String returns a logger configuration string that may be parsed
185 | using ParseConfigurationString.
186 |
187 |
188 |
189 | ## type Context
190 | ``` go
191 | type Context struct {
192 | // contains filtered or unexported fields
193 | }
194 | ```
195 | Context produces loggers for a hierarchy of modules. The context holds
196 | a collection of hierarchical loggers and their writers.
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 | ### func DefaultContext
207 | ``` go
208 | func DefaultContext() *Context
209 | ```
210 | DefaultContext returns the global default logging context.
211 |
212 |
213 | ### func NewContext
214 | ``` go
215 | func NewContext(rootLevel Level) *Context
216 | ```
217 | NewLoggers returns a new Context with no writers set.
218 | If the root level is UNSPECIFIED, WARNING is used.
219 |
220 |
221 |
222 |
223 | ### func (\*Context) AddWriter
224 | ``` go
225 | func (c *Context) AddWriter(name string, writer Writer) error
226 | ```
227 | AddWriter adds a writer to the list to be called for each logging call.
228 | The name cannot be empty, and the writer cannot be nil. If an existing
229 | writer exists with the specified name, an error is returned.
230 |
231 |
232 |
233 | ### func (\*Context) ApplyConfig
234 | ``` go
235 | func (c *Context) ApplyConfig(config Config)
236 | ```
237 | ApplyConfig configures the logging modules according to the provided config.
238 |
239 |
240 |
241 | ### func (\*Context) CompleteConfig
242 | ``` go
243 | func (c *Context) CompleteConfig() Config
244 | ```
245 | CompleteConfig returns all the loggers and their defined levels,
246 | even if that level is UNSPECIFIED.
247 |
248 |
249 |
250 | ### func (\*Context) Config
251 | ``` go
252 | func (c *Context) Config() Config
253 | ```
254 | Config returns the current configuration of the Loggers. Loggers
255 | with UNSPECIFIED level will not be included.
256 |
257 |
258 |
259 | ### func (\*Context) GetLogger
260 | ``` go
261 | func (c *Context) GetLogger(name string) Logger
262 | ```
263 | GetLogger returns a Logger for the given module name, creating it and
264 | its parents if necessary.
265 |
266 |
267 |
268 | ### func (\*Context) RemoveWriter
269 | ``` go
270 | func (c *Context) RemoveWriter(name string) (Writer, error)
271 | ```
272 | RemoveWriter remotes the specified writer. If a writer is not found with
273 | the specified name an error is returned. The writer that was removed is also
274 | returned.
275 |
276 |
277 |
278 | ### func (\*Context) ReplaceWriter
279 | ``` go
280 | func (c *Context) ReplaceWriter(name string, writer Writer) (Writer, error)
281 | ```
282 | ReplaceWriter is a convenience method that does the equivalent of RemoveWriter
283 | followed by AddWriter with the same name. The replaced writer is returned.
284 |
285 |
286 |
287 | ### func (\*Context) ResetLoggerLevels
288 | ``` go
289 | func (c *Context) ResetLoggerLevels()
290 | ```
291 | ResetLoggerLevels iterates through the known logging modules and sets the
292 | levels of all to UNSPECIFIED, except for which is set to WARNING.
293 |
294 |
295 |
296 | ### func (\*Context) ResetWriters
297 | ``` go
298 | func (c *Context) ResetWriters()
299 | ```
300 | ResetWriters is generally only used in testing and removes all the writers.
301 |
302 |
303 |
304 | ### func (\*Context) Writer
305 | ``` go
306 | func (c *Context) Writer(name string) Writer
307 | ```
308 | Writer returns the named writer if one exists.
309 | If there is not a writer with the specified name, nil is returned.
310 |
311 |
312 |
313 | ## type Entry
314 | ``` go
315 | type Entry struct {
316 | // Level is the severity of the log message.
317 | Level Level
318 | // Module is the dotted module name from the logger.
319 | Module string
320 | // Filename is the full path the file that logged the message.
321 | Filename string
322 | // Line is the line number of the Filename.
323 | Line int
324 | // Timestamp is when the log message was created
325 | Timestamp time.Time
326 | // Message is the formatted string from teh log call.
327 | Message string
328 | }
329 | ```
330 | Entry represents a single log message.
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 | ## type Level
343 | ``` go
344 | type Level uint32
345 | ```
346 | Level holds a severity level.
347 |
348 |
349 |
350 | ``` go
351 | const (
352 | UNSPECIFIED Level = iota
353 | TRACE
354 | DEBUG
355 | INFO
356 | WARNING
357 | ERROR
358 | CRITICAL
359 | )
360 | ```
361 | The severity levels. Higher values are more considered more
362 | important.
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 | ### func ParseLevel
371 | ``` go
372 | func ParseLevel(level string) (Level, bool)
373 | ```
374 | ParseLevel converts a string representation of a logging level to a
375 | Level. It returns the level and whether it was valid or not.
376 |
377 |
378 |
379 |
380 | ### func (Level) Short
381 | ``` go
382 | func (level Level) Short() string
383 | ```
384 | Short returns a five character string to use in
385 | aligned logging output.
386 |
387 |
388 |
389 | ### func (Level) String
390 | ``` go
391 | func (level Level) String() string
392 | ```
393 | String implements Stringer.
394 |
395 |
396 |
397 | ## type Logger
398 | ``` go
399 | type Logger struct {
400 | // contains filtered or unexported fields
401 | }
402 | ```
403 | A Logger represents a logging module. It has an associated logging
404 | level which can be changed; messages of lesser severity will
405 | be dropped. Loggers have a hierarchical relationship - see
406 | the package documentation.
407 |
408 | The zero Logger value is usable - any messages logged
409 | to it will be sent to the root Logger.
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 | ### func GetLogger
420 | ``` go
421 | func GetLogger(name string) Logger
422 | ```
423 | GetLogger returns a Logger for the given module name,
424 | creating it and its parents if necessary.
425 |
426 |
427 |
428 |
429 | ### func (Logger) Child
430 | ``` go
431 | func (logger Logger) Child(name string) Logger
432 | ```
433 | Child returns the Logger whose module name is the composed of this
434 | Logger's name and the specified name.
435 |
436 |
437 |
438 | ### func (Logger) Criticalf
439 | ``` go
440 | func (logger Logger) Criticalf(message string, args ...interface{})
441 | ```
442 | Criticalf logs the printf-formatted message at critical level.
443 |
444 |
445 |
446 | ### func (Logger) Debugf
447 | ``` go
448 | func (logger Logger) Debugf(message string, args ...interface{})
449 | ```
450 | Debugf logs the printf-formatted message at debug level.
451 |
452 |
453 |
454 | ### func (Logger) EffectiveLogLevel
455 | ``` go
456 | func (logger Logger) EffectiveLogLevel() Level
457 | ```
458 | EffectiveLogLevel returns the effective min log level of
459 | the receiver - that is, messages with a lesser severity
460 | level will be discarded.
461 |
462 | If the log level of the receiver is unspecified,
463 | it will be taken from the effective log level of its
464 | parent.
465 |
466 |
467 |
468 | ### func (Logger) Errorf
469 | ``` go
470 | func (logger Logger) Errorf(message string, args ...interface{})
471 | ```
472 | Errorf logs the printf-formatted message at error level.
473 |
474 |
475 |
476 | ### func (Logger) Infof
477 | ``` go
478 | func (logger Logger) Infof(message string, args ...interface{})
479 | ```
480 | Infof logs the printf-formatted message at info level.
481 |
482 |
483 |
484 | ### func (Logger) IsDebugEnabled
485 | ``` go
486 | func (logger Logger) IsDebugEnabled() bool
487 | ```
488 | IsDebugEnabled returns whether debugging is enabled
489 | at debug level.
490 |
491 |
492 |
493 | ### func (Logger) IsErrorEnabled
494 | ``` go
495 | func (logger Logger) IsErrorEnabled() bool
496 | ```
497 | IsErrorEnabled returns whether debugging is enabled
498 | at error level.
499 |
500 |
501 |
502 | ### func (Logger) IsInfoEnabled
503 | ``` go
504 | func (logger Logger) IsInfoEnabled() bool
505 | ```
506 | IsInfoEnabled returns whether debugging is enabled
507 | at info level.
508 |
509 |
510 |
511 | ### func (Logger) IsLevelEnabled
512 | ``` go
513 | func (logger Logger) IsLevelEnabled(level Level) bool
514 | ```
515 | IsLevelEnabled returns whether debugging is enabled
516 | for the given log level.
517 |
518 |
519 |
520 | ### func (Logger) IsTraceEnabled
521 | ``` go
522 | func (logger Logger) IsTraceEnabled() bool
523 | ```
524 | IsTraceEnabled returns whether debugging is enabled
525 | at trace level.
526 |
527 |
528 |
529 | ### func (Logger) IsWarningEnabled
530 | ``` go
531 | func (logger Logger) IsWarningEnabled() bool
532 | ```
533 | IsWarningEnabled returns whether debugging is enabled
534 | at warning level.
535 |
536 |
537 |
538 | ### func (Logger) LogCallf
539 | ``` go
540 | func (logger Logger) LogCallf(calldepth int, level Level, message string, args ...interface{})
541 | ```
542 | LogCallf logs a printf-formatted message at the given level.
543 | The location of the call is indicated by the calldepth argument.
544 | A calldepth of 1 means the function that called this function.
545 | A message will be discarded if level is less than the
546 | the effective log level of the logger.
547 | Note that the writers may also filter out messages that
548 | are less than their registered minimum severity level.
549 |
550 |
551 |
552 | ### func (Logger) LogLevel
553 | ``` go
554 | func (logger Logger) LogLevel() Level
555 | ```
556 | LogLevel returns the configured min log level of the logger.
557 |
558 |
559 |
560 | ### func (Logger) Logf
561 | ``` go
562 | func (logger Logger) Logf(level Level, message string, args ...interface{})
563 | ```
564 | Logf logs a printf-formatted message at the given level.
565 | A message will be discarded if level is less than the
566 | the effective log level of the logger.
567 | Note that the writers may also filter out messages that
568 | are less than their registered minimum severity level.
569 |
570 |
571 |
572 | ### func (Logger) Name
573 | ``` go
574 | func (logger Logger) Name() string
575 | ```
576 | Name returns the logger's module name.
577 |
578 |
579 |
580 | ### func (Logger) Parent
581 | ``` go
582 | func (logger Logger) Parent() Logger
583 | ```
584 | Parent returns the Logger whose module name is the same
585 | as this logger without the last period and suffix.
586 | For example the parent of the logger that has the module
587 | "a.b.c" is "a.b".
588 | The Parent of the root logger is still the root logger.
589 |
590 |
591 |
592 | ### func (Logger) SetLogLevel
593 | ``` go
594 | func (logger Logger) SetLogLevel(level Level)
595 | ```
596 | SetLogLevel sets the severity level of the given logger.
597 | The root logger cannot be set to UNSPECIFIED level.
598 | See EffectiveLogLevel for how this affects the
599 | actual messages logged.
600 |
601 |
602 |
603 | ### func (Logger) Tracef
604 | ``` go
605 | func (logger Logger) Tracef(message string, args ...interface{})
606 | ```
607 | Tracef logs the printf-formatted message at trace level.
608 |
609 |
610 |
611 | ### func (Logger) Warningf
612 | ``` go
613 | func (logger Logger) Warningf(message string, args ...interface{})
614 | ```
615 | Warningf logs the printf-formatted message at warning level.
616 |
617 |
618 |
619 | ## type TestWriter
620 | ``` go
621 | type TestWriter struct {
622 | // contains filtered or unexported fields
623 | }
624 | ```
625 | TestWriter is a useful Writer for testing purposes. Each component of the
626 | logging message is stored in the Log array.
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 | ### func (\*TestWriter) Clear
639 | ``` go
640 | func (writer *TestWriter) Clear()
641 | ```
642 | Clear removes any saved log messages.
643 |
644 |
645 |
646 | ### func (\*TestWriter) Log
647 | ``` go
648 | func (writer *TestWriter) Log() []Entry
649 | ```
650 | Log returns a copy of the current logged values.
651 |
652 |
653 |
654 | ### func (\*TestWriter) Write
655 | ``` go
656 | func (writer *TestWriter) Write(entry Entry)
657 | ```
658 | Write saves the params as members in the TestLogValues struct appended to the Log array.
659 |
660 |
661 |
662 | ## type Writer
663 | ``` go
664 | type Writer interface {
665 | // Write writes a message to the Writer with the given level and module
666 | // name. The filename and line hold the file name and line number of the
667 | // code that is generating the log message; the time stamp holds the time
668 | // the log message was generated, and message holds the log message
669 | // itself.
670 | Write(entry Entry)
671 | }
672 | ```
673 | Writer is implemented by any recipient of log messages.
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 | ### func NewMinimumLevelWriter
684 | ``` go
685 | func NewMinimumLevelWriter(writer Writer, minLevel Level) Writer
686 | ```
687 | NewMinLevelWriter returns a Writer that will only pass on the Write calls
688 | to the provided writer if the log level is at or above the specified
689 | minimum level.
690 |
691 |
692 | ### func NewSimpleWriter
693 | ``` go
694 | func NewSimpleWriter(writer io.Writer, formatter func(entry Entry) string) Writer
695 | ```
696 | NewSimpleWriter returns a new writer that writes log messages to the given
697 | io.Writer formatting the messages with the given formatter.
698 |
699 |
700 | ### func RemoveWriter
701 | ``` go
702 | func RemoveWriter(name string) (Writer, error)
703 | ```
704 | RemoveWriter removes the Writer identified by 'name' and returns it.
705 | If the Writer is not found, an error is returned.
706 |
707 |
708 | ### func ReplaceDefaultWriter
709 | ``` go
710 | func ReplaceDefaultWriter(writer Writer) (Writer, error)
711 | ```
712 | ReplaceDefaultWriter is a convenience method that does the equivalent of
713 | RemoveWriter and then RegisterWriter with the name "default". The previous
714 | default writer, if any is returned.
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 | - - -
726 | Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)
727 |
--------------------------------------------------------------------------------
/benchmarks_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | "io/ioutil"
8 | "os"
9 |
10 | gc "gopkg.in/check.v1"
11 |
12 | "github.com/juju/loggo/v2"
13 | )
14 |
15 | type BenchmarksSuite struct {
16 | logger loggo.Logger
17 | writer *writer
18 | }
19 |
20 | var _ = gc.Suite(&BenchmarksSuite{})
21 |
22 | func (s *BenchmarksSuite) SetUpTest(c *gc.C) {
23 | loggo.ResetLogging()
24 | s.logger = loggo.GetLogger("test.writer")
25 | s.writer = &writer{}
26 | err := loggo.RegisterWriter("test", s.writer)
27 | c.Assert(err, gc.IsNil)
28 | }
29 |
30 | func (s *BenchmarksSuite) BenchmarkLoggingNoWriters(c *gc.C) {
31 | // No writers
32 | _, _ = loggo.RemoveWriter("test")
33 | for i := 0; i < c.N; i++ {
34 | s.logger.Warningf("just a simple warning for %d", i)
35 | }
36 | }
37 |
38 | func (s *BenchmarksSuite) BenchmarkLoggingNoWritersNoFormat(c *gc.C) {
39 | // No writers
40 | _, _ = loggo.RemoveWriter("test")
41 | for i := 0; i < c.N; i++ {
42 | s.logger.Warningf("just a simple warning")
43 | }
44 | }
45 |
46 | func (s *BenchmarksSuite) BenchmarkLoggingTestWriters(c *gc.C) {
47 | for i := 0; i < c.N; i++ {
48 | s.logger.Warningf("just a simple warning for %d", i)
49 | }
50 | c.Assert(s.writer.Log(), gc.HasLen, c.N)
51 | }
52 |
53 | func (s *BenchmarksSuite) BenchmarkLoggingDiskWriter(c *gc.C) {
54 | logFile := s.setupTempFileWriter(c)
55 | defer logFile.Close()
56 | msg := "just a simple warning for %d"
57 | for i := 0; i < c.N; i++ {
58 | s.logger.Warningf(msg, i)
59 | }
60 | offset, err := logFile.Seek(0, os.SEEK_CUR)
61 | c.Assert(err, gc.IsNil)
62 | c.Assert((offset > int64(len(msg))*int64(c.N)), gc.Equals, true,
63 | gc.Commentf("Not enough data was written to the log file."))
64 | }
65 |
66 | func (s *BenchmarksSuite) BenchmarkLoggingDiskWriterNoMessages(c *gc.C) {
67 | logFile := s.setupTempFileWriter(c)
68 | defer logFile.Close()
69 | // Change the log level
70 | writer, err := loggo.RemoveWriter("testfile")
71 | c.Assert(err, gc.IsNil)
72 |
73 | err = loggo.RegisterWriter("testfile", loggo.NewMinimumLevelWriter(writer, loggo.WARNING))
74 | c.Assert(err, gc.IsNil)
75 |
76 | msg := "just a simple warning for %d"
77 | for i := 0; i < c.N; i++ {
78 | s.logger.Debugf(msg, i)
79 | }
80 | offset, err := logFile.Seek(0, os.SEEK_CUR)
81 | c.Assert(err, gc.IsNil)
82 | c.Assert(offset, gc.Equals, int64(0),
83 | gc.Commentf("Data was written to the log file."))
84 | }
85 |
86 | func (s *BenchmarksSuite) BenchmarkLoggingDiskWriterNoMessagesLogLevel(c *gc.C) {
87 | logFile := s.setupTempFileWriter(c)
88 | defer logFile.Close()
89 | // Change the log level
90 | s.logger.SetLogLevel(loggo.WARNING)
91 |
92 | msg := "just a simple warning for %d"
93 | for i := 0; i < c.N; i++ {
94 | s.logger.Debugf(msg, i)
95 | }
96 | offset, err := logFile.Seek(0, os.SEEK_CUR)
97 | c.Assert(err, gc.IsNil)
98 | c.Assert(offset, gc.Equals, int64(0),
99 | gc.Commentf("Data was written to the log file."))
100 | }
101 |
102 | func (s *BenchmarksSuite) setupTempFileWriter(c *gc.C) *os.File {
103 | _, _ = loggo.RemoveWriter("test")
104 | logFile, err := ioutil.TempFile(c.MkDir(), "loggo-test")
105 | c.Assert(err, gc.IsNil)
106 |
107 | writer := loggo.NewSimpleWriter(logFile, loggo.DefaultFormatter)
108 | err = loggo.RegisterWriter("testfile", writer)
109 | c.Assert(err, gc.IsNil)
110 | return logFile
111 | }
112 |
--------------------------------------------------------------------------------
/checkers_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | "fmt"
8 | "time"
9 |
10 | gc "gopkg.in/check.v1"
11 | )
12 |
13 | func Between(start, end time.Time) gc.Checker {
14 | if end.Before(start) {
15 | return &betweenChecker{end, start}
16 | }
17 | return &betweenChecker{start, end}
18 | }
19 |
20 | type betweenChecker struct {
21 | start, end time.Time
22 | }
23 |
24 | func (checker *betweenChecker) Info() *gc.CheckerInfo {
25 | info := gc.CheckerInfo{
26 | Name: "Between",
27 | Params: []string{"obtained"},
28 | }
29 | return &info
30 | }
31 |
32 | func (checker *betweenChecker) Check(params []interface{}, names []string) (result bool, error string) {
33 | when, ok := params[0].(time.Time)
34 | if !ok {
35 | return false, "obtained value type must be time.Time"
36 | }
37 | if when.Before(checker.start) {
38 | return false, fmt.Sprintf("obtained time %q is before start time %q", when, checker.start)
39 | }
40 | if when.After(checker.end) {
41 | return false, fmt.Sprintf("obtained time %q is after end time %q", when, checker.end)
42 | }
43 | return true, ""
44 | }
45 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import (
7 | "fmt"
8 | "sort"
9 | "strings"
10 | )
11 |
12 | // Config is a mapping of logger module names to logging severity levels.
13 | type Config map[string]Level
14 |
15 | // String returns a logger configuration string that may be parsed
16 | // using ParseConfigurationString.
17 | func (c Config) String() string {
18 | if c == nil {
19 | return ""
20 | }
21 | // output in alphabetical order.
22 | var names []string
23 | for name := range c {
24 | names = append(names, name)
25 | }
26 | sort.Strings(names)
27 |
28 | var entries []string
29 | for _, name := range names {
30 | level := c[name]
31 | if name == "" {
32 | name = rootString
33 | }
34 | entry := fmt.Sprintf("%s=%s", name, level)
35 | entries = append(entries, entry)
36 | }
37 | return strings.Join(entries, ";")
38 | }
39 |
40 | func parseConfigValue(value string) (string, Level, error) {
41 | pair := strings.SplitN(value, "=", 2)
42 | if len(pair) < 2 {
43 | return "", UNSPECIFIED, fmt.Errorf("config value expected '=', found %q", value)
44 | }
45 | name := strings.TrimSpace(pair[0])
46 | if name == "" {
47 | return "", UNSPECIFIED, fmt.Errorf("config value %q has missing module name", value)
48 | }
49 |
50 | if tag := extractConfigTag(name); tag != "" {
51 | if strings.Contains(tag, ".") {
52 | // Show the original name and not text potentially extracted config
53 | // tag.
54 | return "", UNSPECIFIED, fmt.Errorf("config tag should not contain '.', found %q", name)
55 | }
56 | // Ensure once the normalised extraction has happened, we put the prefix
57 | // back on, so that we don't loose the fact that the config is a tag.
58 | //
59 | // Ideally we would change Config from map[string]Level to
60 | // map[string]ConfigEntry and then we wouldn't need this step, but that
61 | // causes lots of issues in Juju directly.
62 | name = fmt.Sprintf("#%s", tag)
63 | }
64 |
65 | levelStr := strings.TrimSpace(pair[1])
66 | level, ok := ParseLevel(levelStr)
67 | if !ok {
68 | return "", UNSPECIFIED, fmt.Errorf("unknown severity level %q", levelStr)
69 | }
70 | if name == rootString {
71 | name = ""
72 | }
73 | return name, level, nil
74 | }
75 |
76 | // ParseConfigString parses a logger configuration string into a map of logger
77 | // names and their associated log level. This method is provided to allow
78 | // other programs to pre-validate a configuration string rather than just
79 | // calling ConfigureLoggers.
80 | //
81 | // Logging modules are colon- or semicolon-separated; each module is specified
82 | // as =. White space outside of module names and levels is
83 | // ignored. The root module is specified with the name "".
84 | //
85 | // As a special case, a log level may be specified on its own.
86 | // This is equivalent to specifying the level of the root module,
87 | // so "DEBUG" is equivalent to `=DEBUG`
88 | //
89 | // An example specification:
90 | //
91 | // `=ERROR; foo.bar=WARNING`
92 | // `[TAG]=ERROR`
93 | func ParseConfigString(specification string) (Config, error) {
94 | specification = strings.TrimSpace(specification)
95 | if specification == "" {
96 | return nil, nil
97 | }
98 | cfg := make(Config)
99 | if level, ok := ParseLevel(specification); ok {
100 | cfg[""] = level
101 | return cfg, nil
102 | }
103 |
104 | values := strings.FieldsFunc(specification, func(r rune) bool { return r == ';' || r == ':' })
105 | for _, value := range values {
106 | name, level, err := parseConfigValue(value)
107 | if err != nil {
108 | return nil, err
109 | }
110 | cfg[name] = level
111 | }
112 | return cfg, nil
113 | }
114 |
115 | func extractConfigTag(s string) string {
116 | name := strings.TrimSpace(s)
117 | if len(s) < 2 {
118 | return ""
119 | }
120 | if name[0] == '#' {
121 | return strings.ToLower(name[1:])
122 | }
123 | return ""
124 | }
125 |
--------------------------------------------------------------------------------
/config_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import gc "gopkg.in/check.v1"
7 |
8 | type ConfigSuite struct{}
9 |
10 | var _ = gc.Suite(&ConfigSuite{})
11 |
12 | func (*ConfigSuite) TestParseConfigValue(c *gc.C) {
13 | for i, test := range []struct {
14 | value string
15 | module string
16 | level Level
17 | err string
18 | }{{
19 | err: `config value expected '=', found ""`,
20 | }, {
21 | value: "WARNING",
22 | err: `config value expected '=', found "WARNING"`,
23 | }, {
24 | value: "=WARNING",
25 | err: `config value "=WARNING" has missing module name`,
26 | }, {
27 | value: " name = WARNING ",
28 | module: "name",
29 | level: WARNING,
30 | }, {
31 | value: "name = foo",
32 | err: `unknown severity level "foo"`,
33 | }, {
34 | value: "name=DEBUG=INFO",
35 | err: `unknown severity level "DEBUG=INFO"`,
36 | }, {
37 | value: " = info",
38 | module: "",
39 | level: INFO,
40 | }, {
41 | value: "#tag = info",
42 | module: "#tag",
43 | level: INFO,
44 | }, {
45 | value: "#TAG = info",
46 | module: "#tag",
47 | level: INFO,
48 | }, {
49 | value: "#tag.1 = info",
50 | err: `config tag should not contain '.', found "#tag.1"`,
51 | }} {
52 | c.Logf("%d: %s", i, test.value)
53 | module, level, err := parseConfigValue(test.value)
54 | if test.err == "" {
55 | c.Check(err, gc.IsNil)
56 | c.Check(module, gc.Equals, test.module)
57 | c.Check(level, gc.Equals, test.level)
58 | } else {
59 | c.Check(module, gc.Equals, "")
60 | c.Check(level, gc.Equals, UNSPECIFIED)
61 | c.Check(err.Error(), gc.Equals, test.err)
62 | }
63 | }
64 | }
65 |
66 | func (*ConfigSuite) TestPaarseConfigurationString(c *gc.C) {
67 | for i, test := range []struct {
68 | configuration string
69 | expected Config
70 | err string
71 | }{{
72 | configuration: "",
73 | // nil Config, no error
74 | }, {
75 | configuration: "INFO",
76 | expected: Config{"": INFO},
77 | }, {
78 | configuration: "=INFO",
79 | err: `config value "=INFO" has missing module name`,
80 | }, {
81 | configuration: "=UNSPECIFIED",
82 | expected: Config{"": UNSPECIFIED},
83 | }, {
84 | configuration: "=DEBUG",
85 | expected: Config{"": DEBUG},
86 | }, {
87 | configuration: "#label=DEBUG",
88 | expected: Config{"#label": DEBUG},
89 | }, {
90 | configuration: "test.module=debug",
91 | expected: Config{"test.module": DEBUG},
92 | }, {
93 | configuration: "module=info; sub.module=debug; other.module=warning",
94 | expected: Config{
95 | "module": INFO,
96 | "sub.module": DEBUG,
97 | "other.module": WARNING,
98 | },
99 | }, {
100 | // colons not semicolons
101 | configuration: "module=info: sub.module=debug: other.module=warning",
102 | expected: Config{
103 | "module": INFO,
104 | "sub.module": DEBUG,
105 | "other.module": WARNING,
106 | },
107 | }, {
108 | configuration: " foo.bar \t\r\n= \t\r\nCRITICAL \t\r\n; \t\r\nfoo \r\t\n = DEBUG",
109 | expected: Config{
110 | "foo": DEBUG,
111 | "foo.bar": CRITICAL,
112 | },
113 | }, {
114 | configuration: "foo;bar",
115 | err: `config value expected '=', found "foo"`,
116 | }, {
117 | configuration: "foo=",
118 | err: `unknown severity level ""`,
119 | }, {
120 | configuration: "foo=unknown",
121 | err: `unknown severity level "unknown"`,
122 | }} {
123 | c.Logf("%d: %q", i, test.configuration)
124 | config, err := ParseConfigString(test.configuration)
125 | if test.err == "" {
126 | c.Check(err, gc.IsNil)
127 | c.Check(config, gc.DeepEquals, test.expected)
128 | } else {
129 | c.Check(config, gc.IsNil)
130 | c.Check(err.Error(), gc.Equals, test.err)
131 | }
132 | }
133 | }
134 |
135 | func (*ConfigSuite) TestConfigString(c *gc.C) {
136 | for i, test := range []struct {
137 | config Config
138 | expected string
139 | }{{
140 | config: nil,
141 | expected: "",
142 | }, {
143 | config: Config{"": INFO},
144 | expected: "=INFO",
145 | }, {
146 | config: Config{"": UNSPECIFIED},
147 | expected: "=UNSPECIFIED",
148 | }, {
149 | config: Config{"": DEBUG},
150 | expected: "=DEBUG",
151 | }, {
152 | config: Config{"test.module": DEBUG},
153 | expected: "test.module=DEBUG",
154 | }, {
155 | config: Config{
156 | "": WARNING,
157 | "module": INFO,
158 | "sub.module": DEBUG,
159 | "other.module": WARNING,
160 | },
161 | expected: "=WARNING;module=INFO;other.module=WARNING;sub.module=DEBUG",
162 | }} {
163 | c.Logf("%d: %q", i, test.expected)
164 | c.Check(test.config.String(), gc.Equals, test.expected)
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import (
7 | "fmt"
8 | "sort"
9 | "strings"
10 | "sync"
11 | )
12 |
13 | // Context produces loggers for a hierarchy of modules. The context holds
14 | // a collection of hierarchical loggers and their writers.
15 | type Context struct {
16 | root *module
17 |
18 | // Perhaps have one mutex?
19 | // All `modules` variables are managed by the one mutex.
20 | modulesMutex sync.Mutex
21 | modules map[string]*module
22 | modulesTagConfig map[string]Level
23 |
24 | writersMutex sync.Mutex
25 | writers map[string]Writer
26 |
27 | // writeMuxtex is used to serialise write operations.
28 | writeMutex sync.Mutex
29 | }
30 |
31 | // NewContext returns a new Context with no writers set.
32 | // If the root level is UNSPECIFIED, WARNING is used.
33 | func NewContext(rootLevel Level) *Context {
34 | if rootLevel < TRACE || rootLevel > CRITICAL {
35 | rootLevel = WARNING
36 | }
37 | context := &Context{
38 | modules: make(map[string]*module),
39 | modulesTagConfig: make(map[string]Level),
40 | writers: make(map[string]Writer),
41 | }
42 | context.root = &module{
43 | level: rootLevel,
44 | context: context,
45 | }
46 | context.root.parent = context.root
47 | context.modules[""] = context.root
48 | return context
49 | }
50 |
51 | // GetLogger returns a Logger for the given module name, creating it and
52 | // its parents if necessary.
53 | func (c *Context) GetLogger(name string, tags ...string) Logger {
54 | name = strings.TrimSpace(strings.ToLower(name))
55 |
56 | c.modulesMutex.Lock()
57 | defer c.modulesMutex.Unlock()
58 |
59 | return Logger{
60 | impl: c.getLoggerModule(name, tags),
61 | callDepth: defaultCallDepth,
62 | }
63 | }
64 |
65 | // GetAllLoggerTags returns all the logger tags for a given context. The
66 | // names are unique and sorted before returned, to improve consistency.
67 | func (c *Context) GetAllLoggerTags() []string {
68 | c.modulesMutex.Lock()
69 | defer c.modulesMutex.Unlock()
70 |
71 | names := make(map[string]struct{})
72 | for _, module := range c.modules {
73 | for k, v := range module.tagsLookup {
74 | names[k] = v
75 | }
76 | }
77 | labels := make([]string, 0, len(names))
78 | for name := range names {
79 | labels = append(labels, name)
80 | }
81 | sort.Strings(labels)
82 | return labels
83 | }
84 |
85 | func (c *Context) getLoggerModule(name string, tags []string) *module {
86 | if name == rootString {
87 | name = ""
88 | }
89 | impl, found := c.modules[name]
90 | if found {
91 | return impl
92 | }
93 | var parentName string
94 | if i := strings.LastIndex(name, "."); i >= 0 {
95 | parentName = name[0:i]
96 | }
97 | // Labels don't apply to the parent, otherwise would have all labels.
98 | // Selection of the tag would give you all loggers again, which isn't what
99 | // you want.
100 | parent := c.getLoggerModule(parentName, nil)
101 |
102 | // Ensure that we create a new logger module for the name, that includes the
103 | // tag.
104 | level := UNSPECIFIED
105 | labelMap := make(map[string]struct{})
106 | for _, tag := range tags {
107 | labelMap[tag] = struct{}{}
108 |
109 | // First tag wins when setting the logger tag from the config tag
110 | // level cache. If there are no tag configs, then fallback to
111 | // UNSPECIFIED and inherit the level correctly.
112 | if configLevel, ok := c.modulesTagConfig[tag]; ok && level == UNSPECIFIED {
113 | level = configLevel
114 | }
115 | }
116 |
117 | // As it's not possible to modify the parent's labels, it's safe to copy
118 | // them at the time of creation. Otherwise we have to walk the parent chain
119 | // to get the full set of labels for every log message.
120 | labels := make(Labels)
121 | for k, v := range parent.labels {
122 | labels[k] = v
123 | }
124 |
125 | impl = &module{
126 | name: name,
127 | level: level,
128 | parent: parent,
129 | context: c,
130 | tags: tags,
131 | tagsLookup: labelMap,
132 | labels: parent.labels,
133 | }
134 | c.modules[name] = impl
135 | return impl
136 | }
137 |
138 | // getLoggerModulesByTag returns modules that have the associated tag.
139 | func (c *Context) getLoggerModulesByTag(label string) []*module {
140 | var modules []*module
141 | for _, mod := range c.modules {
142 | if len(mod.tags) == 0 {
143 | continue
144 | }
145 |
146 | if _, ok := mod.tagsLookup[label]; ok {
147 | modules = append(modules, mod)
148 | }
149 | }
150 | return modules
151 | }
152 |
153 | // Config returns the current configuration of the Loggers. Loggers
154 | // with UNSPECIFIED level will not be included.
155 | func (c *Context) Config() Config {
156 | result := make(Config)
157 | c.modulesMutex.Lock()
158 | defer c.modulesMutex.Unlock()
159 |
160 | for name, module := range c.modules {
161 | if module.level != UNSPECIFIED {
162 | result[name] = module.level
163 | }
164 | }
165 | return result
166 | }
167 |
168 | // CompleteConfig returns all the loggers and their defined levels,
169 | // even if that level is UNSPECIFIED.
170 | func (c *Context) CompleteConfig() Config {
171 | result := make(Config)
172 | c.modulesMutex.Lock()
173 | defer c.modulesMutex.Unlock()
174 |
175 | for name, module := range c.modules {
176 | result[name] = module.level
177 | }
178 | return result
179 | }
180 |
181 | // ApplyConfig configures the logging modules according to the provided config.
182 | func (c *Context) ApplyConfig(config Config) {
183 | c.modulesMutex.Lock()
184 | defer c.modulesMutex.Unlock()
185 | for name, level := range config {
186 | tag := extractConfigTag(name)
187 | if tag == "" {
188 | module := c.getLoggerModule(name, nil)
189 | module.setLevel(level)
190 | continue
191 | }
192 |
193 | // Ensure that we save the config for lazy loggers to pick up correctly.
194 | c.modulesTagConfig[tag] = level
195 |
196 | // Config contains a named tag, use that for selecting the loggers.
197 | modules := c.getLoggerModulesByTag(tag)
198 | for _, module := range modules {
199 | module.setLevel(level)
200 | }
201 | }
202 | }
203 |
204 | // ResetLoggerLevels iterates through the known logging modules and sets the
205 | // levels of all to UNSPECIFIED, except for which is set to WARNING.
206 | func (c *Context) ResetLoggerLevels() {
207 | c.modulesMutex.Lock()
208 | defer c.modulesMutex.Unlock()
209 | // Setting the root module to UNSPECIFIED will set it to WARNING.
210 | for _, module := range c.modules {
211 | module.setLevel(UNSPECIFIED)
212 | }
213 | // We can safely just wipe everything here.
214 | c.modulesTagConfig = make(map[string]Level)
215 | }
216 |
217 | func (c *Context) write(entry Entry) {
218 | c.writeMutex.Lock()
219 | defer c.writeMutex.Unlock()
220 | for _, writer := range c.getWriters() {
221 | writer.Write(entry)
222 | }
223 | }
224 |
225 | func (c *Context) getWriters() []Writer {
226 | c.writersMutex.Lock()
227 | defer c.writersMutex.Unlock()
228 | var result []Writer
229 | for _, writer := range c.writers {
230 | result = append(result, writer)
231 | }
232 | return result
233 | }
234 |
235 | // AddWriter adds a writer to the list to be called for each logging call.
236 | // The name cannot be empty, and the writer cannot be nil. If an existing
237 | // writer exists with the specified name, an error is returned.
238 | func (c *Context) AddWriter(name string, writer Writer) error {
239 | if name == "" {
240 | return fmt.Errorf("name cannot be empty")
241 | }
242 | if writer == nil {
243 | return fmt.Errorf("writer cannot be nil")
244 | }
245 | c.writersMutex.Lock()
246 | defer c.writersMutex.Unlock()
247 | if _, found := c.writers[name]; found {
248 | return fmt.Errorf("context already has a writer named %q", name)
249 | }
250 | c.writers[name] = writer
251 | return nil
252 | }
253 |
254 | // Writer returns the named writer if one exists.
255 | // If there is not a writer with the specified name, nil is returned.
256 | func (c *Context) Writer(name string) Writer {
257 | c.writersMutex.Lock()
258 | defer c.writersMutex.Unlock()
259 | return c.writers[name]
260 | }
261 |
262 | // RemoveWriter remotes the specified writer. If a writer is not found with
263 | // the specified name an error is returned. The writer that was removed is also
264 | // returned.
265 | func (c *Context) RemoveWriter(name string) (Writer, error) {
266 | c.writersMutex.Lock()
267 | defer c.writersMutex.Unlock()
268 | reg, found := c.writers[name]
269 | if !found {
270 | return nil, fmt.Errorf("context has no writer named %q", name)
271 | }
272 | delete(c.writers, name)
273 | return reg, nil
274 | }
275 |
276 | // ReplaceWriter is a convenience method that does the equivalent of RemoveWriter
277 | // followed by AddWriter with the same name. The replaced writer is returned.
278 | func (c *Context) ReplaceWriter(name string, writer Writer) (Writer, error) {
279 | if name == "" {
280 | return nil, fmt.Errorf("name cannot be empty")
281 | }
282 | if writer == nil {
283 | return nil, fmt.Errorf("writer cannot be nil")
284 | }
285 | c.writersMutex.Lock()
286 | defer c.writersMutex.Unlock()
287 | reg, found := c.writers[name]
288 | if !found {
289 | return nil, fmt.Errorf("context has no writer named %q", name)
290 | }
291 | oldWriter := reg
292 | c.writers[name] = writer
293 | return oldWriter, nil
294 | }
295 |
296 | // ResetWriters is generally only used in testing and removes all the writers.
297 | func (c *Context) ResetWriters() {
298 | c.writersMutex.Lock()
299 | defer c.writersMutex.Unlock()
300 | c.writers = make(map[string]Writer)
301 | }
302 |
303 | // ConfigureLoggers configures loggers according to the given string
304 | // specification, which specifies a set of modules and their associated
305 | // logging levels. Loggers are colon- or semicolon-separated; each
306 | // module is specified as =. White space outside of
307 | // module names and levels is ignored. The root module is specified
308 | // with the name "".
309 | //
310 | // An example specification:
311 | //
312 | // `=ERROR; foo.bar=WARNING`
313 | func (c *Context) ConfigureLoggers(specification string) error {
314 | config, err := ParseConfigString(specification)
315 | if err != nil {
316 | return err
317 | }
318 | c.ApplyConfig(config)
319 | return nil
320 | }
321 |
--------------------------------------------------------------------------------
/context_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | "github.com/juju/loggo/v2"
8 |
9 | gc "gopkg.in/check.v1"
10 | )
11 |
12 | type ContextSuite struct{}
13 |
14 | var _ = gc.Suite(&ContextSuite{})
15 |
16 | func (*ContextSuite) TestNewContextRootLevel(c *gc.C) {
17 | for i, test := range []struct {
18 | level loggo.Level
19 | expected loggo.Level
20 | }{{
21 | level: loggo.UNSPECIFIED,
22 | expected: loggo.WARNING,
23 | }, {
24 | level: loggo.DEBUG,
25 | expected: loggo.DEBUG,
26 | }, {
27 | level: loggo.INFO,
28 | expected: loggo.INFO,
29 | }, {
30 | level: loggo.WARNING,
31 | expected: loggo.WARNING,
32 | }, {
33 | level: loggo.ERROR,
34 | expected: loggo.ERROR,
35 | }, {
36 | level: loggo.CRITICAL,
37 | expected: loggo.CRITICAL,
38 | }, {
39 | level: loggo.Level(42),
40 | expected: loggo.WARNING,
41 | }} {
42 | c.Logf("%d: %s", i, test.level)
43 | context := loggo.NewContext(test.level)
44 | cfg := context.Config()
45 | c.Check(cfg, gc.HasLen, 1)
46 | value, found := cfg[""]
47 | c.Check(found, gc.Equals, true)
48 | c.Check(value, gc.Equals, test.expected)
49 | }
50 | }
51 |
52 | func logAllSeverities(logger loggo.Logger) {
53 | logger.Criticalf("something critical")
54 | logger.Errorf("an error")
55 | logger.Warningf("a warning message")
56 | logger.Infof("an info message")
57 | logger.Debugf("a debug message")
58 | logger.Tracef("a trace message")
59 | }
60 |
61 | func checkLogEntry(c *gc.C, entry, expected loggo.Entry) {
62 | c.Check(entry.Level, gc.Equals, expected.Level)
63 | c.Check(entry.Module, gc.Equals, expected.Module)
64 | c.Check(entry.Message, gc.Equals, expected.Message)
65 | }
66 |
67 | func checkLogEntries(c *gc.C, obtained, expected []loggo.Entry) {
68 | if c.Check(len(obtained), gc.Equals, len(expected)) {
69 | for i := range obtained {
70 | checkLogEntry(c, obtained[i], expected[i])
71 | }
72 | }
73 | }
74 |
75 | func (*ContextSuite) TestGetLoggerRoot(c *gc.C) {
76 | context := loggo.NewContext(loggo.DEBUG)
77 | blank := context.GetLogger("")
78 | root := context.GetLogger("")
79 | c.Assert(blank, gc.DeepEquals, root)
80 | }
81 |
82 | func (*ContextSuite) TestGetLoggerCase(c *gc.C) {
83 | context := loggo.NewContext(loggo.DEBUG)
84 | upper := context.GetLogger("TEST")
85 | lower := context.GetLogger("test")
86 | c.Assert(upper, gc.DeepEquals, lower)
87 | c.Assert(upper.Name(), gc.Equals, "test")
88 | }
89 |
90 | func (*ContextSuite) TestGetLoggerSpace(c *gc.C) {
91 | context := loggo.NewContext(loggo.DEBUG)
92 | space := context.GetLogger(" test ")
93 | lower := context.GetLogger("test")
94 | c.Assert(space, gc.DeepEquals, lower)
95 | c.Assert(space.Name(), gc.Equals, "test")
96 | }
97 |
98 | func (*ContextSuite) TestNewContextNoWriter(c *gc.C) {
99 | // Should be no output.
100 | context := loggo.NewContext(loggo.DEBUG)
101 | logger := context.GetLogger("test")
102 | logAllSeverities(logger)
103 | }
104 |
105 | func (*ContextSuite) newContextWithTestWriter(c *gc.C, level loggo.Level) (*loggo.Context, *loggo.TestWriter) {
106 | writer := &loggo.TestWriter{}
107 | context := loggo.NewContext(level)
108 | err := context.AddWriter("test", writer)
109 | c.Assert(err, gc.IsNil)
110 | return context, writer
111 | }
112 |
113 | func (s *ContextSuite) TestNewContextRootSeverityWarning(c *gc.C) {
114 | context, writer := s.newContextWithTestWriter(c, loggo.WARNING)
115 | logger := context.GetLogger("test")
116 | logAllSeverities(logger)
117 | checkLogEntries(c, writer.Log(), []loggo.Entry{
118 | {Level: loggo.CRITICAL, Module: "test", Message: "something critical"},
119 | {Level: loggo.ERROR, Module: "test", Message: "an error"},
120 | {Level: loggo.WARNING, Module: "test", Message: "a warning message"},
121 | })
122 | }
123 |
124 | func (s *ContextSuite) TestNewContextRootSeverityTrace(c *gc.C) {
125 | context, writer := s.newContextWithTestWriter(c, loggo.TRACE)
126 | logger := context.GetLogger("test")
127 | logAllSeverities(logger)
128 | checkLogEntries(c, writer.Log(), []loggo.Entry{
129 | {Level: loggo.CRITICAL, Module: "test", Message: "something critical"},
130 | {Level: loggo.ERROR, Module: "test", Message: "an error"},
131 | {Level: loggo.WARNING, Module: "test", Message: "a warning message"},
132 | {Level: loggo.INFO, Module: "test", Message: "an info message"},
133 | {Level: loggo.DEBUG, Module: "test", Message: "a debug message"},
134 | {Level: loggo.TRACE, Module: "test", Message: "a trace message"},
135 | })
136 | }
137 |
138 | func (*ContextSuite) TestNewContextConfig(c *gc.C) {
139 | context := loggo.NewContext(loggo.DEBUG)
140 | config := context.Config()
141 | c.Assert(config, gc.DeepEquals, loggo.Config{"": loggo.DEBUG})
142 | }
143 |
144 | func (*ContextSuite) TestNewLoggerAddsConfig(c *gc.C) {
145 | context := loggo.NewContext(loggo.DEBUG)
146 | _ = context.GetLogger("test.module")
147 | c.Assert(context.Config(), gc.DeepEquals, loggo.Config{
148 | "": loggo.DEBUG,
149 | })
150 | c.Assert(context.CompleteConfig(), gc.DeepEquals, loggo.Config{
151 | "": loggo.DEBUG,
152 | "test": loggo.UNSPECIFIED,
153 | "test.module": loggo.UNSPECIFIED,
154 | })
155 | }
156 |
157 | func (*ContextSuite) TestConfigureLoggers(c *gc.C) {
158 | context := loggo.NewContext(loggo.INFO)
159 | err := context.ConfigureLoggers("testing.module=debug")
160 | c.Assert(err, gc.IsNil)
161 | expected := "=INFO;testing.module=DEBUG"
162 | c.Assert(context.Config().String(), gc.Equals, expected)
163 | }
164 |
165 | func (*ContextSuite) TestApplyNilConfig(c *gc.C) {
166 | context := loggo.NewContext(loggo.DEBUG)
167 | context.ApplyConfig(nil)
168 | c.Assert(context.Config(), gc.DeepEquals, loggo.Config{"": loggo.DEBUG})
169 | }
170 |
171 | func (*ContextSuite) TestApplyConfigRootUnspecified(c *gc.C) {
172 | context := loggo.NewContext(loggo.DEBUG)
173 | context.ApplyConfig(loggo.Config{"": loggo.UNSPECIFIED})
174 | c.Assert(context.Config(), gc.DeepEquals, loggo.Config{"": loggo.WARNING})
175 | }
176 |
177 | func (*ContextSuite) TestApplyConfigRootTrace(c *gc.C) {
178 | context := loggo.NewContext(loggo.WARNING)
179 | context.ApplyConfig(loggo.Config{"": loggo.TRACE})
180 | c.Assert(context.Config(), gc.DeepEquals, loggo.Config{"": loggo.TRACE})
181 | }
182 |
183 | func (*ContextSuite) TestApplyConfigCreatesModules(c *gc.C) {
184 | context := loggo.NewContext(loggo.WARNING)
185 | context.ApplyConfig(loggo.Config{"first.second": loggo.TRACE})
186 | c.Assert(context.Config(), gc.DeepEquals,
187 | loggo.Config{
188 | "": loggo.WARNING,
189 | "first.second": loggo.TRACE,
190 | })
191 | c.Assert(context.CompleteConfig(), gc.DeepEquals,
192 | loggo.Config{
193 | "": loggo.WARNING,
194 | "first": loggo.UNSPECIFIED,
195 | "first.second": loggo.TRACE,
196 | })
197 | }
198 |
199 | func (*ContextSuite) TestApplyConfigAdditive(c *gc.C) {
200 | context := loggo.NewContext(loggo.WARNING)
201 | context.ApplyConfig(loggo.Config{"first.second": loggo.TRACE})
202 | context.ApplyConfig(loggo.Config{"other.module": loggo.DEBUG})
203 | c.Assert(context.Config(), gc.DeepEquals,
204 | loggo.Config{
205 | "": loggo.WARNING,
206 | "first.second": loggo.TRACE,
207 | "other.module": loggo.DEBUG,
208 | })
209 | c.Assert(context.CompleteConfig(), gc.DeepEquals,
210 | loggo.Config{
211 | "": loggo.WARNING,
212 | "first": loggo.UNSPECIFIED,
213 | "first.second": loggo.TRACE,
214 | "other": loggo.UNSPECIFIED,
215 | "other.module": loggo.DEBUG,
216 | })
217 | }
218 |
219 | func (*ContextSuite) TestGetAllLoggerTags(c *gc.C) {
220 | context := loggo.NewContext(loggo.WARNING)
221 | context.GetLogger("a.b", "one")
222 | context.GetLogger("c.d", "one")
223 | context.GetLogger("e", "two")
224 |
225 | labels := context.GetAllLoggerTags()
226 | c.Assert(labels, gc.DeepEquals, []string{"one", "two"})
227 | }
228 |
229 | func (*ContextSuite) TestGetAllLoggerTagsWithApplyConfig(c *gc.C) {
230 | context := loggo.NewContext(loggo.WARNING)
231 | context.ApplyConfig(loggo.Config{"#one": loggo.TRACE})
232 |
233 | labels := context.GetAllLoggerTags()
234 | c.Assert(labels, gc.DeepEquals, []string{})
235 | }
236 |
237 | func (*ContextSuite) TestApplyConfigTags(c *gc.C) {
238 | context := loggo.NewContext(loggo.WARNING)
239 | context.GetLogger("a.b", "one")
240 | context.GetLogger("c.d", "one")
241 | context.GetLogger("e", "two")
242 |
243 | context.ApplyConfig(loggo.Config{"#one": loggo.TRACE})
244 | context.ApplyConfig(loggo.Config{"#two": loggo.DEBUG})
245 |
246 | c.Assert(context.Config(), gc.DeepEquals,
247 | loggo.Config{
248 | "": loggo.WARNING,
249 | "a.b": loggo.TRACE,
250 | "c.d": loggo.TRACE,
251 | "e": loggo.DEBUG,
252 | })
253 | c.Assert(context.CompleteConfig(), gc.DeepEquals,
254 | loggo.Config{
255 | "": loggo.WARNING,
256 | "a": loggo.UNSPECIFIED,
257 | "a.b": loggo.TRACE,
258 | "c": loggo.UNSPECIFIED,
259 | "c.d": loggo.TRACE,
260 | "e": loggo.DEBUG,
261 | })
262 | }
263 |
264 | func (*ContextSuite) TestApplyConfigLabelsAppliesToNewLoggers(c *gc.C) {
265 | context := loggo.NewContext(loggo.WARNING)
266 |
267 | context.ApplyConfig(loggo.Config{"#one": loggo.TRACE})
268 | context.ApplyConfig(loggo.Config{"#two": loggo.DEBUG})
269 |
270 | context.GetLogger("a.b", "one")
271 | context.GetLogger("c.d", "one")
272 | context.GetLogger("e", "two")
273 |
274 | c.Assert(context.Config(), gc.DeepEquals,
275 | loggo.Config{
276 | "": loggo.WARNING,
277 | "a.b": loggo.TRACE,
278 | "c.d": loggo.TRACE,
279 | "e": loggo.DEBUG,
280 | })
281 | c.Assert(context.CompleteConfig(), gc.DeepEquals,
282 | loggo.Config{
283 | "": loggo.WARNING,
284 | "a": loggo.UNSPECIFIED,
285 | "a.b": loggo.TRACE,
286 | "c": loggo.UNSPECIFIED,
287 | "c.d": loggo.TRACE,
288 | "e": loggo.DEBUG,
289 | })
290 | }
291 |
292 | func (*ContextSuite) TestApplyConfigLabelsAppliesToNewLoggersWithMultipleTags(c *gc.C) {
293 | context := loggo.NewContext(loggo.WARNING)
294 |
295 | // Invert the order here, to ensure that the config order doesn't matter,
296 | // but the way the tags are ordered in `GetLogger`.
297 | context.ApplyConfig(loggo.Config{"#two": loggo.DEBUG})
298 | context.ApplyConfig(loggo.Config{"#one": loggo.TRACE})
299 |
300 | context.GetLogger("a.b", "one", "two")
301 |
302 | c.Assert(context.Config(), gc.DeepEquals,
303 | loggo.Config{
304 | "": loggo.WARNING,
305 | "a.b": loggo.TRACE,
306 | })
307 | c.Assert(context.CompleteConfig(), gc.DeepEquals,
308 | loggo.Config{
309 | "": loggo.WARNING,
310 | "a": loggo.UNSPECIFIED,
311 | "a.b": loggo.TRACE,
312 | })
313 | }
314 |
315 | func (*ContextSuite) TestApplyConfigLabelsResetLoggerLevels(c *gc.C) {
316 | context := loggo.NewContext(loggo.WARNING)
317 |
318 | context.ApplyConfig(loggo.Config{"#one": loggo.TRACE})
319 | context.ApplyConfig(loggo.Config{"#two": loggo.DEBUG})
320 |
321 | context.GetLogger("a.b", "one")
322 | context.GetLogger("c.d", "one")
323 | context.GetLogger("e", "two")
324 |
325 | context.ResetLoggerLevels()
326 |
327 | c.Assert(context.Config(), gc.DeepEquals,
328 | loggo.Config{
329 | "": loggo.WARNING,
330 | })
331 | c.Assert(context.CompleteConfig(), gc.DeepEquals,
332 | loggo.Config{
333 | "": loggo.WARNING,
334 | "a": loggo.UNSPECIFIED,
335 | "a.b": loggo.UNSPECIFIED,
336 | "c": loggo.UNSPECIFIED,
337 | "c.d": loggo.UNSPECIFIED,
338 | "e": loggo.UNSPECIFIED,
339 | })
340 | }
341 |
342 | func (*ContextSuite) TestApplyConfigTagsAddative(c *gc.C) {
343 | context := loggo.NewContext(loggo.WARNING)
344 | context.ApplyConfig(loggo.Config{"#one": loggo.TRACE})
345 | context.ApplyConfig(loggo.Config{"#two": loggo.DEBUG})
346 | c.Assert(context.Config(), gc.DeepEquals,
347 | loggo.Config{
348 | "": loggo.WARNING,
349 | })
350 | c.Assert(context.CompleteConfig(), gc.DeepEquals,
351 | loggo.Config{
352 | "": loggo.WARNING,
353 | })
354 | }
355 |
356 | func (*ContextSuite) TestApplyConfigWithMalformedTag(c *gc.C) {
357 | context := loggo.NewContext(loggo.WARNING)
358 | context.GetLogger("a.b", "one")
359 |
360 | context.ApplyConfig(loggo.Config{"#ONE.1": loggo.TRACE})
361 |
362 | c.Assert(context.Config(), gc.DeepEquals,
363 | loggo.Config{
364 | "": loggo.WARNING,
365 | })
366 | c.Assert(context.CompleteConfig(), gc.DeepEquals,
367 | loggo.Config{
368 | "": loggo.WARNING,
369 | "a": loggo.UNSPECIFIED,
370 | "a.b": loggo.UNSPECIFIED,
371 | })
372 | }
373 |
374 | func (*ContextSuite) TestResetLoggerTags(c *gc.C) {
375 | context := loggo.NewContext(loggo.DEBUG)
376 | context.ApplyConfig(loggo.Config{"first.second": loggo.TRACE})
377 | context.ResetLoggerLevels()
378 | c.Assert(context.Config(), gc.DeepEquals,
379 | loggo.Config{
380 | "": loggo.WARNING,
381 | })
382 | c.Assert(context.CompleteConfig(), gc.DeepEquals,
383 | loggo.Config{
384 | "": loggo.WARNING,
385 | "first": loggo.UNSPECIFIED,
386 | "first.second": loggo.UNSPECIFIED,
387 | })
388 | }
389 |
390 | func (*ContextSuite) TestWriterNamesNone(c *gc.C) {
391 | context := loggo.NewContext(loggo.DEBUG)
392 | writers := context.WriterNames()
393 | c.Assert(writers, gc.HasLen, 0)
394 | }
395 |
396 | func (*ContextSuite) TestAddWriterNoName(c *gc.C) {
397 | context := loggo.NewContext(loggo.DEBUG)
398 | err := context.AddWriter("", nil)
399 | c.Assert(err.Error(), gc.Equals, "name cannot be empty")
400 | }
401 |
402 | func (*ContextSuite) TestAddWriterNil(c *gc.C) {
403 | context := loggo.NewContext(loggo.DEBUG)
404 | err := context.AddWriter("foo", nil)
405 | c.Assert(err.Error(), gc.Equals, "writer cannot be nil")
406 | }
407 |
408 | func (*ContextSuite) TestNamedAddWriter(c *gc.C) {
409 | context := loggo.NewContext(loggo.DEBUG)
410 | err := context.AddWriter("foo", &writer{name: "foo"})
411 | c.Assert(err, gc.IsNil)
412 | err = context.AddWriter("foo", &writer{name: "foo"})
413 | c.Assert(err.Error(), gc.Equals, `context already has a writer named "foo"`)
414 |
415 | writers := context.WriterNames()
416 | c.Assert(writers, gc.DeepEquals, []string{"foo"})
417 | }
418 |
419 | func (*ContextSuite) TestRemoveWriter(c *gc.C) {
420 | context := loggo.NewContext(loggo.DEBUG)
421 | w, err := context.RemoveWriter("unknown")
422 | c.Assert(err.Error(), gc.Equals, `context has no writer named "unknown"`)
423 | c.Assert(w, gc.IsNil)
424 | }
425 |
426 | func (*ContextSuite) TestRemoveWriterFound(c *gc.C) {
427 | context := loggo.NewContext(loggo.DEBUG)
428 | original := &writer{name: "foo"}
429 | err := context.AddWriter("foo", original)
430 | c.Assert(err, gc.IsNil)
431 | existing, err := context.RemoveWriter("foo")
432 | c.Assert(err, gc.IsNil)
433 | c.Assert(existing, gc.Equals, original)
434 |
435 | writers := context.WriterNames()
436 | c.Assert(writers, gc.HasLen, 0)
437 | }
438 |
439 | func (*ContextSuite) TestReplaceWriterNoName(c *gc.C) {
440 | context := loggo.NewContext(loggo.DEBUG)
441 | existing, err := context.ReplaceWriter("", nil)
442 | c.Assert(err.Error(), gc.Equals, "name cannot be empty")
443 | c.Assert(existing, gc.IsNil)
444 | }
445 |
446 | func (*ContextSuite) TestReplaceWriterNil(c *gc.C) {
447 | context := loggo.NewContext(loggo.DEBUG)
448 | existing, err := context.ReplaceWriter("foo", nil)
449 | c.Assert(err.Error(), gc.Equals, "writer cannot be nil")
450 | c.Assert(existing, gc.IsNil)
451 | }
452 |
453 | func (*ContextSuite) TestReplaceWriterNotFound(c *gc.C) {
454 | context := loggo.NewContext(loggo.DEBUG)
455 | existing, err := context.ReplaceWriter("foo", &writer{})
456 | c.Assert(err.Error(), gc.Equals, `context has no writer named "foo"`)
457 | c.Assert(existing, gc.IsNil)
458 | }
459 |
460 | func (*ContextSuite) TestMultipleWriters(c *gc.C) {
461 | first := &writer{}
462 | second := &writer{}
463 | third := &writer{}
464 | context := loggo.NewContext(loggo.TRACE)
465 | err := context.AddWriter("first", first)
466 | c.Assert(err, gc.IsNil)
467 | err = context.AddWriter("second", second)
468 | c.Assert(err, gc.IsNil)
469 | err = context.AddWriter("third", third)
470 | c.Assert(err, gc.IsNil)
471 |
472 | logger := context.GetLogger("test")
473 | logAllSeverities(logger)
474 |
475 | expected := []loggo.Entry{
476 | {Level: loggo.CRITICAL, Module: "test", Message: "something critical"},
477 | {Level: loggo.ERROR, Module: "test", Message: "an error"},
478 | {Level: loggo.WARNING, Module: "test", Message: "a warning message"},
479 | {Level: loggo.INFO, Module: "test", Message: "an info message"},
480 | {Level: loggo.DEBUG, Module: "test", Message: "a debug message"},
481 | {Level: loggo.TRACE, Module: "test", Message: "a trace message"},
482 | }
483 |
484 | checkLogEntries(c, first.Log(), expected)
485 | checkLogEntries(c, second.Log(), expected)
486 | checkLogEntries(c, third.Log(), expected)
487 | }
488 |
489 | func (*ContextSuite) TestWriter(c *gc.C) {
490 | first := &writer{name: "first"}
491 | second := &writer{name: "second"}
492 | context := loggo.NewContext(loggo.TRACE)
493 | err := context.AddWriter("first", first)
494 | c.Assert(err, gc.IsNil)
495 | err = context.AddWriter("second", second)
496 | c.Assert(err, gc.IsNil)
497 |
498 | c.Check(context.Writer("unknown"), gc.IsNil)
499 | c.Check(context.Writer("first"), gc.Equals, first)
500 | c.Check(context.Writer("second"), gc.Equals, second)
501 |
502 | c.Check(first, gc.Not(gc.Equals), second)
503 | }
504 |
505 | type writer struct {
506 | loggo.TestWriter
507 | // The name exists to discriminate writer equality.
508 | name string
509 | }
510 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | /*
5 | [godoc-link-here]
6 |
7 | Module level logging for Go
8 |
9 | This package provides an alternative to the standard library log package.
10 |
11 | The actual logging functions never return errors. If you are logging
12 | something, you really don't want to be worried about the logging
13 | having trouble.
14 |
15 | Modules have names that are defined by dotted strings.
16 | "first.second.third"
17 |
18 | There is a root module that has the name `""`. Each module
19 | (except the root module) has a parent, identified by the part of
20 | the name without the last dotted value.
21 | * the parent of "first.second.third" is "first.second"
22 | * the parent of "first.second" is "first"
23 | * the parent of "first" is "" (the root module)
24 |
25 | Each module can specify its own severity level. Logging calls that are of
26 | a lower severity than the module's effective severity level are not written
27 | out.
28 |
29 | Loggers are created through their Context. There is a default global context
30 | that is used if you just want simple use. Contexts are used where you may want
31 | different sets of writers for different loggers. Most use cases are fine with
32 | just using the default global context.
33 |
34 | Loggers are created using the GetLogger function.
35 | logger := loggo.GetLogger("foo.bar")
36 |
37 | The default global context has one writer registered, which will write to Stderr,
38 | and the root module, which will only emit warnings and above.
39 | If you want to continue using the default
40 | logger, but have it emit all logging levels you need to do the following.
41 |
42 | writer, err := loggo.RemoveWriter("default")
43 | // err is non-nil if and only if the name isn't found.
44 | loggo.RegisterWriter("default", writer)
45 |
46 | To make loggo produce colored output, you can do the following,
47 | having imported github.com/juju/loggo/loggocolor:
48 |
49 | loggo.ReplaceDefaultWriter(loggocolor.NewWriter(os.Stderr))
50 | */
51 | package loggo
52 |
--------------------------------------------------------------------------------
/entry.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import "time"
7 |
8 | // Entry represents a single log message.
9 | type Entry struct {
10 | // Level is the severity of the log message.
11 | Level Level
12 | // Module is the dotted module name from the logger.
13 | Module string
14 | // Filename is the full path the file that logged the message.
15 | Filename string
16 | // Line is the line number of the Filename.
17 | Line int
18 | // Timestamp is when the log message was created
19 | Timestamp time.Time
20 | // Message is the formatted string from teh log call.
21 | Message string
22 | // Labels are the labels associated with the log message.
23 | Labels Labels
24 | }
25 |
--------------------------------------------------------------------------------
/example/first.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/juju/loggo/v2"
5 | )
6 |
7 | var first = loggo.GetLogger("first")
8 |
9 | func FirstCritical(message string) {
10 | first.Criticalf(message)
11 | }
12 |
13 | func FirstError(message string) {
14 | first.Errorf(message)
15 | }
16 |
17 | func FirstWarning(message string) {
18 | first.Warningf(message)
19 | }
20 |
21 | func FirstInfo(message string) {
22 | first.Infof(message)
23 | }
24 |
25 | func FirstDebug(message string) {
26 | first.Debugf(message)
27 | }
28 |
29 | func FirstTrace(message string) {
30 | first.Tracef(message)
31 | }
32 |
--------------------------------------------------------------------------------
/example/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 |
8 | "github.com/juju/loggo/v2"
9 | )
10 |
11 | var rootLogger = loggo.GetLogger("")
12 |
13 | func main() {
14 | args := os.Args
15 | if len(args) > 1 {
16 | if err := loggo.ConfigureLoggers(args[1]); err != nil {
17 | log.Fatal(err)
18 | }
19 | } else {
20 | fmt.Println("Add a parameter to configure the logging:")
21 | fmt.Println("E.g. \"=INFO;first=TRACE\"")
22 | }
23 | fmt.Println("\nCurrent logging levels:")
24 | fmt.Println(loggo.LoggerInfo())
25 | fmt.Println("")
26 |
27 | rootLogger.Infof("Start of test.")
28 |
29 | FirstCritical("first critical")
30 | FirstError("first error")
31 | FirstWarning("first warning")
32 | FirstInfo("first info")
33 | FirstDebug("first debug")
34 | FirstTrace("first trace")
35 |
36 | SecondCritical("second critical")
37 | SecondError("second error")
38 | SecondWarning("second warning")
39 | SecondInfo("second info")
40 | SecondDebug("second debug")
41 | SecondTrace("second trace")
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/example/second.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/juju/loggo/v2"
5 | )
6 |
7 | var second = loggo.GetLogger("second")
8 |
9 | func SecondCritical(message string) {
10 | second.Criticalf(message)
11 | }
12 |
13 | func SecondError(message string) {
14 | second.Errorf(message)
15 | }
16 |
17 | func SecondWarning(message string) {
18 | second.Warningf(message)
19 | }
20 |
21 | func SecondInfo(message string) {
22 | second.Infof(message)
23 | }
24 |
25 | func SecondDebug(message string) {
26 | second.Debugf(message)
27 | }
28 | func SecondTrace(message string) {
29 | second.Tracef(message)
30 | }
31 |
--------------------------------------------------------------------------------
/export_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | // WriterNames returns the names of the context's writers for testing purposes.
7 | func (c *Context) WriterNames() []string {
8 | c.writersMutex.Lock()
9 | defer c.writersMutex.Unlock()
10 | var result []string
11 | for name := range c.writers {
12 | result = append(result, name)
13 | }
14 | return result
15 | }
16 |
17 | func ResetDefaultContext() {
18 | ResetLogging()
19 | _ = DefaultContext().AddWriter(DefaultWriterName, defaultWriter())
20 | }
21 |
--------------------------------------------------------------------------------
/formatter.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import (
7 | "fmt"
8 | "os"
9 | "path/filepath"
10 | "time"
11 | )
12 |
13 | // DefaultFormatter returns the parameters separated by spaces except for
14 | // filename and line which are separated by a colon. The timestamp is shown
15 | // to second resolution in UTC. For example:
16 | // 2016-07-02 15:04:05
17 | func DefaultFormatter(entry Entry) string {
18 | ts := entry.Timestamp.In(time.UTC).Format("2006-01-02 15:04:05")
19 | // Just get the basename from the filename
20 | filename := filepath.Base(entry.Filename)
21 | return fmt.Sprintf("%s %s %s %s:%d %s", ts, entry.Level, entry.Module, filename, entry.Line, entry.Message)
22 | }
23 |
24 | // TimeFormat is the time format used for the default writer.
25 | // This can be set with the environment variable LOGGO_TIME_FORMAT.
26 | var TimeFormat = initTimeFormat()
27 |
28 | func initTimeFormat() string {
29 | format := os.Getenv("LOGGO_TIME_FORMAT")
30 | if format != "" {
31 | return format
32 | }
33 | return "15:04:05"
34 | }
35 |
--------------------------------------------------------------------------------
/formatter_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | "time"
8 |
9 | gc "gopkg.in/check.v1"
10 |
11 | "github.com/juju/loggo/v2"
12 | )
13 |
14 | type formatterSuite struct{}
15 |
16 | var _ = gc.Suite(&formatterSuite{})
17 |
18 | func (*formatterSuite) TestDefaultFormat(c *gc.C) {
19 | location, err := time.LoadLocation("UTC")
20 | testTime := time.Date(2013, 5, 3, 10, 53, 24, 123456, location)
21 | c.Assert(err, gc.IsNil)
22 | entry := loggo.Entry{
23 | Level: loggo.WARNING,
24 | Module: "test.module",
25 | Filename: "some/deep/filename",
26 | Line: 42,
27 | Timestamp: testTime,
28 | Message: "hello world!",
29 | }
30 | formatted := loggo.DefaultFormatter(entry)
31 | c.Assert(formatted, gc.Equals, "2013-05-03 10:53:24 WARNING test.module filename:42 hello world!")
32 | }
33 |
--------------------------------------------------------------------------------
/global.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | var (
7 | defaultContext = newDefaultContxt()
8 | )
9 |
10 | func newDefaultContxt() *Context {
11 | ctx := NewContext(WARNING)
12 | if err := ctx.AddWriter(DefaultWriterName, defaultWriter()); err != nil {
13 | panic(err)
14 | }
15 | return ctx
16 | }
17 |
18 | // DefaultContext returns the global default logging context.
19 | func DefaultContext() *Context {
20 | return defaultContext
21 | }
22 |
23 | // LoggerInfo returns information about the configured loggers and their
24 | // logging levels. The information is returned in the format expected by
25 | // ConfigureLoggers. Loggers with UNSPECIFIED level will not
26 | // be included.
27 | func LoggerInfo() string {
28 | return defaultContext.Config().String()
29 | }
30 |
31 | // GetLogger returns a Logger for the given module name,
32 | // creating it and its parents if necessary.
33 | func GetLogger(name string) Logger {
34 | return defaultContext.GetLogger(name)
35 | }
36 |
37 | // GetLoggerWithTags returns a Logger for the given module name with the correct
38 | // associated tags, creating it and its parents if necessary.
39 | func GetLoggerWithTags(name string, tags ...string) Logger {
40 | return defaultContext.GetLogger(name, tags...)
41 | }
42 |
43 | // ResetLogging iterates through the known modules and sets the levels of all
44 | // to UNSPECIFIED, except for which is set to WARNING. The call also
45 | // removes all writers in the DefaultContext and puts the original default
46 | // writer back as the only writer.
47 | func ResetLogging() {
48 | defaultContext.ResetLoggerLevels()
49 | defaultContext.ResetWriters()
50 | }
51 |
52 | // ResetWriters puts the list of writers back into the initial state.
53 | func ResetWriters() {
54 | defaultContext.ResetWriters()
55 | }
56 |
57 | // ReplaceDefaultWriter is a convenience method that does the equivalent of
58 | // RemoveWriter and then RegisterWriter with the name "default". The previous
59 | // default writer, if any is returned.
60 | func ReplaceDefaultWriter(writer Writer) (Writer, error) {
61 | return defaultContext.ReplaceWriter(DefaultWriterName, writer)
62 | }
63 |
64 | // RegisterWriter adds the writer to the list of writers in the DefaultContext
65 | // that get notified when logging. If there is already a registered writer
66 | // with that name, an error is returned.
67 | func RegisterWriter(name string, writer Writer) error {
68 | return defaultContext.AddWriter(name, writer)
69 | }
70 |
71 | // RemoveWriter removes the Writer identified by 'name' and returns it.
72 | // If the Writer is not found, an error is returned.
73 | func RemoveWriter(name string) (Writer, error) {
74 | return defaultContext.RemoveWriter(name)
75 | }
76 |
77 | // ConfigureLoggers configures loggers on the default context according to the
78 | // given string specification, which specifies a set of modules and their
79 | // associated logging levels. Loggers are colon- or semicolon-separated; each
80 | // module is specified as =. White space outside of module
81 | // names and levels is ignored. The root module is specified with the name
82 | // "".
83 | //
84 | // An example specification:
85 | //
86 | // `=ERROR; foo.bar=WARNING`
87 | func ConfigureLoggers(specification string) error {
88 | return defaultContext.ConfigureLoggers(specification)
89 | }
90 |
--------------------------------------------------------------------------------
/global_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | gc "gopkg.in/check.v1"
8 |
9 | "github.com/juju/loggo/v2"
10 | )
11 |
12 | type GlobalSuite struct{}
13 |
14 | var _ = gc.Suite(&GlobalSuite{})
15 |
16 | func (*GlobalSuite) SetUpTest(c *gc.C) {
17 | loggo.ResetDefaultContext()
18 | }
19 |
20 | func (*GlobalSuite) TestRootLogger(c *gc.C) {
21 | var root loggo.Logger
22 |
23 | got := loggo.GetLogger("")
24 |
25 | c.Check(got.Name(), gc.Equals, root.Name())
26 | c.Check(got.LogLevel(), gc.Equals, root.LogLevel())
27 | }
28 |
29 | func (*GlobalSuite) TestModuleName(c *gc.C) {
30 | logger := loggo.GetLogger("loggo.testing")
31 | c.Check(logger.Name(), gc.Equals, "loggo.testing")
32 | }
33 |
34 | func (*GlobalSuite) TestLevel(c *gc.C) {
35 | logger := loggo.GetLogger("testing")
36 | level := logger.LogLevel()
37 | c.Check(level, gc.Equals, loggo.UNSPECIFIED)
38 | }
39 |
40 | func (*GlobalSuite) TestEffectiveLevel(c *gc.C) {
41 | logger := loggo.GetLogger("testing")
42 | level := logger.EffectiveLogLevel()
43 | c.Check(level, gc.Equals, loggo.WARNING)
44 | }
45 |
46 | func (*GlobalSuite) TestLevelsSharedForSameModule(c *gc.C) {
47 | logger1 := loggo.GetLogger("testing.module")
48 | logger2 := loggo.GetLogger("testing.module")
49 |
50 | logger1.SetLogLevel(loggo.INFO)
51 | c.Assert(logger1.IsInfoEnabled(), gc.Equals, true)
52 | c.Assert(logger2.IsInfoEnabled(), gc.Equals, true)
53 | }
54 |
55 | func (*GlobalSuite) TestModuleLowered(c *gc.C) {
56 | logger1 := loggo.GetLogger("TESTING.MODULE")
57 | logger2 := loggo.GetLogger("Testing")
58 |
59 | c.Assert(logger1.Name(), gc.Equals, "testing.module")
60 | c.Assert(logger2.Name(), gc.Equals, "testing")
61 | }
62 |
63 | func (s *GlobalSuite) TestConfigureLoggers(c *gc.C) {
64 | err := loggo.ConfigureLoggers("testing.module=debug")
65 | c.Assert(err, gc.IsNil)
66 | expected := "=WARNING;testing.module=DEBUG"
67 | c.Assert(loggo.DefaultContext().Config().String(), gc.Equals, expected)
68 | c.Assert(loggo.LoggerInfo(), gc.Equals, expected)
69 | }
70 |
71 | func (*GlobalSuite) TestRegisterWriterExistingName(c *gc.C) {
72 | err := loggo.RegisterWriter("default", &writer{})
73 | c.Assert(err, gc.ErrorMatches, `context already has a writer named "default"`)
74 | }
75 |
76 | func (*GlobalSuite) TestReplaceDefaultWriter(c *gc.C) {
77 | oldWriter, err := loggo.ReplaceDefaultWriter(&writer{})
78 | c.Assert(oldWriter, gc.NotNil)
79 | c.Assert(err, gc.IsNil)
80 | c.Assert(loggo.DefaultContext().WriterNames(), gc.DeepEquals, []string{"default"})
81 | }
82 |
83 | func (*GlobalSuite) TestRemoveWriter(c *gc.C) {
84 | oldWriter, err := loggo.RemoveWriter("default")
85 | c.Assert(oldWriter, gc.NotNil)
86 | c.Assert(err, gc.IsNil)
87 | c.Assert(loggo.DefaultContext().WriterNames(), gc.HasLen, 0)
88 | }
89 |
90 | func (s *GlobalSuite) TestGetLoggerWithTags(c *gc.C) {
91 | logger := loggo.GetLoggerWithTags("parent", "labela", "labelb")
92 | c.Check(logger.Tags(), gc.DeepEquals, []string{"labela", "labelb"})
93 | }
94 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/juju/loggo/v2
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a
7 | gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2
8 | )
9 |
10 | require (
11 | github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6 // indirect
12 | github.com/mattn/go-colorable v0.0.6 // indirect
13 | github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c // indirect
14 | golang.org/x/sys v0.16.0 // indirect
15 | )
16 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
2 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
3 | github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6 h1:yjdywwaxd8vTEXuA4EdgUBkiCQEQG7YAY3k9S1PaZKg=
4 | github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
5 | github.com/mattn/go-colorable v0.0.6 h1:jGqlOoCjqVR4hfTO9H1qrR2xi0xZNYmX2T1xlw7P79c=
6 | github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
7 | github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c h1:3nKFouDdpgGUV/uerJcYWH45ZbJzX0SiVWfTgmUeTzc=
8 | github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
9 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
10 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
11 | gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2 h1:+j1SppRob9bAgoYmsdW9NNBdKZfgYuWpqnYHv78Qt8w=
12 | gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
13 |
--------------------------------------------------------------------------------
/helper.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import (
7 | "runtime"
8 | "sync"
9 | )
10 |
11 | var (
12 | helperMutex sync.RWMutex
13 | helpers map[uintptr]struct{}
14 | )
15 |
16 | // Helper passed 1 marks the caller as a helper function and will skip it when
17 | // capturing the callsite location.
18 | func Helper(skip int) {
19 | helper(skip + 1)
20 | }
21 |
22 | func helper(skip int) {
23 | callers := [1]uintptr{}
24 | if runtime.Callers(skip+2, callers[:]) == 0 {
25 | panic("failed to get caller information")
26 | }
27 | pc := callers[0]
28 | helperMutex.RLock()
29 | if _, ok := helpers[pc]; ok {
30 | helperMutex.RUnlock()
31 | return
32 | }
33 | helperMutex.RUnlock()
34 | helperMutex.Lock()
35 | defer helperMutex.Unlock()
36 | if helpers == nil {
37 | helpers = make(map[uintptr]struct{})
38 | }
39 | helpers[pc] = struct{}{}
40 | }
41 |
42 | // caller behaves like runtime.Caller but skips functions marked by helper.
43 | func caller(skip int) (uintptr, string, int, bool) {
44 | pc := [8]uintptr{}
45 | n := runtime.Callers(skip+2, pc[:])
46 | if n == 0 {
47 | return 0, "", 0, false
48 | }
49 | helperMutex.RLock()
50 | pcs := pc[:]
51 | for i := n - 1; i >= 0; i-- {
52 | if _, ok := helpers[pc[i]]; ok {
53 | pcs = pc[i:]
54 | break
55 | }
56 | }
57 | helperMutex.RUnlock()
58 | frames := runtime.CallersFrames(pcs)
59 | if frame, ok := frames.Next(); ok {
60 | return frame.PC, frame.File, frame.Line, true
61 | }
62 | return 0, "", 0, false
63 | }
64 |
--------------------------------------------------------------------------------
/labels.go:
--------------------------------------------------------------------------------
1 | // Copyright 2024 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | const (
7 | // LoggerTags is the name of the label used to record the
8 | // logger tags for a log entry.
9 | LoggerTags = "logger-tags"
10 | )
11 |
12 | // Labels represents key values which are assigned to a log entry.
13 | type Labels map[string]string
14 |
--------------------------------------------------------------------------------
/level.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import (
7 | "strings"
8 | "sync/atomic"
9 | )
10 |
11 | // The severity levels. Higher values are more considered more
12 | // important.
13 | const (
14 | UNSPECIFIED Level = iota
15 | TRACE
16 | DEBUG
17 | INFO
18 | WARNING
19 | ERROR
20 | CRITICAL
21 | )
22 |
23 | // Level holds a severity level.
24 | type Level uint32
25 |
26 | // ParseLevel converts a string representation of a logging level to a
27 | // Level. It returns the level and whether it was valid or not.
28 | func ParseLevel(level string) (Level, bool) {
29 | level = strings.ToUpper(level)
30 | switch level {
31 | case "UNSPECIFIED":
32 | return UNSPECIFIED, true
33 | case "TRACE":
34 | return TRACE, true
35 | case "DEBUG":
36 | return DEBUG, true
37 | case "INFO":
38 | return INFO, true
39 | case "WARN", "WARNING":
40 | return WARNING, true
41 | case "ERROR":
42 | return ERROR, true
43 | case "CRITICAL":
44 | return CRITICAL, true
45 | default:
46 | return UNSPECIFIED, false
47 | }
48 | }
49 |
50 | // String implements Stringer.
51 | func (level Level) String() string {
52 | switch level {
53 | case UNSPECIFIED:
54 | return "UNSPECIFIED"
55 | case TRACE:
56 | return "TRACE"
57 | case DEBUG:
58 | return "DEBUG"
59 | case INFO:
60 | return "INFO"
61 | case WARNING:
62 | return "WARNING"
63 | case ERROR:
64 | return "ERROR"
65 | case CRITICAL:
66 | return "CRITICAL"
67 | default:
68 | return ""
69 | }
70 | }
71 |
72 | // Short returns a five character string to use in
73 | // aligned logging output.
74 | func (level Level) Short() string {
75 | switch level {
76 | case TRACE:
77 | return "TRACE"
78 | case DEBUG:
79 | return "DEBUG"
80 | case INFO:
81 | return "INFO "
82 | case WARNING:
83 | return "WARN "
84 | case ERROR:
85 | return "ERROR"
86 | case CRITICAL:
87 | return "CRITC"
88 | default:
89 | return " "
90 | }
91 | }
92 |
93 | // get atomically gets the value of the given level.
94 | func (level *Level) get() Level {
95 | return Level(atomic.LoadUint32((*uint32)(level)))
96 | }
97 |
98 | // set atomically sets the value of the receiver
99 | // to the given level.
100 | func (level *Level) set(newLevel Level) {
101 | atomic.StoreUint32((*uint32)(level), uint32(newLevel))
102 | }
103 |
--------------------------------------------------------------------------------
/level_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | gc "gopkg.in/check.v1"
8 |
9 | "github.com/juju/loggo/v2"
10 | )
11 |
12 | type LevelSuite struct{}
13 |
14 | var _ = gc.Suite(&LevelSuite{})
15 |
16 | var parseLevelTests = []struct {
17 | str string
18 | level loggo.Level
19 | fail bool
20 | }{{
21 | str: "trace",
22 | level: loggo.TRACE,
23 | }, {
24 | str: "TrAce",
25 | level: loggo.TRACE,
26 | }, {
27 | str: "TRACE",
28 | level: loggo.TRACE,
29 | }, {
30 | str: "debug",
31 | level: loggo.DEBUG,
32 | }, {
33 | str: "DEBUG",
34 | level: loggo.DEBUG,
35 | }, {
36 | str: "info",
37 | level: loggo.INFO,
38 | }, {
39 | str: "INFO",
40 | level: loggo.INFO,
41 | }, {
42 | str: "warn",
43 | level: loggo.WARNING,
44 | }, {
45 | str: "WARN",
46 | level: loggo.WARNING,
47 | }, {
48 | str: "warning",
49 | level: loggo.WARNING,
50 | }, {
51 | str: "WARNING",
52 | level: loggo.WARNING,
53 | }, {
54 | str: "error",
55 | level: loggo.ERROR,
56 | }, {
57 | str: "ERROR",
58 | level: loggo.ERROR,
59 | }, {
60 | str: "critical",
61 | level: loggo.CRITICAL,
62 | }, {
63 | str: "not_specified",
64 | fail: true,
65 | }, {
66 | str: "other",
67 | fail: true,
68 | }, {
69 | str: "",
70 | fail: true,
71 | }}
72 |
73 | func (s *LevelSuite) TestParseLevel(c *gc.C) {
74 | for _, test := range parseLevelTests {
75 | level, ok := loggo.ParseLevel(test.str)
76 | c.Assert(level, gc.Equals, test.level)
77 | c.Assert(ok, gc.Equals, !test.fail)
78 | }
79 | }
80 |
81 | var levelStringValueTests = map[loggo.Level]string{
82 | loggo.UNSPECIFIED: "UNSPECIFIED",
83 | loggo.DEBUG: "DEBUG",
84 | loggo.TRACE: "TRACE",
85 | loggo.INFO: "INFO",
86 | loggo.WARNING: "WARNING",
87 | loggo.ERROR: "ERROR",
88 | loggo.CRITICAL: "CRITICAL",
89 | loggo.Level(42): "", // other values are unknown
90 | }
91 |
92 | func (s *LevelSuite) TestLevelStringValue(c *gc.C) {
93 | for level, str := range levelStringValueTests {
94 | c.Assert(level.String(), gc.Equals, str)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import (
7 | "fmt"
8 | "strings"
9 | "time"
10 | )
11 |
12 | const (
13 | // defaultCallDepth is the default number of stack frames to ascend to
14 | // find the caller.
15 | defaultCallDepth = 2
16 | )
17 |
18 | // A Logger represents a logging module. It has an associated logging
19 | // level which can be changed; messages of lesser severity will
20 | // be dropped. Loggers have a hierarchical relationship - see
21 | // the package documentation.
22 | //
23 | // The zero Logger value is usable - any messages logged
24 | // to it will be sent to the root Logger.
25 | type Logger struct {
26 | impl *module
27 | labels Labels
28 |
29 | // CallDepth is the number of stack frames to ascend to find the caller.
30 | callDepth int
31 | }
32 |
33 | // WithLabels returns a logger whose module is the same as this logger and
34 | // the returned logger will add the specified labels to each log entry.
35 | // WithLabels only target a specific logger with labels. Children of the logger
36 | // will not inherit the labels.
37 | // To add labels to all child loggers, use ChildWithLabels.
38 | func (logger Logger) WithLabels(labels Labels) Logger {
39 | if len(labels) == 0 {
40 | return logger
41 | }
42 |
43 | result := logger
44 | result.labels = make(Labels)
45 | for k, v := range labels {
46 | result.labels[k] = v
47 | }
48 | return result
49 | }
50 |
51 | // WithCallDepth returns a logger whose call depth is set to the specified
52 | // value.
53 | func (logger Logger) WithCallDepth(callDepth int) Logger {
54 | result := logger
55 | result.callDepth = callDepth
56 | return result
57 | }
58 |
59 | func (logger Logger) getModule() *module {
60 | if logger.impl == nil {
61 | return defaultContext.root
62 | }
63 | return logger.impl
64 | }
65 |
66 | // Root returns the root logger for the Logger's context.
67 | func (logger Logger) Root() Logger {
68 | module := logger.getModule()
69 | return module.context.GetLogger("")
70 | }
71 |
72 | // Parent returns the Logger whose module name is the same
73 | // as this logger without the last period and suffix.
74 | // For example the parent of the logger that has the module
75 | // "a.b.c" is "a.b".
76 | // The Parent of the root logger is still the root logger.
77 | func (logger Logger) Parent() Logger {
78 | return Logger{
79 | impl: logger.getModule().parent,
80 | callDepth: defaultCallDepth,
81 | }
82 | }
83 |
84 | // Child returns the Logger whose module name is the composed of this
85 | // Logger's name and the specified name.
86 | func (logger Logger) Child(name string) Logger {
87 | module := logger.getModule()
88 | path := module.name
89 | if path == "" {
90 | path = name
91 | } else {
92 | path += "." + name
93 | }
94 | return module.context.GetLogger(path)
95 | }
96 |
97 | // ChildWithTags returns the Logger whose module name is the composed of this
98 | // Logger's name and the specified name with the correct associated tags.
99 | func (logger Logger) ChildWithTags(name string, tags ...string) Logger {
100 | module := logger.getModule()
101 | path := module.name
102 | if path == "" {
103 | path = name
104 | } else {
105 | path += "." + name
106 | }
107 | return module.context.GetLogger(path, tags...)
108 | }
109 |
110 | // ChildWithLabels returns the Logger whose module name is the composed of this
111 | // Logger's name and the specified name with the correct associated labels.
112 | // Adding labels to the child logger will cause all child loggers to also
113 | // inherit the labels of the parent(s) loggers.
114 | // For targeting a singular logger with labels, use WithLabels which are not
115 | // inherited by child loggers.
116 | func (logger Logger) ChildWithLabels(name string, labels Labels) Logger {
117 | module := logger.getModule()
118 | path := module.name
119 | if path == "" {
120 | path = name
121 | } else {
122 | path += "." + name
123 | }
124 |
125 | merged := make(Labels)
126 | for k, v := range logger.impl.labels {
127 | merged[k] = v
128 | }
129 | for k, v := range labels {
130 | merged[k] = v
131 | }
132 |
133 | result := module.context.GetLogger(path)
134 | result.impl.labels = merged
135 | return result
136 | }
137 |
138 | // Name returns the logger's module name.
139 | func (logger Logger) Name() string {
140 | return logger.getModule().Name()
141 | }
142 |
143 | // LogLevel returns the configured min log level of the logger.
144 | func (logger Logger) LogLevel() Level {
145 | return logger.getModule().level
146 | }
147 |
148 | // Tags returns the configured tags of the logger's module.
149 | func (logger Logger) Tags() []string {
150 | return logger.getModule().tags
151 | }
152 |
153 | // EffectiveLogLevel returns the effective min log level of
154 | // the receiver - that is, messages with a lesser severity
155 | // level will be discarded.
156 | //
157 | // If the log level of the receiver is unspecified,
158 | // it will be taken from the effective log level of its
159 | // parent.
160 | func (logger Logger) EffectiveLogLevel() Level {
161 | return logger.getModule().getEffectiveLogLevel()
162 | }
163 |
164 | // SetLogLevel sets the severity level of the given logger.
165 | // The root logger cannot be set to UNSPECIFIED level.
166 | // See EffectiveLogLevel for how this affects the
167 | // actual messages logged.
168 | func (logger Logger) SetLogLevel(level Level) {
169 | logger.getModule().setLevel(level)
170 | }
171 |
172 | // Logf logs a printf-formatted message at the given level.
173 | // A message will be discarded if level is less than the
174 | // the effective log level of the logger.
175 | // Note that the writers may also filter out messages that
176 | // are less than their registered minimum severity level.
177 | func (logger Logger) Logf(level Level, message string, args ...interface{}) {
178 | logger.logf(level, message, args...)
179 | }
180 |
181 | func (logger Logger) logf(level Level, message string, args ...interface{}) {
182 | logger.logCallf(logger.callDepth, level, message, nil, args...)
183 | }
184 |
185 | // LogWithlabelsf logs a printf-formatted message at the given level with extra
186 | // labels. The given labels will be added to the log entry.
187 | // A message will be discarded if level is less than the the effective log level
188 | // of the logger. Note that the writers may also filter out messages that are
189 | // less than their registered minimum severity level.
190 | func (logger Logger) LogWithLabelsf(level Level, message string, extraLabels map[string]string, args ...interface{}) {
191 | logger.logCallf(logger.callDepth-1, level, message, extraLabels, args...)
192 | }
193 |
194 | // LogCallf logs a printf-formatted message at the given level.
195 | // The location of the call is indicated by the calldepth argument.
196 | // A calldepth of 1 means the function that called this function.
197 | // A message will be discarded if level is less than the
198 | // the effective log level of the logger.
199 | // Note that the writers may also filter out messages that
200 | // are less than their registered minimum severity level.
201 | func (logger Logger) LogCallf(calldepth int, level Level, message string, args ...interface{}) {
202 | logger.logCallf(calldepth, level, message, nil, args...)
203 | }
204 |
205 | // logCallf is a private method for logging a printf-formatted message at the
206 | // given level. Used by LogWithLabelsf and LogCallf.
207 | func (logger Logger) logCallf(calldepth int, level Level, message string, extraLabels map[string]string, args ...interface{}) {
208 | module := logger.getModule()
209 | if !module.willWrite(level) {
210 | return
211 | }
212 | // Gather time, and filename, line number.
213 | now := time.Now() // get this early.
214 | // Param to Caller is the call depth. Since this method is called from
215 | // the Logger methods, we want the place that those were called from.
216 | _, file, line, ok := caller(calldepth + 1)
217 | if !ok {
218 | file = "???"
219 | line = 0
220 | }
221 | // Trim newline off format string, following usual
222 | // Go logging conventions.
223 | if len(message) > 0 && message[len(message)-1] == '\n' {
224 | message = message[0 : len(message)-1]
225 | }
226 |
227 | // To avoid having a proliferation of Info/Infof methods,
228 | // only use Sprintf if there are any args, and rely on the
229 | // `go vet` tool for the obvious cases where someone has forgotten
230 | // to provide an arg.
231 | formattedMessage := message
232 | if len(args) > 0 {
233 | formattedMessage = fmt.Sprintf(message, args...)
234 | }
235 |
236 | entry := Entry{
237 | Level: level,
238 | Filename: file,
239 | Line: line,
240 | Timestamp: now,
241 | Message: formattedMessage,
242 | }
243 | entry.Labels = make(Labels)
244 | if len(module.tags) > 0 {
245 | entry.Labels[LoggerTags] = strings.Join(module.tags, ",")
246 | }
247 | for k, v := range module.labels {
248 | entry.Labels[k] = v
249 | }
250 | for k, v := range logger.labels {
251 | entry.Labels[k] = v
252 | }
253 | // Add extra labels if there's any given.
254 | for k, v := range extraLabels {
255 | entry.Labels[k] = v
256 | }
257 | module.write(entry)
258 | }
259 |
260 | // Criticalf logs the printf-formatted message at critical level.
261 | func (logger Logger) Criticalf(message string, args ...interface{}) {
262 | logger.logf(CRITICAL, message, args...)
263 | }
264 |
265 | // Errorf logs the printf-formatted message at error level.
266 | func (logger Logger) Errorf(message string, args ...interface{}) {
267 | logger.logf(ERROR, message, args...)
268 | }
269 |
270 | // Warningf logs the printf-formatted message at warning level.
271 | func (logger Logger) Warningf(message string, args ...interface{}) {
272 | logger.logf(WARNING, message, args...)
273 | }
274 |
275 | // Infof logs the printf-formatted message at info level.
276 | func (logger Logger) Infof(message string, args ...interface{}) {
277 | logger.logf(INFO, message, args...)
278 | }
279 |
280 | // InfoWithLabelsf logs the printf-formatted message at info level with extra
281 | // labels.
282 | func (logger Logger) InfoWithLabelsf(message string, extraLabels map[string]string, args ...interface{}) {
283 | logger.logCallf(logger.callDepth, INFO, message, extraLabels, args...)
284 | }
285 |
286 | // Debugf logs the printf-formatted message at debug level.
287 | func (logger Logger) Debugf(message string, args ...interface{}) {
288 | logger.logf(DEBUG, message, args...)
289 | }
290 |
291 | // Tracef logs the printf-formatted message at trace level.
292 | func (logger Logger) Tracef(message string, args ...interface{}) {
293 | logger.logf(TRACE, message, args...)
294 | }
295 |
296 | // IsLevelEnabled returns whether debugging is enabled
297 | // for the given log level.
298 | func (logger Logger) IsLevelEnabled(level Level) bool {
299 | return logger.getModule().willWrite(level)
300 | }
301 |
302 | // IsErrorEnabled returns whether debugging is enabled
303 | // at error level.
304 | func (logger Logger) IsErrorEnabled() bool {
305 | return logger.IsLevelEnabled(ERROR)
306 | }
307 |
308 | // IsWarningEnabled returns whether debugging is enabled
309 | // at warning level.
310 | func (logger Logger) IsWarningEnabled() bool {
311 | return logger.IsLevelEnabled(WARNING)
312 | }
313 |
314 | // IsInfoEnabled returns whether debugging is enabled
315 | // at info level.
316 | func (logger Logger) IsInfoEnabled() bool {
317 | return logger.IsLevelEnabled(INFO)
318 | }
319 |
320 | // IsDebugEnabled returns whether debugging is enabled
321 | // at debug level.
322 | func (logger Logger) IsDebugEnabled() bool {
323 | return logger.IsLevelEnabled(DEBUG)
324 | }
325 |
326 | // IsTraceEnabled returns whether debugging is enabled
327 | // at trace level.
328 | func (logger Logger) IsTraceEnabled() bool {
329 | return logger.IsLevelEnabled(TRACE)
330 | }
331 |
332 | // Helper marks the caller as a helper function and will skip it when capturing
333 | // the callsite location.
334 | func (logger Logger) Helper() {
335 | helper(2)
336 | }
337 |
--------------------------------------------------------------------------------
/logger_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | gc "gopkg.in/check.v1"
8 |
9 | "github.com/juju/loggo/v2"
10 | )
11 |
12 | type LoggerSuite struct{}
13 |
14 | var _ = gc.Suite(&LoggerSuite{})
15 |
16 | func (*LoggerSuite) SetUpTest(c *gc.C) {
17 | loggo.ResetDefaultContext()
18 | }
19 |
20 | func (s *LoggerSuite) TestRootLogger(c *gc.C) {
21 | root := loggo.Logger{}.WithCallDepth(2)
22 | c.Check(root.Name(), gc.Equals, "")
23 | c.Check(root.LogLevel(), gc.Equals, loggo.WARNING)
24 | c.Check(root.IsErrorEnabled(), gc.Equals, true)
25 | c.Check(root.IsWarningEnabled(), gc.Equals, true)
26 | c.Check(root.IsInfoEnabled(), gc.Equals, false)
27 | c.Check(root.IsDebugEnabled(), gc.Equals, false)
28 | c.Check(root.IsTraceEnabled(), gc.Equals, false)
29 | }
30 |
31 | func (s *LoggerSuite) TestWithLabels(c *gc.C) {
32 | writer := &loggo.TestWriter{}
33 | context := loggo.NewContext(loggo.INFO)
34 | err := context.AddWriter("test", writer)
35 | c.Assert(err, gc.IsNil)
36 |
37 | logger := context.GetLogger("testing")
38 | loggerWithLabels := logger.WithLabels(loggo.Labels{"foo": "bar"})
39 | loggerWithTagsAndLabels := logger.
40 | ChildWithTags("withTags", "tag1", "tag2").
41 | WithLabels(loggo.Labels{"hello": "world"})
42 |
43 | logger.Logf(loggo.INFO, "without labels")
44 | loggerWithLabels.Logf(loggo.INFO, "with labels")
45 | loggerWithTagsAndLabels.Logf(loggo.INFO, "with tags and labels")
46 |
47 | logs := writer.Log()
48 | c.Assert(logs, gc.HasLen, 3)
49 | c.Check(logs[0].Message, gc.Equals, "without labels")
50 | c.Check(logs[0].Labels, gc.HasLen, 0)
51 | c.Check(logs[1].Message, gc.Equals, "with labels")
52 | c.Check(logs[1].Labels, gc.DeepEquals, loggo.Labels{"foo": "bar"})
53 | c.Check(logs[2].Message, gc.Equals, "with tags and labels")
54 | c.Check(logs[2].Labels, gc.DeepEquals, loggo.Labels{
55 | "logger-tags": "tag1,tag2",
56 | "hello": "world",
57 | })
58 | }
59 |
60 | func (s *LoggerSuite) TestNonInheritedLabels(c *gc.C) {
61 | writer := &loggo.TestWriter{}
62 | context := loggo.NewContext(loggo.INFO)
63 | err := context.AddWriter("test", writer)
64 | c.Assert(err, gc.IsNil)
65 |
66 | logger := context.GetLogger("testing").
67 | WithLabels(loggo.Labels{"hello": "world"})
68 |
69 | inheritedLoggerWithLabels := logger.
70 | ChildWithLabels("inherited", loggo.Labels{"foo": "bar"})
71 |
72 | logger.Logf(loggo.INFO, "with labels")
73 | inheritedLoggerWithLabels.Logf(loggo.INFO, "with inherited labels")
74 |
75 | logs := writer.Log()
76 | c.Assert(logs, gc.HasLen, 2)
77 |
78 | // The second log message should _only_ have the inherited labels.
79 |
80 | c.Check(logs[0].Message, gc.Equals, "with labels")
81 | c.Check(logs[0].Labels, gc.DeepEquals, loggo.Labels{"hello": "world"})
82 |
83 | c.Check(logs[1].Message, gc.Equals, "with inherited labels")
84 | c.Check(logs[1].Labels, gc.DeepEquals, loggo.Labels{"foo": "bar"})
85 | }
86 |
87 | func (s *LoggerSuite) TestNonInheritedWithInheritedLabels(c *gc.C) {
88 | writer := &loggo.TestWriter{}
89 | context := loggo.NewContext(loggo.INFO)
90 | err := context.AddWriter("test", writer)
91 | c.Assert(err, gc.IsNil)
92 |
93 | logger := context.GetLogger("testing")
94 |
95 | inheritedLoggerWithLabels := logger.
96 | ChildWithLabels("inherited", loggo.Labels{"foo": "bar"})
97 |
98 | scopedLoggerWithLabels := inheritedLoggerWithLabels.
99 | WithLabels(loggo.Labels{"hello": "world"})
100 |
101 | inheritedLoggerWithLabels.Logf(loggo.INFO, "with inherited labels")
102 | scopedLoggerWithLabels.Logf(loggo.INFO, "with scoped labels")
103 |
104 | logs := writer.Log()
105 | c.Assert(logs, gc.HasLen, 2)
106 |
107 | // The second log message should have both the inherited labels and
108 | // scoped labels.
109 |
110 | c.Check(logs[0].Message, gc.Equals, "with inherited labels")
111 | c.Check(logs[0].Labels, gc.DeepEquals, loggo.Labels{"foo": "bar"})
112 |
113 | c.Check(logs[1].Message, gc.Equals, "with scoped labels")
114 | c.Check(logs[1].Labels, gc.DeepEquals, loggo.Labels{
115 | "foo": "bar",
116 | "hello": "world",
117 | })
118 | }
119 |
120 | func (s *LoggerSuite) TestInheritedLabels(c *gc.C) {
121 | writer := &loggo.TestWriter{}
122 | context := loggo.NewContext(loggo.INFO)
123 | err := context.AddWriter("test", writer)
124 | c.Assert(err, gc.IsNil)
125 |
126 | logger := context.GetLogger("testing")
127 |
128 | nestedLoggerWithLabels := logger.
129 | ChildWithLabels("nested", loggo.Labels{"foo": "bar"})
130 | deepNestedLoggerWithLabels := nestedLoggerWithLabels.
131 | ChildWithLabels("nested", loggo.Labels{"foo": "bar"}).
132 | ChildWithLabels("deepnested", loggo.Labels{"fred": "tim"})
133 |
134 | loggerWithTagsAndLabels := logger.
135 | ChildWithLabels("nested-labels", loggo.Labels{"hello": "world"}).
136 | ChildWithTags("nested-tag", "tag1", "tag2")
137 |
138 | logger.Logf(loggo.INFO, "without labels")
139 | nestedLoggerWithLabels.Logf(loggo.INFO, "with nested labels")
140 | deepNestedLoggerWithLabels.Logf(loggo.INFO, "with deep nested labels")
141 | loggerWithTagsAndLabels.Logf(loggo.INFO, "with tags and labels")
142 |
143 | logs := writer.Log()
144 | c.Assert(logs, gc.HasLen, 4)
145 | c.Check(logs[0].Message, gc.Equals, "without labels")
146 | c.Check(logs[0].Labels, gc.HasLen, 0)
147 |
148 | c.Check(logs[1].Message, gc.Equals, "with nested labels")
149 | c.Check(logs[1].Labels, gc.DeepEquals, loggo.Labels{"foo": "bar"})
150 |
151 | c.Check(logs[2].Message, gc.Equals, "with deep nested labels")
152 | c.Check(logs[2].Labels, gc.DeepEquals, loggo.Labels{
153 | "foo": "bar",
154 | "fred": "tim",
155 | })
156 |
157 | c.Check(logs[3].Message, gc.Equals, "with tags and labels")
158 | c.Check(logs[3].Labels, gc.DeepEquals, loggo.Labels{
159 | "logger-tags": "tag1,tag2",
160 | "hello": "world",
161 | })
162 | }
163 |
164 | func (s *LoggerSuite) TestLogWithStaticAndDynamicLabels(c *gc.C) {
165 | writer := &loggo.TestWriter{}
166 | context := loggo.NewContext(loggo.INFO)
167 | err := context.AddWriter("test", writer)
168 | c.Assert(err, gc.IsNil)
169 |
170 | logger := context.GetLogger("testing")
171 | loggerWithLabels := logger.WithLabels(loggo.Labels{"foo": "bar"})
172 |
173 | loggerWithLabels.LogWithLabelsf(loggo.INFO, "no extra labels", nil)
174 | loggerWithLabels.LogWithLabelsf(loggo.INFO, "with extra labels", map[string]string{
175 | "domain": "status",
176 | "kind": "machine",
177 | "id": "0",
178 | "value": "idle",
179 | })
180 |
181 | logs := writer.Log()
182 | c.Assert(logs, gc.HasLen, 2)
183 | c.Check(logs[0].Message, gc.Equals, "no extra labels")
184 | c.Check(logs[0].Labels, gc.DeepEquals, loggo.Labels{"foo": "bar"})
185 | c.Check(logs[1].Message, gc.Equals, "with extra labels")
186 | c.Check(logs[1].Labels, gc.DeepEquals, loggo.Labels{
187 | "foo": "bar", "domain": "status", "id": "0", "kind": "machine", "value": "idle"})
188 | }
189 |
190 | func (s *LoggerSuite) TestLogWithExtraLabels(c *gc.C) {
191 | writer := &loggo.TestWriter{}
192 | context := loggo.NewContext(loggo.INFO)
193 | err := context.AddWriter("test", writer)
194 | c.Assert(err, gc.IsNil)
195 |
196 | logger := context.GetLogger("testing")
197 |
198 | logger.LogWithLabelsf(loggo.INFO, "no extra labels", nil)
199 | logger.LogWithLabelsf(loggo.INFO, "with extra labels", map[string]string{
200 | "domain": "status",
201 | "kind": "machine",
202 | "id": "0",
203 | "value": "idle",
204 | })
205 |
206 | logs := writer.Log()
207 | c.Assert(logs, gc.HasLen, 2)
208 | c.Check(logs[0].Message, gc.Equals, "no extra labels")
209 | c.Check(logs[0].Labels, gc.HasLen, 0)
210 | c.Check(logs[1].Message, gc.Equals, "with extra labels")
211 | c.Check(logs[1].Labels, gc.DeepEquals, loggo.Labels{
212 | "domain": "status", "id": "0", "kind": "machine", "value": "idle"})
213 | }
214 |
215 | func (s *LoggerSuite) TestInfoWithLabelsf(c *gc.C) {
216 | writer := &loggo.TestWriter{}
217 | context := loggo.NewContext(loggo.INFO)
218 | err := context.AddWriter("test", writer)
219 | c.Assert(err, gc.IsNil)
220 |
221 | logger := context.GetLogger("testing")
222 | logger.SetLogLevel(loggo.INFO)
223 | c.Assert(logger.LogLevel(), gc.Equals, loggo.INFO)
224 |
225 | logger.InfoWithLabelsf("no extra labels", nil)
226 | logger.InfoWithLabelsf("with extra labels", map[string]string{
227 | "domain": "status",
228 | "kind": "machine",
229 | "id": "0",
230 | "value": "idle",
231 | })
232 |
233 | logs := writer.Log()
234 | c.Assert(logs, gc.HasLen, 2)
235 | c.Check(logs[0].Message, gc.Equals, "no extra labels")
236 | c.Check(logs[0].Labels, gc.HasLen, 0)
237 | c.Check(logs[0].Level, gc.Equals, loggo.INFO)
238 | c.Check(logs[1].Message, gc.Equals, "with extra labels")
239 | c.Check(logs[1].Labels, gc.DeepEquals, loggo.Labels{
240 | "domain": "status", "id": "0", "kind": "machine", "value": "idle"})
241 | c.Check(logs[1].Level, gc.Equals, loggo.INFO)
242 | }
243 |
244 | func (s *LoggerSuite) TestSetLevel(c *gc.C) {
245 | logger := loggo.GetLogger("testing")
246 |
247 | c.Assert(logger.LogLevel(), gc.Equals, loggo.UNSPECIFIED)
248 | c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.WARNING)
249 | c.Assert(logger.IsErrorEnabled(), gc.Equals, true)
250 | c.Assert(logger.IsWarningEnabled(), gc.Equals, true)
251 | c.Assert(logger.IsInfoEnabled(), gc.Equals, false)
252 | c.Assert(logger.IsDebugEnabled(), gc.Equals, false)
253 | c.Assert(logger.IsTraceEnabled(), gc.Equals, false)
254 | logger.SetLogLevel(loggo.TRACE)
255 | c.Assert(logger.LogLevel(), gc.Equals, loggo.TRACE)
256 | c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.TRACE)
257 | c.Assert(logger.IsErrorEnabled(), gc.Equals, true)
258 | c.Assert(logger.IsWarningEnabled(), gc.Equals, true)
259 | c.Assert(logger.IsInfoEnabled(), gc.Equals, true)
260 | c.Assert(logger.IsDebugEnabled(), gc.Equals, true)
261 | c.Assert(logger.IsTraceEnabled(), gc.Equals, true)
262 | logger.SetLogLevel(loggo.DEBUG)
263 | c.Assert(logger.LogLevel(), gc.Equals, loggo.DEBUG)
264 | c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.DEBUG)
265 | c.Assert(logger.IsErrorEnabled(), gc.Equals, true)
266 | c.Assert(logger.IsWarningEnabled(), gc.Equals, true)
267 | c.Assert(logger.IsInfoEnabled(), gc.Equals, true)
268 | c.Assert(logger.IsDebugEnabled(), gc.Equals, true)
269 | c.Assert(logger.IsTraceEnabled(), gc.Equals, false)
270 | logger.SetLogLevel(loggo.INFO)
271 | c.Assert(logger.LogLevel(), gc.Equals, loggo.INFO)
272 | c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.INFO)
273 | c.Assert(logger.IsErrorEnabled(), gc.Equals, true)
274 | c.Assert(logger.IsWarningEnabled(), gc.Equals, true)
275 | c.Assert(logger.IsInfoEnabled(), gc.Equals, true)
276 | c.Assert(logger.IsDebugEnabled(), gc.Equals, false)
277 | c.Assert(logger.IsTraceEnabled(), gc.Equals, false)
278 | logger.SetLogLevel(loggo.WARNING)
279 | c.Assert(logger.LogLevel(), gc.Equals, loggo.WARNING)
280 | c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.WARNING)
281 | c.Assert(logger.IsErrorEnabled(), gc.Equals, true)
282 | c.Assert(logger.IsWarningEnabled(), gc.Equals, true)
283 | c.Assert(logger.IsInfoEnabled(), gc.Equals, false)
284 | c.Assert(logger.IsDebugEnabled(), gc.Equals, false)
285 | c.Assert(logger.IsTraceEnabled(), gc.Equals, false)
286 | logger.SetLogLevel(loggo.ERROR)
287 | c.Assert(logger.LogLevel(), gc.Equals, loggo.ERROR)
288 | c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.ERROR)
289 | c.Assert(logger.IsErrorEnabled(), gc.Equals, true)
290 | c.Assert(logger.IsWarningEnabled(), gc.Equals, false)
291 | c.Assert(logger.IsInfoEnabled(), gc.Equals, false)
292 | c.Assert(logger.IsDebugEnabled(), gc.Equals, false)
293 | c.Assert(logger.IsTraceEnabled(), gc.Equals, false)
294 | // This is added for completeness, but not really expected to be used.
295 | logger.SetLogLevel(loggo.CRITICAL)
296 | c.Assert(logger.LogLevel(), gc.Equals, loggo.CRITICAL)
297 | c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.CRITICAL)
298 | c.Assert(logger.IsErrorEnabled(), gc.Equals, false)
299 | c.Assert(logger.IsWarningEnabled(), gc.Equals, false)
300 | c.Assert(logger.IsInfoEnabled(), gc.Equals, false)
301 | c.Assert(logger.IsDebugEnabled(), gc.Equals, false)
302 | c.Assert(logger.IsTraceEnabled(), gc.Equals, false)
303 | logger.SetLogLevel(loggo.UNSPECIFIED)
304 | c.Assert(logger.LogLevel(), gc.Equals, loggo.UNSPECIFIED)
305 | c.Assert(logger.EffectiveLogLevel(), gc.Equals, loggo.WARNING)
306 | }
307 |
308 | func (s *LoggerSuite) TestModuleLowered(c *gc.C) {
309 | logger1 := loggo.GetLogger("TESTING.MODULE")
310 | logger2 := loggo.GetLogger("Testing")
311 |
312 | c.Assert(logger1.Name(), gc.Equals, "testing.module")
313 | c.Assert(logger2.Name(), gc.Equals, "testing")
314 | }
315 |
316 | func (s *LoggerSuite) TestLevelsInherited(c *gc.C) {
317 | root := loggo.GetLogger("")
318 | first := loggo.GetLogger("first")
319 | second := loggo.GetLogger("first.second")
320 |
321 | root.SetLogLevel(loggo.ERROR)
322 | c.Assert(root.LogLevel(), gc.Equals, loggo.ERROR)
323 | c.Assert(root.EffectiveLogLevel(), gc.Equals, loggo.ERROR)
324 | c.Assert(first.LogLevel(), gc.Equals, loggo.UNSPECIFIED)
325 | c.Assert(first.EffectiveLogLevel(), gc.Equals, loggo.ERROR)
326 | c.Assert(second.LogLevel(), gc.Equals, loggo.UNSPECIFIED)
327 | c.Assert(second.EffectiveLogLevel(), gc.Equals, loggo.ERROR)
328 |
329 | first.SetLogLevel(loggo.DEBUG)
330 | c.Assert(root.LogLevel(), gc.Equals, loggo.ERROR)
331 | c.Assert(root.EffectiveLogLevel(), gc.Equals, loggo.ERROR)
332 | c.Assert(first.LogLevel(), gc.Equals, loggo.DEBUG)
333 | c.Assert(first.EffectiveLogLevel(), gc.Equals, loggo.DEBUG)
334 | c.Assert(second.LogLevel(), gc.Equals, loggo.UNSPECIFIED)
335 | c.Assert(second.EffectiveLogLevel(), gc.Equals, loggo.DEBUG)
336 |
337 | second.SetLogLevel(loggo.INFO)
338 | c.Assert(root.LogLevel(), gc.Equals, loggo.ERROR)
339 | c.Assert(root.EffectiveLogLevel(), gc.Equals, loggo.ERROR)
340 | c.Assert(first.LogLevel(), gc.Equals, loggo.DEBUG)
341 | c.Assert(first.EffectiveLogLevel(), gc.Equals, loggo.DEBUG)
342 | c.Assert(second.LogLevel(), gc.Equals, loggo.INFO)
343 | c.Assert(second.EffectiveLogLevel(), gc.Equals, loggo.INFO)
344 |
345 | first.SetLogLevel(loggo.UNSPECIFIED)
346 | c.Assert(root.LogLevel(), gc.Equals, loggo.ERROR)
347 | c.Assert(root.EffectiveLogLevel(), gc.Equals, loggo.ERROR)
348 | c.Assert(first.LogLevel(), gc.Equals, loggo.UNSPECIFIED)
349 | c.Assert(first.EffectiveLogLevel(), gc.Equals, loggo.ERROR)
350 | c.Assert(second.LogLevel(), gc.Equals, loggo.INFO)
351 | c.Assert(second.EffectiveLogLevel(), gc.Equals, loggo.INFO)
352 | }
353 |
354 | func (s *LoggerSuite) TestParent(c *gc.C) {
355 | logger := loggo.GetLogger("a.b.c")
356 | b := logger.Parent()
357 | a := b.Parent()
358 | root := a.Parent()
359 |
360 | c.Check(b.Name(), gc.Equals, "a.b")
361 | c.Check(a.Name(), gc.Equals, "a")
362 | c.Check(root.Name(), gc.Equals, "")
363 | c.Check(root.Parent(), gc.DeepEquals, root)
364 | }
365 |
366 | func (s *LoggerSuite) TestParentSameContext(c *gc.C) {
367 | ctx := loggo.NewContext(loggo.DEBUG)
368 |
369 | logger := ctx.GetLogger("a.b.c")
370 | b := logger.Parent()
371 |
372 | c.Check(b, gc.DeepEquals, ctx.GetLogger("a.b"))
373 | c.Check(b, gc.Not(gc.DeepEquals), loggo.GetLogger("a.b"))
374 | }
375 |
376 | func (s *LoggerSuite) TestChild(c *gc.C) {
377 | root := loggo.GetLogger("")
378 |
379 | a := root.Child("a")
380 | logger := a.Child("b.c")
381 |
382 | c.Check(a.Name(), gc.Equals, "a")
383 | c.Check(logger.Name(), gc.Equals, "a.b.c")
384 | c.Check(logger.Parent(), gc.DeepEquals, a.Child("b"))
385 | }
386 |
387 | func (s *LoggerSuite) TestChildSameContext(c *gc.C) {
388 | ctx := loggo.NewContext(loggo.DEBUG)
389 |
390 | logger := ctx.GetLogger("a")
391 | b := logger.Child("b")
392 |
393 | c.Check(b, gc.DeepEquals, ctx.GetLogger("a.b"))
394 | c.Check(b, gc.Not(gc.DeepEquals), loggo.GetLogger("a.b"))
395 | }
396 |
397 | func (s *LoggerSuite) TestChildSameContextWithTags(c *gc.C) {
398 | ctx := loggo.NewContext(loggo.DEBUG)
399 |
400 | logger := ctx.GetLogger("a", "parent")
401 | b := logger.ChildWithTags("b", "child")
402 |
403 | c.Check(ctx.GetAllLoggerTags(), gc.DeepEquals, []string{"child", "parent"})
404 | c.Check(logger.Tags(), gc.DeepEquals, []string{"parent"})
405 | c.Check(b.Tags(), gc.DeepEquals, []string{"child"})
406 | }
407 |
408 | func (s *LoggerSuite) TestRoot(c *gc.C) {
409 | logger := loggo.GetLogger("a.b.c")
410 | root := logger.Root()
411 |
412 | c.Check(root.Name(), gc.Equals, "")
413 | c.Check(root.Child("a.b.c"), gc.DeepEquals, logger)
414 | }
415 |
416 | func (s *LoggerSuite) TestRootSameContext(c *gc.C) {
417 | ctx := loggo.NewContext(loggo.DEBUG)
418 |
419 | logger := ctx.GetLogger("a.b.c")
420 | root := logger.Root()
421 |
422 | c.Check(root.Name(), gc.Equals, "")
423 | c.Check(root.Child("a.b.c"), gc.DeepEquals, logger)
424 | c.Check(root.Child("a.b.c"), gc.Not(gc.DeepEquals), loggo.GetLogger("a.b.c"))
425 | }
426 |
--------------------------------------------------------------------------------
/logging_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | "time"
8 |
9 | gc "gopkg.in/check.v1"
10 |
11 | "github.com/juju/loggo/v2"
12 | )
13 |
14 | type LoggingSuite struct {
15 | context *loggo.Context
16 | writer *writer
17 | logger loggo.Logger
18 |
19 | // Test that labels get outputted to loggo.Entry
20 | Labels map[string]string
21 | }
22 |
23 | var _ = gc.Suite(&LoggingSuite{})
24 | var _ = gc.Suite(&LoggingSuite{Labels: loggo.Labels{"logger-tags": "ONE,TWO"}})
25 |
26 | func (s *LoggingSuite) SetUpTest(c *gc.C) {
27 | s.writer = &writer{}
28 | s.context = loggo.NewContext(loggo.TRACE)
29 | err := s.context.AddWriter("test", s.writer)
30 | c.Assert(err, gc.IsNil)
31 | s.logger = s.context.GetLogger("test", "ONE,TWO")
32 | }
33 |
34 | func (s *LoggingSuite) TestLoggingStrings(c *gc.C) {
35 | s.logger.Infof("simple")
36 | s.logger.Infof("with args %d", 42)
37 | s.logger.Infof("working 100%")
38 | s.logger.Infof("missing %s")
39 |
40 | checkLogEntries(c, s.writer.Log(), []loggo.Entry{
41 | {Level: loggo.INFO, Module: "test", Message: "simple", Labels: s.Labels},
42 | {Level: loggo.INFO, Module: "test", Message: "with args 42", Labels: s.Labels},
43 | {Level: loggo.INFO, Module: "test", Message: "working 100%", Labels: s.Labels},
44 | {Level: loggo.INFO, Module: "test", Message: "missing %s", Labels: s.Labels},
45 | })
46 | }
47 |
48 | func (s *LoggingSuite) TestLoggingLimitWarning(c *gc.C) {
49 | s.logger.SetLogLevel(loggo.WARNING)
50 | start := time.Now()
51 | logAllSeverities(s.logger)
52 | end := time.Now()
53 | entries := s.writer.Log()
54 | checkLogEntries(c, entries, []loggo.Entry{
55 | {Level: loggo.CRITICAL, Module: "test", Message: "something critical", Labels: s.Labels},
56 | {Level: loggo.ERROR, Module: "test", Message: "an error", Labels: s.Labels},
57 | {Level: loggo.WARNING, Module: "test", Message: "a warning message", Labels: s.Labels},
58 | })
59 |
60 | for _, entry := range entries {
61 | c.Check(entry.Timestamp, Between(start, end))
62 | }
63 | }
64 |
65 | func (s *LoggingSuite) TestLocationCapture(c *gc.C) {
66 | s.helperInfof(c, "helper message") //tag helper-location
67 | s.logger.Criticalf("critical message") //tag critical-location
68 | s.logger.Errorf("error message") //tag error-location
69 | s.logger.Warningf("warning message") //tag warning-location
70 | s.logger.Infof("info message") //tag info-location
71 | s.logger.Debugf("debug message") //tag debug-location
72 | s.logger.Tracef("trace message") //tag trace-location
73 | s.logger.Logf(loggo.INFO, "logf msg") //tag logf-location
74 | s.logger.LogCallf(1, loggo.INFO, "logcallf msg") //tag logcallf-location
75 | s.logger.LogWithLabelsf(loggo.INFO, "logwithlabelsf msg", nil) //tag logwithlabelsf-location
76 |
77 | log := s.writer.Log()
78 | tags := []string{
79 | "helper-location",
80 | "critical-location",
81 | "error-location",
82 | "warning-location",
83 | "info-location",
84 | "debug-location",
85 | "trace-location",
86 | "logf-location",
87 | "logcallf-location",
88 | "logwithlabelsf-location",
89 | }
90 | c.Assert(log, gc.HasLen, len(tags))
91 | for x := range tags {
92 | assertLocation(c, log[x], tags[x])
93 | }
94 | }
95 |
96 | func (s *LoggingSuite) helperInfof(c *gc.C, format string, args ...any) {
97 | s.logger.Helper()
98 | s.logger.Infof(format, args...)
99 | }
100 |
101 | func (s *LoggingSuite) TestLogDoesntLogWeirdLevels(c *gc.C) {
102 | s.logger.Logf(loggo.UNSPECIFIED, "message")
103 | c.Assert(s.writer.Log(), gc.HasLen, 0)
104 |
105 | s.logger.Logf(loggo.Level(42), "message")
106 | c.Assert(s.writer.Log(), gc.HasLen, 0)
107 |
108 | s.logger.Logf(loggo.CRITICAL+loggo.Level(1), "message")
109 | c.Assert(s.writer.Log(), gc.HasLen, 0)
110 | }
111 |
--------------------------------------------------------------------------------
/loggocolor/writer.go:
--------------------------------------------------------------------------------
1 | package loggocolor
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "path/filepath"
7 |
8 | "github.com/juju/ansiterm"
9 |
10 | "github.com/juju/loggo/v2"
11 | )
12 |
13 | var (
14 | // SeverityColor defines the colors for the levels output by the ColorWriter.
15 | SeverityColor = map[loggo.Level]*ansiterm.Context{
16 | loggo.TRACE: ansiterm.Foreground(ansiterm.Default),
17 | loggo.DEBUG: ansiterm.Foreground(ansiterm.Green),
18 | loggo.INFO: ansiterm.Foreground(ansiterm.BrightBlue),
19 | loggo.WARNING: ansiterm.Foreground(ansiterm.Yellow),
20 | loggo.ERROR: ansiterm.Foreground(ansiterm.BrightRed),
21 | loggo.CRITICAL: {
22 | Foreground: ansiterm.White,
23 | Background: ansiterm.Red,
24 | },
25 | }
26 | // LocationColor defines the colors for the location output by the ColorWriter.
27 | LocationColor = ansiterm.Foreground(ansiterm.BrightBlue)
28 | )
29 |
30 | type colorWriter struct {
31 | writer *ansiterm.Writer
32 | }
33 |
34 | // NewColorWriter will write out colored severity levels if the writer is
35 | // outputting to a terminal.
36 | func NewWriter(writer io.Writer) loggo.Writer {
37 | return &colorWriter{ansiterm.NewWriter(writer)}
38 | }
39 |
40 | // NewcolorWriter will write out colored severity levels whether or not the
41 | // writer is outputting to a terminal.
42 | func NewColorWriter(writer io.Writer) loggo.Writer {
43 | w := ansiterm.NewWriter(writer)
44 | w.SetColorCapable(true)
45 | return &colorWriter{w}
46 | }
47 |
48 | // Write implements Writer.
49 | func (w *colorWriter) Write(entry loggo.Entry) {
50 | ts := entry.Timestamp.Format(loggo.TimeFormat)
51 | // Just get the basename from the filename
52 | filename := filepath.Base(entry.Filename)
53 |
54 | fmt.Fprintf(w.writer, "%s ", ts)
55 | SeverityColor[entry.Level].Fprintf(w.writer, entry.Level.Short())
56 | fmt.Fprintf(w.writer, " %s ", entry.Module)
57 | LocationColor.Fprintf(w.writer, "%s:%d ", filename, entry.Line)
58 | fmt.Fprintln(w.writer, entry.Message)
59 | }
60 |
--------------------------------------------------------------------------------
/module.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | // Do not change rootName: modules.resolve() will misbehave if it isn't "".
7 | const (
8 | rootString = ""
9 | )
10 |
11 | type module struct {
12 | name string
13 | level Level
14 | parent *module
15 | context *Context
16 |
17 | tags []string
18 | tagsLookup map[string]struct{}
19 |
20 | labels Labels
21 | }
22 |
23 | // Name returns the module's name.
24 | func (m *module) Name() string {
25 | if m.name == "" {
26 | return rootString
27 | }
28 | return m.name
29 | }
30 |
31 | func (m *module) willWrite(level Level) bool {
32 | if level < TRACE || level > CRITICAL {
33 | return false
34 | }
35 | return level >= m.getEffectiveLogLevel()
36 | }
37 |
38 | func (m *module) getEffectiveLogLevel() Level {
39 | // Note: the root module is guaranteed to have a
40 | // specified logging level, so acts as a suitable sentinel
41 | // for this loop.
42 | for {
43 | if level := m.level.get(); level != UNSPECIFIED {
44 | return level
45 | }
46 | m = m.parent
47 | }
48 | }
49 |
50 | // setLevel sets the severity level of the given module.
51 | // The root module cannot be set to UNSPECIFIED level.
52 | func (m *module) setLevel(level Level) {
53 | // The root module can't be unspecified.
54 | if m.name == "" && level == UNSPECIFIED {
55 | level = WARNING
56 | }
57 | m.level.set(level)
58 | }
59 |
60 | func (m *module) write(entry Entry) {
61 | entry.Module = m.name
62 | m.context.write(entry)
63 | }
64 |
--------------------------------------------------------------------------------
/package_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | "testing"
8 |
9 | gc "gopkg.in/check.v1"
10 | )
11 |
12 | func Test(t *testing.T) {
13 | gc.TestingT(t)
14 | }
15 |
--------------------------------------------------------------------------------
/testwriter.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import (
7 | "path"
8 | "sync"
9 | )
10 |
11 | // TestWriter is a useful Writer for testing purposes. Each component of the
12 | // logging message is stored in the Log array.
13 | type TestWriter struct {
14 | mu sync.Mutex
15 | log []Entry
16 | }
17 |
18 | // Write saves the params as members in the TestLogValues struct appended to the Log array.
19 | func (writer *TestWriter) Write(entry Entry) {
20 | writer.mu.Lock()
21 | defer writer.mu.Unlock()
22 | entry.Filename = path.Base(entry.Filename)
23 | writer.log = append(writer.log, entry)
24 | }
25 |
26 | // Clear removes any saved log messages.
27 | func (writer *TestWriter) Clear() {
28 | writer.mu.Lock()
29 | defer writer.mu.Unlock()
30 | writer.log = nil
31 | }
32 |
33 | // Log returns a copy of the current logged values.
34 | func (writer *TestWriter) Log() []Entry {
35 | writer.mu.Lock()
36 | defer writer.mu.Unlock()
37 | v := make([]Entry, len(writer.log))
38 | copy(v, writer.log)
39 | return v
40 | }
41 |
--------------------------------------------------------------------------------
/util_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2016 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo_test
5 |
6 | import (
7 | "fmt"
8 | "io/ioutil"
9 | "strings"
10 |
11 | "github.com/juju/loggo/v2"
12 |
13 | gc "gopkg.in/check.v1"
14 | )
15 |
16 | func init() {
17 | setLocationsForTags("logging_test.go")
18 | setLocationsForTags("writer_test.go")
19 | }
20 |
21 | func assertLocation(c *gc.C, msg loggo.Entry, tag string) {
22 | loc := location(tag)
23 | c.Check(fmt.Sprintf("%s:%d", msg.Filename, msg.Line), gc.Equals,
24 | fmt.Sprintf("%s:%d", loc.file, loc.line),
25 | gc.Commentf("tag=%s", tag))
26 | }
27 |
28 | // All this location stuff is to avoid having hard coded line numbers
29 | // in the tests. Any line where as a test writer you want to capture the
30 | // file and line number, add a comment that has `//tag name` as the end of
31 | // the line. The name must be unique across all the tests, and the test
32 | // will panic if it is not. This name is then used to read the actual
33 | // file and line numbers.
34 |
35 | func location(tag string) Location {
36 | loc, ok := tagToLocation[tag]
37 | if !ok {
38 | panic(fmt.Errorf("tag %q not found", tag))
39 | }
40 | return loc
41 | }
42 |
43 | type Location struct {
44 | file string
45 | line int
46 | }
47 |
48 | func (loc Location) String() string {
49 | return fmt.Sprintf("%s:%d", loc.file, loc.line)
50 | }
51 |
52 | var tagToLocation = make(map[string]Location)
53 |
54 | func setLocationsForTags(filename string) {
55 | data, err := ioutil.ReadFile(filename)
56 | if err != nil {
57 | panic(err)
58 | }
59 | lines := strings.Split(string(data), "\n")
60 | for i, line := range lines {
61 | if j := strings.Index(line, "//tag "); j >= 0 {
62 | tag := line[j+len("//tag "):]
63 | if _, found := tagToLocation[tag]; found {
64 | panic(fmt.Errorf("tag %q already processed previously", tag))
65 | }
66 | tagToLocation[tag] = Location{file: filename, line: i + 1}
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/writer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import (
7 | "fmt"
8 | "io"
9 | "os"
10 | )
11 |
12 | // DefaultWriterName is the name of the default writer for
13 | // a Context.
14 | const DefaultWriterName = "default"
15 |
16 | // Writer is implemented by any recipient of log messages.
17 | type Writer interface {
18 | // Write writes a message to the Writer with the given level and module
19 | // name. The filename and line hold the file name and line number of the
20 | // code that is generating the log message; the time stamp holds the time
21 | // the log message was generated, and message holds the log message
22 | // itself.
23 | Write(entry Entry)
24 | }
25 |
26 | // NewMinLevelWriter returns a Writer that will only pass on the Write calls
27 | // to the provided writer if the log level is at or above the specified
28 | // minimum level.
29 | func NewMinimumLevelWriter(writer Writer, minLevel Level) Writer {
30 | return &minLevelWriter{
31 | writer: writer,
32 | level: minLevel,
33 | }
34 | }
35 |
36 | type minLevelWriter struct {
37 | writer Writer
38 | level Level
39 | }
40 |
41 | // Write writes the log record.
42 | func (w minLevelWriter) Write(entry Entry) {
43 | if entry.Level < w.level {
44 | return
45 | }
46 | w.writer.Write(entry)
47 | }
48 |
49 | type simpleWriter struct {
50 | writer io.Writer
51 | formatter func(entry Entry) string
52 | }
53 |
54 | // NewSimpleWriter returns a new writer that writes log messages to the given
55 | // io.Writer formatting the messages with the given formatter.
56 | func NewSimpleWriter(writer io.Writer, formatter func(entry Entry) string) Writer {
57 | if formatter == nil {
58 | formatter = DefaultFormatter
59 | }
60 | return &simpleWriter{writer, formatter}
61 | }
62 |
63 | func (simple *simpleWriter) Write(entry Entry) {
64 | logLine := simple.formatter(entry)
65 | fmt.Fprintln(simple.writer, logLine)
66 | }
67 |
68 | func defaultWriter() Writer {
69 | return NewSimpleWriter(os.Stderr, DefaultFormatter)
70 | }
71 |
--------------------------------------------------------------------------------
/writer_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Canonical Ltd.
2 | // Licensed under the LGPLv3, see LICENCE file for details.
3 |
4 | package loggo
5 |
6 | import (
7 | "bytes"
8 | "time"
9 |
10 | gc "gopkg.in/check.v1"
11 | )
12 |
13 | type SimpleWriterSuite struct{}
14 |
15 | var _ = gc.Suite(&SimpleWriterSuite{})
16 |
17 | func (s *SimpleWriterSuite) TestNewSimpleWriter(c *gc.C) {
18 | now := time.Now()
19 | formatter := func(entry Entry) string {
20 | return "<< " + entry.Message + " >>"
21 | }
22 | buf := &bytes.Buffer{}
23 |
24 | writer := NewSimpleWriter(buf, formatter)
25 | writer.Write(Entry{
26 | Level: INFO,
27 | Module: "test",
28 | Filename: "somefile.go",
29 | Line: 12,
30 | Timestamp: now,
31 | Message: "a message",
32 | Labels: nil,
33 | })
34 |
35 | c.Check(buf.String(), gc.Equals, "<< a message >>\n")
36 | }
37 |
38 | func (s *SimpleWriterSuite) TestNewSimpleWriterWithLabels(c *gc.C) {
39 | now := time.Now()
40 | formatter := func(entry Entry) string {
41 | return "<< " + entry.Message + " >>"
42 | }
43 | buf := &bytes.Buffer{}
44 |
45 | writer := NewSimpleWriter(buf, formatter)
46 | writer.Write(Entry{
47 | Level: INFO,
48 | Module: "test",
49 | Filename: "somefile.go",
50 | Line: 12,
51 | Timestamp: now,
52 | Message: "a message",
53 | Labels: Labels{LoggerTags: "ONE,TWO"},
54 | })
55 |
56 | c.Check(buf.String(), gc.Equals, "<< a message >>\n")
57 | }
58 |
--------------------------------------------------------------------------------