├── .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\]|[![GoDoc](https://godoc.org/github.com/juju/loggo?status.svg)](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 | [![GoDoc](https://godoc.org/github.com/juju/loggo?status.svg)](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 | --------------------------------------------------------------------------------