├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc.go ├── go.mod ├── goclean.sh ├── interface.go ├── log.go └── v2 ├── LICENSE ├── attrs.go ├── buffer.go ├── closure.go ├── go.mod ├── go.sum ├── handler.go ├── handler_test.go ├── interface.go ├── level.go ├── log.go ├── log_test.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Temp files 2 | *~ 3 | 4 | # Log files 5 | *.log 6 | 7 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 8 | *.o 9 | *.a 10 | *.so 11 | 12 | # Folders 13 | _obj 14 | _test 15 | 16 | # Architecture specific extensions/prefixes 17 | *.[568vq] 18 | [568vq].out 19 | 20 | *.cgo1.go 21 | *.cgo2.c 22 | _cgo_defun.c 23 | _cgo_gotypes.go 24 | _cgo_export.* 25 | 26 | _testmain.go 27 | 28 | *.exe 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7.x 4 | - 1.8.x 5 | sudo: false 6 | install: 7 | - go get -d -t -v ./... 8 | - go get -v golang.org/x/tools/cmd/cover 9 | - go get -v github.com/bradfitz/goimports 10 | - go get -v github.com/golang/lint/golint 11 | script: 12 | - export PATH=$PATH:$HOME/gopath/bin 13 | - ./goclean.sh 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2013-2014 Conformal Systems LLC. 4 | 5 | Permission to use, copy, modify, and distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | btclog 2 | ====== 3 | 4 | [![Build Status](http://img.shields.io/travis/btcsuite/btclog.svg)](https://travis-ci.org/btcsuite/btclog) 5 | [![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) 6 | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/btcsuite/btclog) 7 | 8 | Package btclog defines a logger interface and provides a default implementation 9 | of a subsystem-aware leveled logger implementing the same interface. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | $ go get github.com/btcsuite/btclog 15 | ``` 16 | 17 | ## GPG Verification Key 18 | 19 | All official release tags are signed by Conformal so users can ensure the code 20 | has not been tampered with and is coming from the btcsuite developers. To 21 | verify the signature perform the following: 22 | 23 | - Download the public key from the Conformal website at 24 | https://opensource.conformal.com/GIT-GPG-KEY-conformal.txt 25 | 26 | - Import the public key into your GPG keyring: 27 | ```bash 28 | gpg --import GIT-GPG-KEY-conformal.txt 29 | ``` 30 | 31 | - Verify the release tag with the following command where `TAG_NAME` is a 32 | placeholder for the specific tag: 33 | ```bash 34 | git tag -v TAG_NAME 35 | ``` 36 | 37 | ## License 38 | 39 | Package btclog is licensed under the [copyfree](http://copyfree.org) ISC 40 | License. 41 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013-2017 The btcsuite developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package btclog defines an interface and default implementation for subsystem 7 | logging. 8 | 9 | Log level verbosity may be modified at runtime for each individual subsystem 10 | logger. 11 | 12 | The default implementation in this package must be created by the Backend type. 13 | Backends can write to any io.Writer, including multi-writers created by 14 | io.MultiWriter. Multi-writers allow log output to be written to many writers, 15 | including standard output and log files. 16 | 17 | Optional logging behavior can be specified by using the LOGFLAGS environment 18 | variable and overridden per-Backend by using the WithFlags call option. Multiple 19 | LOGFLAGS options can be specified, separated by commas. The following options 20 | are recognized: 21 | 22 | longfile: Include the full filepath and line number in all log messages 23 | 24 | shortfile: Include the filename and line number in all log messages. 25 | Overrides longfile. 26 | */ 27 | package btclog 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/btcsuite/btclog 2 | 3 | go 1.17.0 4 | -------------------------------------------------------------------------------- /goclean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # The script does automatic checking on a Go package and its sub-packages, including: 3 | # 1. gofmt (http://golang.org/cmd/gofmt/) 4 | # 2. goimports (https://github.com/bradfitz/goimports) 5 | # 3. golint (https://github.com/golang/lint) 6 | # 4. go vet (http://golang.org/cmd/vet) 7 | # 5. race detector (http://blog.golang.org/race-detector) 8 | # 6. test coverage (http://blog.golang.org/cover) 9 | 10 | set -e 11 | 12 | # Automatic checks 13 | test -z $(gofmt -l -w . | tee /dev/stderr) 14 | test -z $(goimports -l -w . | tee /dev/stderr) 15 | test -z $(golint ./... | tee /dev/stderr) 16 | go vet ./... 17 | env GORACE="halt_on_error=1" go test -v -race ./... 18 | 19 | # Run test coverage on each subdirectories and merge the coverage profile. 20 | 21 | echo "mode: count" > profile.cov 22 | 23 | # Standard go tooling behavior is to ignore dirs with leading underscores. 24 | for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -type d) 25 | do 26 | if ls $dir/*.go &> /dev/null; then 27 | go test -covermode=count -coverprofile=$dir/profile.tmp $dir 28 | if [ -f $dir/profile.tmp ]; then 29 | cat $dir/profile.tmp | tail -n +2 >> profile.cov 30 | rm $dir/profile.tmp 31 | fi 32 | fi 33 | done 34 | 35 | # To submit the test coverage result to coveralls.io, 36 | # use goveralls (https://github.com/mattn/goveralls) 37 | # goveralls -coverprofile=profile.cov -service=travis-ci 38 | -------------------------------------------------------------------------------- /interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013-2017 The btcsuite developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | 5 | package btclog 6 | 7 | // Logger is an interface which describes a level-based logger. A default 8 | // implementation of Logger is implemented by this package and can be created 9 | // by calling (*Backend).Logger. 10 | type Logger interface { 11 | // Tracef formats message according to format specifier and writes to 12 | // to log with LevelTrace. 13 | Tracef(format string, params ...interface{}) 14 | 15 | // Debugf formats message according to format specifier and writes to 16 | // log with LevelDebug. 17 | Debugf(format string, params ...interface{}) 18 | 19 | // Infof formats message according to format specifier and writes to 20 | // log with LevelInfo. 21 | Infof(format string, params ...interface{}) 22 | 23 | // Warnf formats message according to format specifier and writes to 24 | // to log with LevelWarn. 25 | Warnf(format string, params ...interface{}) 26 | 27 | // Errorf formats message according to format specifier and writes to 28 | // to log with LevelError. 29 | Errorf(format string, params ...interface{}) 30 | 31 | // Criticalf formats message according to format specifier and writes to 32 | // log with LevelCritical. 33 | Criticalf(format string, params ...interface{}) 34 | 35 | // Trace formats message using the default formats for its operands 36 | // and writes to log with LevelTrace. 37 | Trace(v ...interface{}) 38 | 39 | // Debug formats message using the default formats for its operands 40 | // and writes to log with LevelDebug. 41 | Debug(v ...interface{}) 42 | 43 | // Info formats message using the default formats for its operands 44 | // and writes to log with LevelInfo. 45 | Info(v ...interface{}) 46 | 47 | // Warn formats message using the default formats for its operands 48 | // and writes to log with LevelWarn. 49 | Warn(v ...interface{}) 50 | 51 | // Error formats message using the default formats for its operands 52 | // and writes to log with LevelError. 53 | Error(v ...interface{}) 54 | 55 | // Critical formats message using the default formats for its operands 56 | // and writes to log with LevelCritical. 57 | Critical(v ...interface{}) 58 | 59 | // Level returns the current logging level. 60 | Level() Level 61 | 62 | // SetLevel changes the logging level to the passed level. 63 | SetLevel(level Level) 64 | } 65 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The btcsuite developers 2 | // Use of this source code is governed by an ISC 3 | // license that can be found in the LICENSE file. 4 | // 5 | // Copyright (c) 2009 The Go Authors. All rights reserved. 6 | // 7 | // Redistribution and use in source and binary forms, with or without 8 | // modification, are permitted provided that the following conditions are 9 | // met: 10 | // 11 | // * Redistributions of source code must retain the above copyright 12 | // notice, this list of conditions and the following disclaimer. 13 | // * Redistributions in binary form must reproduce the above 14 | // copyright notice, this list of conditions and the following disclaimer 15 | // in the documentation and/or other materials provided with the 16 | // distribution. 17 | // * Neither the name of Google Inc. nor the names of its 18 | // contributors may be used to endorse or promote products derived from 19 | // this software without specific prior written permission. 20 | // 21 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | package btclog 34 | 35 | import ( 36 | "bytes" 37 | "fmt" 38 | "io" 39 | "io/ioutil" 40 | "os" 41 | "runtime" 42 | "strings" 43 | "sync" 44 | "sync/atomic" 45 | "time" 46 | ) 47 | 48 | // defaultFlags specifies changes to the default logger behavior. It is set 49 | // during package init and configured using the LOGFLAGS environment variable. 50 | // New logger backends can override these default flags using WithFlags. 51 | var defaultFlags uint32 52 | 53 | // Flags to modify Backend's behavior. 54 | const ( 55 | // Llongfile modifies the logger output to include full path and line number 56 | // of the logging callsite, e.g. /a/b/c/main.go:123. 57 | Llongfile uint32 = 1 << iota 58 | 59 | // Lshortfile modifies the logger output to include filename and line number 60 | // of the logging callsite, e.g. main.go:123. Overrides Llongfile. 61 | Lshortfile 62 | ) 63 | 64 | // Read logger flags from the LOGFLAGS environment variable. Multiple flags can 65 | // be set at once, separated by commas. 66 | func init() { 67 | for _, f := range strings.Split(os.Getenv("LOGFLAGS"), ",") { 68 | switch f { 69 | case "longfile": 70 | defaultFlags |= Llongfile 71 | case "shortfile": 72 | defaultFlags |= Lshortfile 73 | } 74 | } 75 | } 76 | 77 | // Level is the level at which a logger is configured. All messages sent 78 | // to a level which is below the current level are filtered. 79 | type Level uint32 80 | 81 | // Level constants. 82 | const ( 83 | LevelTrace Level = iota 84 | LevelDebug 85 | LevelInfo 86 | LevelWarn 87 | LevelError 88 | LevelCritical 89 | LevelOff 90 | ) 91 | 92 | // levelStrs defines the human-readable names for each logging level. 93 | var levelStrs = [...]string{"TRC", "DBG", "INF", "WRN", "ERR", "CRT", "OFF"} 94 | 95 | // LevelFromString returns a level based on the input string s. If the input 96 | // can't be interpreted as a valid log level, the info level and false is 97 | // returned. 98 | func LevelFromString(s string) (l Level, ok bool) { 99 | switch strings.ToLower(s) { 100 | case "trace", "trc": 101 | return LevelTrace, true 102 | case "debug", "dbg": 103 | return LevelDebug, true 104 | case "info", "inf": 105 | return LevelInfo, true 106 | case "warn", "wrn": 107 | return LevelWarn, true 108 | case "error", "err": 109 | return LevelError, true 110 | case "critical", "crt": 111 | return LevelCritical, true 112 | case "off": 113 | return LevelOff, true 114 | default: 115 | return LevelInfo, false 116 | } 117 | } 118 | 119 | // String returns the tag of the logger used in log messages, or "OFF" if 120 | // the level will not produce any log output. 121 | func (l Level) String() string { 122 | if l >= LevelOff { 123 | return "OFF" 124 | } 125 | return levelStrs[l] 126 | } 127 | 128 | // NewBackend creates a logger backend from a Writer. 129 | func NewBackend(w io.Writer, opts ...BackendOption) *Backend { 130 | b := &Backend{w: w, flag: defaultFlags} 131 | for _, o := range opts { 132 | o(b) 133 | } 134 | return b 135 | } 136 | 137 | // Backend is a logging backend. Subsystems created from the backend write to 138 | // the backend's Writer. Backend provides atomic writes to the Writer from all 139 | // subsystems. 140 | type Backend struct { 141 | w io.Writer 142 | mu sync.Mutex // ensures atomic writes 143 | flag uint32 144 | } 145 | 146 | // BackendOption is a function used to modify the behavior of a Backend. 147 | type BackendOption func(b *Backend) 148 | 149 | // WithFlags configures a Backend to use the specified flags rather than using 150 | // the package's defaults as determined through the LOGFLAGS environment 151 | // variable. 152 | func WithFlags(flags uint32) BackendOption { 153 | return func(b *Backend) { 154 | b.flag = flags 155 | } 156 | } 157 | 158 | // bufferPool defines a concurrent safe free list of byte slices used to provide 159 | // temporary buffers for formatting log messages prior to outputting them. 160 | var bufferPool = sync.Pool{ 161 | New: func() interface{} { 162 | b := make([]byte, 0, 120) 163 | return &b // pointer to slice to avoid boxing alloc 164 | }, 165 | } 166 | 167 | // buffer returns a byte slice from the free list. A new buffer is allocated if 168 | // there are not any available on the free list. The returned byte slice should 169 | // be returned to the fee list by using the recycleBuffer function when the 170 | // caller is done with it. 171 | func buffer() *[]byte { 172 | return bufferPool.Get().(*[]byte) 173 | } 174 | 175 | // recycleBuffer puts the provided byte slice, which should have been obtain via 176 | // the buffer function, back on the free list. 177 | func recycleBuffer(b *[]byte) { 178 | *b = (*b)[:0] 179 | bufferPool.Put(b) 180 | } 181 | 182 | // From stdlib log package. 183 | // Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid 184 | // zero-padding. 185 | func itoa(buf *[]byte, i int, wid int) { 186 | // Assemble decimal in reverse order. 187 | var b [20]byte 188 | bp := len(b) - 1 189 | for i >= 10 || wid > 1 { 190 | wid-- 191 | q := i / 10 192 | b[bp] = byte('0' + i - q*10) 193 | bp-- 194 | i = q 195 | } 196 | // i < 10 197 | b[bp] = byte('0' + i) 198 | *buf = append(*buf, b[bp:]...) 199 | } 200 | 201 | // Appends a header in the default format 'YYYY-MM-DD hh:mm:ss.sss [LVL] TAG: '. 202 | // If either of the Lshortfile or Llongfile flags are specified, the file named 203 | // and line number are included after the tag and before the final colon. 204 | func formatHeader(buf *[]byte, t time.Time, lvl, tag string, file string, line int) { 205 | year, month, day := t.Date() 206 | hour, min, sec := t.Clock() 207 | ms := t.Nanosecond() / 1e6 208 | 209 | itoa(buf, year, 4) 210 | *buf = append(*buf, '-') 211 | itoa(buf, int(month), 2) 212 | *buf = append(*buf, '-') 213 | itoa(buf, day, 2) 214 | *buf = append(*buf, ' ') 215 | itoa(buf, hour, 2) 216 | *buf = append(*buf, ':') 217 | itoa(buf, min, 2) 218 | *buf = append(*buf, ':') 219 | itoa(buf, sec, 2) 220 | *buf = append(*buf, '.') 221 | itoa(buf, ms, 3) 222 | *buf = append(*buf, " ["...) 223 | *buf = append(*buf, lvl...) 224 | *buf = append(*buf, "] "...) 225 | *buf = append(*buf, tag...) 226 | if file != "" { 227 | *buf = append(*buf, ' ') 228 | *buf = append(*buf, file...) 229 | *buf = append(*buf, ':') 230 | itoa(buf, line, -1) 231 | } 232 | *buf = append(*buf, ": "...) 233 | } 234 | 235 | // calldepth is the call depth of the callsite function relative to the 236 | // caller of the subsystem logger. It is used to recover the filename and line 237 | // number of the logging call if either the short or long file flags are 238 | // specified. 239 | const calldepth = 3 240 | 241 | // callsite returns the file name and line number of the callsite to the 242 | // subsystem logger. 243 | func callsite(flag uint32) (string, int) { 244 | _, file, line, ok := runtime.Caller(calldepth) 245 | if !ok { 246 | return "???", 0 247 | } 248 | if flag&Lshortfile != 0 { 249 | short := file 250 | for i := len(file) - 1; i > 0; i-- { 251 | if os.IsPathSeparator(file[i]) { 252 | short = file[i+1:] 253 | break 254 | } 255 | } 256 | file = short 257 | } 258 | return file, line 259 | } 260 | 261 | // print outputs a log message to the writer associated with the backend after 262 | // creating a prefix for the given level and tag according to the formatHeader 263 | // function and formatting the provided arguments using the default formatting 264 | // rules. 265 | func (b *Backend) print(lvl, tag string, args ...interface{}) { 266 | t := time.Now() // get as early as possible 267 | 268 | bytebuf := buffer() 269 | 270 | var file string 271 | var line int 272 | if b.flag&(Lshortfile|Llongfile) != 0 { 273 | file, line = callsite(b.flag) 274 | } 275 | 276 | formatHeader(bytebuf, t, lvl, tag, file, line) 277 | buf := bytes.NewBuffer(*bytebuf) 278 | fmt.Fprintln(buf, args...) 279 | *bytebuf = buf.Bytes() 280 | 281 | b.mu.Lock() 282 | b.w.Write(*bytebuf) 283 | b.mu.Unlock() 284 | 285 | recycleBuffer(bytebuf) 286 | } 287 | 288 | // printf outputs a log message to the writer associated with the backend after 289 | // creating a prefix for the given level and tag according to the formatHeader 290 | // function and formatting the provided arguments according to the given format 291 | // specifier. 292 | func (b *Backend) printf(lvl, tag string, format string, args ...interface{}) { 293 | t := time.Now() // get as early as possible 294 | 295 | bytebuf := buffer() 296 | 297 | var file string 298 | var line int 299 | if b.flag&(Lshortfile|Llongfile) != 0 { 300 | file, line = callsite(b.flag) 301 | } 302 | 303 | formatHeader(bytebuf, t, lvl, tag, file, line) 304 | buf := bytes.NewBuffer(*bytebuf) 305 | fmt.Fprintf(buf, format, args...) 306 | *bytebuf = append(buf.Bytes(), '\n') 307 | 308 | b.mu.Lock() 309 | b.w.Write(*bytebuf) 310 | b.mu.Unlock() 311 | 312 | recycleBuffer(bytebuf) 313 | } 314 | 315 | // Logger returns a new logger for a particular subsystem that writes to the 316 | // Backend b. A tag describes the subsystem and is included in all log 317 | // messages. The logger uses the info verbosity level by default. 318 | func (b *Backend) Logger(subsystemTag string) Logger { 319 | return &slog{LevelInfo, subsystemTag, b} 320 | } 321 | 322 | // slog is a subsystem logger for a Backend. Implements the Logger interface. 323 | type slog struct { 324 | lvl Level // atomic 325 | tag string 326 | b *Backend 327 | } 328 | 329 | // Trace formats message using the default formats for its operands, prepends 330 | // the prefix as necessary, and writes to log with LevelTrace. 331 | // 332 | // This is part of the Logger interface implementation. 333 | func (l *slog) Trace(args ...interface{}) { 334 | lvl := l.Level() 335 | if lvl <= LevelTrace { 336 | l.b.print("TRC", l.tag, args...) 337 | } 338 | } 339 | 340 | // Tracef formats message according to format specifier, prepends the prefix as 341 | // necessary, and writes to log with LevelTrace. 342 | // 343 | // This is part of the Logger interface implementation. 344 | func (l *slog) Tracef(format string, args ...interface{}) { 345 | lvl := l.Level() 346 | if lvl <= LevelTrace { 347 | l.b.printf("TRC", l.tag, format, args...) 348 | } 349 | } 350 | 351 | // Debug formats message using the default formats for its operands, prepends 352 | // the prefix as necessary, and writes to log with LevelDebug. 353 | // 354 | // This is part of the Logger interface implementation. 355 | func (l *slog) Debug(args ...interface{}) { 356 | lvl := l.Level() 357 | if lvl <= LevelDebug { 358 | l.b.print("DBG", l.tag, args...) 359 | } 360 | } 361 | 362 | // Debugf formats message according to format specifier, prepends the prefix as 363 | // necessary, and writes to log with LevelDebug. 364 | // 365 | // This is part of the Logger interface implementation. 366 | func (l *slog) Debugf(format string, args ...interface{}) { 367 | lvl := l.Level() 368 | if lvl <= LevelDebug { 369 | l.b.printf("DBG", l.tag, format, args...) 370 | } 371 | } 372 | 373 | // Info formats message using the default formats for its operands, prepends 374 | // the prefix as necessary, and writes to log with LevelInfo. 375 | // 376 | // This is part of the Logger interface implementation. 377 | func (l *slog) Info(args ...interface{}) { 378 | lvl := l.Level() 379 | if lvl <= LevelInfo { 380 | l.b.print("INF", l.tag, args...) 381 | } 382 | } 383 | 384 | // Infof formats message according to format specifier, prepends the prefix as 385 | // necessary, and writes to log with LevelInfo. 386 | // 387 | // This is part of the Logger interface implementation. 388 | func (l *slog) Infof(format string, args ...interface{}) { 389 | lvl := l.Level() 390 | if lvl <= LevelInfo { 391 | l.b.printf("INF", l.tag, format, args...) 392 | } 393 | } 394 | 395 | // Warn formats message using the default formats for its operands, prepends 396 | // the prefix as necessary, and writes to log with LevelWarn. 397 | // 398 | // This is part of the Logger interface implementation. 399 | func (l *slog) Warn(args ...interface{}) { 400 | lvl := l.Level() 401 | if lvl <= LevelWarn { 402 | l.b.print("WRN", l.tag, args...) 403 | } 404 | } 405 | 406 | // Warnf formats message according to format specifier, prepends the prefix as 407 | // necessary, and writes to log with LevelWarn. 408 | // 409 | // This is part of the Logger interface implementation. 410 | func (l *slog) Warnf(format string, args ...interface{}) { 411 | lvl := l.Level() 412 | if lvl <= LevelWarn { 413 | l.b.printf("WRN", l.tag, format, args...) 414 | } 415 | } 416 | 417 | // Error formats message using the default formats for its operands, prepends 418 | // the prefix as necessary, and writes to log with LevelError. 419 | // 420 | // This is part of the Logger interface implementation. 421 | func (l *slog) Error(args ...interface{}) { 422 | lvl := l.Level() 423 | if lvl <= LevelError { 424 | l.b.print("ERR", l.tag, args...) 425 | } 426 | } 427 | 428 | // Errorf formats message according to format specifier, prepends the prefix as 429 | // necessary, and writes to log with LevelError. 430 | // 431 | // This is part of the Logger interface implementation. 432 | func (l *slog) Errorf(format string, args ...interface{}) { 433 | lvl := l.Level() 434 | if lvl <= LevelError { 435 | l.b.printf("ERR", l.tag, format, args...) 436 | } 437 | } 438 | 439 | // Critical formats message using the default formats for its operands, prepends 440 | // the prefix as necessary, and writes to log with LevelCritical. 441 | // 442 | // This is part of the Logger interface implementation. 443 | func (l *slog) Critical(args ...interface{}) { 444 | lvl := l.Level() 445 | if lvl <= LevelCritical { 446 | l.b.print("CRT", l.tag, args...) 447 | } 448 | } 449 | 450 | // Criticalf formats message according to format specifier, prepends the prefix 451 | // as necessary, and writes to log with LevelCritical. 452 | // 453 | // This is part of the Logger interface implementation. 454 | func (l *slog) Criticalf(format string, args ...interface{}) { 455 | lvl := l.Level() 456 | if lvl <= LevelCritical { 457 | l.b.printf("CRT", l.tag, format, args...) 458 | } 459 | } 460 | 461 | // Level returns the current logging level 462 | // 463 | // This is part of the Logger interface implementation. 464 | func (l *slog) Level() Level { 465 | return Level(atomic.LoadUint32((*uint32)(&l.lvl))) 466 | } 467 | 468 | // SetLevel changes the logging level to the passed level. 469 | // 470 | // This is part of the Logger interface implementation. 471 | func (l *slog) SetLevel(level Level) { 472 | atomic.StoreUint32((*uint32)(&l.lvl), uint32(level)) 473 | } 474 | 475 | // Disabled is a Logger that will never output anything. 476 | var Disabled Logger 477 | 478 | func init() { 479 | Disabled = &slog{lvl: LevelOff, b: NewBackend(ioutil.Discard)} 480 | } 481 | -------------------------------------------------------------------------------- /v2/LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2013-2014 Conformal Systems LLC. 4 | Copyright (c) 2024 The btcsuite developers. 5 | 6 | Permission to use, copy, modify, and distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /v2/attrs.go: -------------------------------------------------------------------------------- 1 | package btclog 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "log/slog" 7 | ) 8 | 9 | // Hex is a convenience function for hex-encoded log attributes. 10 | func Hex(key string, value []byte) slog.Attr { 11 | return slog.String(key, hex.EncodeToString(value)) 12 | } 13 | 14 | // Hex6 is a convenience function for hex-encoded log attributes which prints 15 | // a maximum of 6 bytes. 16 | func Hex6(key string, value []byte) slog.Attr { 17 | return HexN(key, value, 6) 18 | } 19 | 20 | // Hex3 is a convenience function for hex-encoded log attributes which prints 21 | // a maximum of 3 bytes. 22 | func Hex3(key string, value []byte) slog.Attr { 23 | return HexN(key, value, 3) 24 | } 25 | 26 | // Hex2 is a convenience function for hex-encoded log attributes which prints 27 | // a maximum of 2 bytes. 28 | func Hex2(key string, value []byte) slog.Attr { 29 | return HexN(key, value, 2) 30 | } 31 | 32 | // HexN is a convenience function for hex-encoded log attributes which prints 33 | // a maximum of n bytes. 34 | func HexN(key string, value []byte, n uint) slog.Attr { 35 | if len(value) <= int(n) { 36 | return slog.String(key, hex.EncodeToString(value)) 37 | } 38 | 39 | return slog.String(key, hex.EncodeToString(value[:n])) 40 | } 41 | 42 | // Fmt returns a slog.Attr with the formatted message which is only computed 43 | // when needed. 44 | // 45 | // Example usage: 46 | // 47 | // log.InfoS(ctx, "Main message", Fmt("key", "%.12f", 3.241)) 48 | func Fmt(key string, msg string, params ...any) slog.Attr { 49 | return slog.Any(key, Sprintf(msg, params...)) 50 | } 51 | 52 | // ClosureAttr returns an slog attribute that will only perform the given 53 | // logging operation if the corresponding log level is enabled. 54 | // 55 | // Example usage: 56 | // 57 | // log.InfoS(ctx, "msg", ClosureAttr("key", func() string { 58 | // // Replace with an expensive string computation call. 59 | // return "expensive string" 60 | // })) 61 | func ClosureAttr(key string, compute func() string) slog.Attr { 62 | return slog.Any(key, NewClosure(compute)) 63 | } 64 | 65 | type attrsKey struct{} 66 | 67 | // WithCtx returns a copy of the context with which the logging attributes are 68 | // associated. 69 | // 70 | // Usage: 71 | // 72 | // unusedCtx := log.WithCtx(unusedCtx, "height", 1234) 73 | // ... 74 | // log.InfoS(unusedCtx, "Height processed") // Will contain attribute: height=1234 75 | func WithCtx(ctx context.Context, attrs ...any) context.Context { 76 | return context.WithValue(ctx, attrsKey{}, mergeAttrs(ctx, attrs)) 77 | } 78 | 79 | // mergeAttrs returns the attributes from the context merged with the provided 80 | // attributes. 81 | func mergeAttrs(ctx context.Context, attrs []any) []any { 82 | resp, _ := ctx.Value(attrsKey{}).([]any) // We know the type. 83 | resp = append(resp, attrs...) 84 | 85 | return resp 86 | } 87 | -------------------------------------------------------------------------------- /v2/buffer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // Copyright 2009 The Go Authors. 6 | // 7 | // Redistribution and use in source and binary forms, with or without 8 | // modification, are permitted provided that the following conditions are 9 | // met: 10 | // 11 | // * Redistributions of source code must retain the above copyright 12 | // notice, this list of conditions and the following disclaimer. 13 | // * Redistributions in binary form must reproduce the above 14 | // copyright notice, this list of conditions and the following disclaimer 15 | // in the documentation and/or other materials provided with the 16 | // distribution. 17 | // * Neither the name of Google LLC nor the names of its 18 | // contributors may be used to endorse or promote products derived from 19 | // this software without specific prior written permission. 20 | // 21 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | // Adapted from go/src/log/slog/internal/buffer.go 34 | 35 | package btclog 36 | 37 | import "sync" 38 | 39 | type buffer []byte 40 | 41 | // bufferPool defines a concurrent safe free list of byte slices used to provide 42 | // temporary buffers for formatting log messages prior to outputting them. 43 | var bufferPool = sync.Pool{ 44 | New: func() any { 45 | b := make([]byte, 0, 1024) 46 | return (*buffer)(&b) 47 | }, 48 | } 49 | 50 | // newBuffer returns a byte slice from the free list. A new buffer is allocated 51 | // if there are not any available on the free list. The returned byte slice 52 | // should be returned to the fee list by using the recycleBuffer function when 53 | // the caller is done with it. 54 | func newBuffer() *buffer { 55 | return bufferPool.Get().(*buffer) 56 | } 57 | 58 | // free puts the provided byte slice, which should have been obtained via the 59 | // newBuffer function, back on the free list. 60 | func (b *buffer) free() { 61 | // To reduce peak allocation, return only smaller buffers to the pool. 62 | const maxBufferSize = 16 << 10 63 | if cap(*b) <= maxBufferSize { 64 | *b = (*b)[:0] 65 | bufferPool.Put(b) 66 | } 67 | } 68 | 69 | func (b *buffer) writeByte(p byte) { 70 | *b = append(*b, p) 71 | } 72 | 73 | func (b *buffer) writeBytes(p []byte) { 74 | *b = append(*b, p...) 75 | } 76 | 77 | func (b *buffer) writeString(s string) { 78 | *b = append(*b, s...) 79 | } 80 | -------------------------------------------------------------------------------- /v2/closure.go: -------------------------------------------------------------------------------- 1 | package btclog 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Closure is used to provide a closure over expensive logging operations so 8 | // that they don't have to be performed when the logging level doesn't warrant 9 | // it. 10 | type Closure func() string 11 | 12 | // String invokes the underlying function and returns the result. 13 | func (c Closure) String() string { 14 | return c() 15 | } 16 | 17 | // NewClosure returns a new closure over a function that returns a string 18 | // which itself provides a Stringer interface so that it can be used with the 19 | // logging system. 20 | func NewClosure(compute func() string) Closure { 21 | return compute 22 | } 23 | 24 | // Sprintf returns a fmt.Stringer that will lazily compute the string when 25 | // needed. This is useful when the string is expensive to compute and may not be 26 | // needed due to the log level being used. 27 | // 28 | // Example usage: 29 | // 30 | // log.InfoS(ctx, "msg", "key", Sprintf("%.12f", 3.241)) 31 | func Sprintf(msg string, params ...any) fmt.Stringer { 32 | return NewClosure(func() string { 33 | return fmt.Sprintf(msg, params...) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/btcsuite/btclog/v2 2 | 3 | require github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c 4 | 5 | go 1.21 6 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzgaNgrCPsFWd7cGYNpeFUgd9ZIgyM0= 2 | github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= 3 | -------------------------------------------------------------------------------- /v2/handler.go: -------------------------------------------------------------------------------- 1 | package btclog 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "strconv" 9 | "sync" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/btcsuite/btclog" 14 | ) 15 | 16 | // DefaultSkipDepth is the default number of stack frames to ascend when 17 | // determining the call site of a log. Users of this package may want to alter 18 | // this depth depending on if they wrap the logger at all. 19 | const DefaultSkipDepth = 5 20 | 21 | // HandlerOption is the signature of a functional option that can be used to 22 | // modify the behaviour of the DefaultHandler. 23 | type HandlerOption func(*handlerOpts) 24 | 25 | // handlerOpts holds options that can be modified by a HandlerOption. 26 | type handlerOpts struct { 27 | // flag is a bit vector that alters certain behaviours of the logger. 28 | // Currently, it is only used to alter how the call site will be logged. 29 | flag uint32 30 | 31 | // withTimestamp defines whether the logs timestamp should be included 32 | // in the log line. 33 | withTimestamp bool 34 | 35 | // timeSource is used to obtain the timestamp for a log line, if not 36 | // set then the slog packages provided timestamp will be used. 37 | timeSource func() time.Time 38 | 39 | // callSiteSkipDepth is the number of stack frames to ascend when 40 | // determining the call site of a log. Users of this package may want 41 | // to alter this depth depending on if they wrap the logger at all. 42 | callSiteSkipDepth int 43 | 44 | // styledLevel is a call-back that can be used to determine how the log 45 | // level will appear when printed. 46 | styledLevel func(btclog.Level) string 47 | 48 | // styledCallSite is a call-back that can be used to determine how the 49 | // call-site will appear when printed. 50 | styledCallSite func(string, int) string 51 | 52 | // styledKey is a call-back that can be used to determine how any key 53 | // in an attributes key-value pair will appear when printed. 54 | styledKey func(string) string 55 | } 56 | 57 | // defaultHandlerOpts constructs a handlerOpts with default settings. 58 | func defaultHandlerOpts() *handlerOpts { 59 | return &handlerOpts{ 60 | flag: defaultFlags, 61 | withTimestamp: true, 62 | callSiteSkipDepth: DefaultSkipDepth, 63 | } 64 | } 65 | 66 | // WithCallerFlags can be used to overwrite the default caller flag option. 67 | func WithCallerFlags(flags uint32) HandlerOption { 68 | return func(opts *handlerOpts) { 69 | opts.flag = flags 70 | } 71 | } 72 | 73 | // WithTimeSource can be used to overwrite the time sourced from the slog 74 | // Record. 75 | func WithTimeSource(fn func() time.Time) HandlerOption { 76 | return func(opts *handlerOpts) { 77 | opts.timeSource = fn 78 | } 79 | } 80 | 81 | // WithCallSiteSkipDepth can be used to set the call-site skip depth. 82 | func WithCallSiteSkipDepth(depth int) HandlerOption { 83 | return func(opts *handlerOpts) { 84 | opts.callSiteSkipDepth = depth 85 | } 86 | } 87 | 88 | // WithStyledLevel can be used adjust the level string before it is printed. 89 | func WithStyledLevel(fn func(btclog.Level) string) HandlerOption { 90 | return func(opts *handlerOpts) { 91 | opts.styledLevel = fn 92 | } 93 | } 94 | 95 | // WithStyledCallSite can be used adjust the call-site string before it is 96 | // printed. 97 | func WithStyledCallSite(fn func(file string, line int) string) HandlerOption { 98 | return func(opts *handlerOpts) { 99 | opts.styledCallSite = fn 100 | } 101 | } 102 | 103 | // WithStyledKeys can be used adjust the key strings for any key-value 104 | // attribute pair. 105 | func WithStyledKeys(fn func(string) string) HandlerOption { 106 | return func(opts *handlerOpts) { 107 | opts.styledKey = fn 108 | } 109 | } 110 | 111 | // WithNoTimestamp is an option that can be used to omit timestamps from the log 112 | // lines. 113 | func WithNoTimestamp() HandlerOption { 114 | return func(opts *handlerOpts) { 115 | opts.withTimestamp = false 116 | } 117 | } 118 | 119 | // DefaultHandler is a Handler that can be used along with NewSLogger to 120 | // instantiate a structured logger. 121 | type DefaultHandler struct { 122 | level *atomic.Int64 123 | 124 | opts *handlerOpts 125 | buf *buffer 126 | mu *sync.Mutex 127 | w io.Writer 128 | 129 | tag string 130 | prefix string 131 | 132 | fields []slog.Attr 133 | 134 | flag uint32 135 | callstackOffset bool 136 | } 137 | 138 | // A compile-time check to ensure that DefaultHandler implements Handler. 139 | var _ Handler = (*DefaultHandler)(nil) 140 | 141 | // Level returns the current logging level of the Handler. 142 | // 143 | // NOTE: This is part of the Handler interface. 144 | func (d *DefaultHandler) Level() btclog.Level { 145 | return fromSlogLevel(slog.Level(d.level.Load())) 146 | } 147 | 148 | // SetLevel changes the logging level of the Handler to the passed 149 | // level. 150 | // 151 | // NOTE: This is part of the Handler interface. 152 | func (d *DefaultHandler) SetLevel(level btclog.Level) { 153 | d.level.Store(int64(toSlogLevel(level))) 154 | } 155 | 156 | // NewDefaultHandler creates a new Handler that can be used along with 157 | // NewSLogger to instantiate a structured logger. 158 | func NewDefaultHandler(w io.Writer, options ...HandlerOption) *DefaultHandler { 159 | opts := defaultHandlerOpts() 160 | for _, o := range options { 161 | o(opts) 162 | } 163 | 164 | handler := &DefaultHandler{ 165 | w: w, 166 | opts: opts, 167 | buf: newBuffer(), 168 | mu: &sync.Mutex{}, 169 | level: &atomic.Int64{}, 170 | } 171 | handler.level.Store(int64(levelInfo)) 172 | 173 | return handler 174 | } 175 | 176 | // Enabled reports whether the handler handles records at the given level. 177 | // 178 | // NOTE: this is part of the slog.Handler interface. 179 | func (d *DefaultHandler) Enabled(_ context.Context, level slog.Level) bool { 180 | return d.level.Load() <= int64(level) 181 | } 182 | 183 | // Handle handles the Record. It will only be called if Enabled returns true. 184 | // 185 | // NOTE: this is part of the slog.Handler interface. 186 | func (d *DefaultHandler) Handle(_ context.Context, r slog.Record) error { 187 | buf := newBuffer() 188 | defer buf.free() 189 | 190 | // Timestamp. 191 | if d.opts.withTimestamp { 192 | // First check if the options provided specified a different 193 | // time source to use. Otherwise, use the provided record time. 194 | if d.opts.timeSource != nil { 195 | writeTimestamp(buf, d.opts.timeSource()) 196 | } else if !r.Time.IsZero() { 197 | writeTimestamp(buf, r.Time) 198 | } 199 | } 200 | 201 | // Level. 202 | d.writeLevel(buf, r.Level) 203 | 204 | // Sub-system tag. 205 | if d.tag != "" { 206 | buf.writeString(" " + d.tag) 207 | } 208 | 209 | // The call-site. 210 | skipBase := d.opts.callSiteSkipDepth 211 | if d.opts.flag&(Lshortfile|Llongfile) != 0 { 212 | skip := skipBase 213 | if d.callstackOffset && skip >= 2 { 214 | skip -= 2 215 | } 216 | file, line := callsite(d.opts.flag, skip) 217 | d.writeCallSite(buf, file, line) 218 | } 219 | 220 | // Finish off the header. 221 | buf.writeByte(':') 222 | buf.writeByte(' ') 223 | 224 | // Maybe write a prefix if one has been specified. 225 | if d.prefix != "" { 226 | buf.writeString(d.prefix) 227 | 228 | if r.Message != "" { 229 | buf.writeByte(' ') 230 | } 231 | } 232 | 233 | // Write the log message itself. 234 | if r.Message != "" { 235 | buf.writeString(r.Message) 236 | } 237 | 238 | // Append logger fields. 239 | for _, attr := range d.fields { 240 | d.appendAttr(buf, attr) 241 | } 242 | 243 | // Append slog attributes. 244 | r.Attrs(func(a slog.Attr) bool { 245 | d.appendAttr(buf, a) 246 | return true 247 | }) 248 | buf.writeByte('\n') 249 | 250 | d.mu.Lock() 251 | defer d.mu.Unlock() 252 | _, err := d.w.Write(*buf) 253 | 254 | return err 255 | } 256 | 257 | // WithAttrs returns a new Handler with the given attributes added. 258 | // 259 | // NOTE: this is part of the slog.Handler interface. 260 | func (d *DefaultHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 261 | return d.with(d.tag, d.prefix, true, false, attrs...) 262 | } 263 | 264 | // WithGroup returns a new Handler with the given group appended to 265 | // the receiver's existing groups. All this implementation does is that it adds 266 | // to the existing tag used for the logger. 267 | // 268 | // NOTE: this is part of the slog.Handler interface. 269 | func (d *DefaultHandler) WithGroup(name string) slog.Handler { 270 | if d.tag != "" { 271 | name = d.tag + "." + name 272 | } 273 | return d.with(name, d.prefix, true, false) 274 | } 275 | 276 | // SubSystem returns a copy of the given handler but with the new tag. All 277 | // attributes added with WithAttrs will be kept but all groups added with 278 | // WithGroup are lost. 279 | // 280 | // NOTE: this creates a new logger with an independent log level. This 281 | // means that SetLevel needs to be called on the new logger to change 282 | // the level as any changes to the parent logger's level after creation 283 | // will not be inherited by the new logger. 284 | // 285 | // NOTE: this is part of the Handler interface. 286 | func (d *DefaultHandler) SubSystem(tag string) Handler { 287 | return d.with(tag, d.prefix, false, false) 288 | } 289 | 290 | // WithPrefix returns a copy of the Handler but with the given string prefixed 291 | // to each log message. Note that the subsystem of the original logger is kept 292 | // but any existing prefix is overridden. 293 | // 294 | // NOTE: this creates a new logger with an inherited log level. This 295 | // means that if SetLevel is called on the parent logger, then this new 296 | // level will be inherited by the new logger 297 | // 298 | // NOTE: this is part of the Handler interface. 299 | func (d *DefaultHandler) WithPrefix(prefix string) Handler { 300 | return d.with(d.tag, prefix, false, true) 301 | } 302 | 303 | // with returns a new logger with the given attributes added. 304 | // withCallstackOffset should be false if the caller returns a concrete 305 | // DefaultHandler and true if the caller returns the Handler interface. 306 | // The shareLevel param determines whether the new handler shares the same 307 | // level reference or gets its own independent level. 308 | func (d *DefaultHandler) with(tag, prefix string, withCallstackOffset bool, 309 | shareLevel bool, attrs ...slog.Attr) *DefaultHandler { 310 | 311 | d.mu.Lock() 312 | sl := *d 313 | d.mu.Unlock() 314 | sl.buf = newBuffer() 315 | 316 | sl.mu = &sync.Mutex{} 317 | sl.fields = append( 318 | make([]slog.Attr, 0, len(d.fields)+len(attrs)), d.fields..., 319 | ) 320 | sl.fields = append(sl.fields, attrs...) 321 | sl.callstackOffset = withCallstackOffset 322 | sl.tag = tag 323 | sl.prefix = prefix 324 | 325 | // If shareLevel is false, create a new independent level. Otherwise, 326 | // sl.level already points to d.level. 327 | if !shareLevel { 328 | newLevel := &atomic.Int64{} 329 | newLevel.Store(d.level.Load()) 330 | sl.level = newLevel 331 | } 332 | 333 | return &sl 334 | } 335 | 336 | // appendAttr extracts a key-value pair from the slog.Attr and writes it to the 337 | // buffer. 338 | func (d *DefaultHandler) appendAttr(buf *buffer, a slog.Attr) { 339 | // Resolve the Attr's value before doing anything else. 340 | a.Value = a.Value.Resolve() 341 | 342 | // Ignore empty Attrs. 343 | if a.Equal(slog.Attr{}) { 344 | return 345 | } 346 | 347 | d.appendKey(buf, a.Key) 348 | appendValue(buf, a.Value) 349 | } 350 | 351 | // writeLevel writes the given slog.Level to the buffer in its string form. 352 | func (d *DefaultHandler) writeLevel(buf *buffer, level slog.Level) { 353 | lvl := fromSlogLevel(level) 354 | if d.opts.styledLevel != nil { 355 | buf.writeString(d.opts.styledLevel(fromSlogLevel(level))) 356 | 357 | return 358 | } 359 | 360 | buf.writeByte('[') 361 | buf.writeString(lvl.String()) 362 | buf.writeByte(']') 363 | } 364 | 365 | // writeCallSite writes the given file path and line number to the buffer as a 366 | // string. 367 | func (d *DefaultHandler) writeCallSite(buf *buffer, file string, line int) { 368 | if file == "" { 369 | return 370 | } 371 | buf.writeByte(' ') 372 | 373 | if d.opts.styledCallSite != nil { 374 | buf.writeString(d.opts.styledCallSite(file, line)) 375 | 376 | return 377 | } 378 | 379 | *buf = append(*buf, file...) 380 | buf.writeByte(':') 381 | itoa(buf, line, -1) 382 | } 383 | 384 | // appendString writes the given string to the buffer. It may wrap the string in 385 | // quotes. 386 | func appendString(buf *buffer, str string) { 387 | if needsQuoting(str) { 388 | *buf = strconv.AppendQuote(*buf, str) 389 | } else { 390 | buf.writeString(str) 391 | } 392 | } 393 | 394 | // appendKey writes the given key string to the buffer along with an `=` 395 | // character. This is generally useful before calling appendValue. 396 | func (d *DefaultHandler) appendKey(buf *buffer, key string) { 397 | buf.writeByte(' ') 398 | if needsQuoting(key) { 399 | key = strconv.Quote(key) 400 | } 401 | key += "=" 402 | 403 | if d.opts.styledKey != nil { 404 | buf.writeString(d.opts.styledKey(key)) 405 | 406 | return 407 | } 408 | 409 | buf.writeString(key) 410 | } 411 | 412 | // appendValue writes the given slog.Value to the buffer. 413 | func appendValue(buf *buffer, v slog.Value) { 414 | defer func() { 415 | // Recovery in case of nil pointer dereferences. 416 | if r := recover(); r != nil { 417 | // Catch any panics that are most likely due to nil 418 | // pointers. 419 | appendString(buf, fmt.Sprintf("!PANIC: %v", r)) 420 | } 421 | }() 422 | 423 | appendTextValue(buf, v) 424 | } 425 | 426 | // appendTextValue writes the given slog.Value to the buffer. It attempts to 427 | // choose the most appropriate formatting for the Value type. 428 | func appendTextValue(buf *buffer, v slog.Value) { 429 | switch v.Kind() { 430 | case slog.KindString: 431 | appendString(buf, v.String()) 432 | case slog.KindAny: 433 | appendString(buf, fmt.Sprintf("%+v", v.Any())) 434 | default: 435 | appendString(buf, fmt.Sprintf("%s", v)) 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /v2/handler_test.go: -------------------------------------------------------------------------------- 1 | package btclog 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "testing" 9 | "time" 10 | 11 | "github.com/btcsuite/btclog" 12 | ) 13 | 14 | // TestDefaultHandler tests that the DefaultHandler's output looks as expected. 15 | func TestDefaultHandler(t *testing.T) { 16 | t.Parallel() 17 | 18 | for _, test := range tests { 19 | t.Run(test.name, func(t *testing.T) { 20 | var buf bytes.Buffer 21 | handler := NewDefaultHandler(&buf, test.handlerOpts...) 22 | logger := NewSLogger(handler) 23 | logger.SetLevel(test.level) 24 | 25 | if handler.Level() != test.level { 26 | t.Fatalf("Incorrect level. Expected %s, "+ 27 | "got %s", test.level, handler.Level()) 28 | } 29 | 30 | test.logFunc(t, logger) 31 | 32 | if buf.String() != test.expectedLog { 33 | t.Fatalf("Log result mismatch. Expected "+ 34 | "\n\"%s\", got \n\"%s\"", 35 | test.expectedLog, buf.Bytes()) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | var timeSource = func() time.Time { 42 | return time.Date(2009, time.January, 3, 12, 0, 0, 0, time.UTC) 43 | } 44 | 45 | var tests = []struct { 46 | name string 47 | handlerOpts []HandlerOption 48 | level btclog.Level 49 | logFunc func(t *testing.T, log Logger) 50 | expectedLog string 51 | }{ 52 | { 53 | name: "Basic calls and levels", 54 | handlerOpts: []HandlerOption{WithTimeSource(timeSource)}, 55 | level: LevelDebug, 56 | logFunc: func(t *testing.T, log Logger) { 57 | log.Info("Test Basic Log") 58 | log.Debugf("Test basic log with %s", "format") 59 | log.Trace("Log should not appear due to level") 60 | }, 61 | expectedLog: `2009-01-03 12:00:00.000 [INF]: Test Basic Log 62 | 2009-01-03 12:00:00.000 [DBG]: Test basic log with format 63 | `, 64 | }, 65 | { 66 | name: "Call site", 67 | handlerOpts: []HandlerOption{ 68 | WithNoTimestamp(), 69 | WithCallSiteSkipDepth(6), 70 | WithCallerFlags(Lshortfile), 71 | }, 72 | level: LevelInfo, 73 | logFunc: func(t *testing.T, log Logger) { 74 | log.Info("Test Basic Log") 75 | }, 76 | expectedLog: `[INF] handler_test.go:30: Test Basic Log 77 | `, 78 | }, 79 | { 80 | name: "Sub-system tag", 81 | handlerOpts: []HandlerOption{WithNoTimestamp()}, 82 | level: LevelTrace, 83 | logFunc: func(t *testing.T, log Logger) { 84 | subLog := log.SubSystem("SUBS") 85 | subLog.Trace("Test Basic Log") 86 | }, 87 | expectedLog: `[TRC] SUBS: Test Basic Log 88 | `, 89 | }, 90 | { 91 | name: "Prefixed logging", 92 | handlerOpts: []HandlerOption{WithNoTimestamp()}, 93 | level: LevelTrace, 94 | logFunc: func(t *testing.T, log Logger) { 95 | // We use trace level to ensure that logger level is 96 | // carried over to the new prefixed logger. 97 | log.Tracef("Test Basic Log") 98 | 99 | subLog := log.SubSystem("SUBS") 100 | subLog.Tracef("Test Basic Log") 101 | 102 | pLog := subLog.WithPrefix("(Client)") 103 | pLog.Tracef("Test Basic Log") 104 | 105 | }, 106 | expectedLog: `[TRC]: Test Basic Log 107 | [TRC] SUBS: Test Basic Log 108 | [TRC] SUBS: (Client) Test Basic Log 109 | `, 110 | }, 111 | { 112 | name: "Test all levels", 113 | handlerOpts: []HandlerOption{WithNoTimestamp()}, 114 | level: LevelTrace, 115 | logFunc: func(t *testing.T, log Logger) { 116 | log.Trace("Trace") 117 | log.Debug("Debug") 118 | log.Info("Info") 119 | log.Warn("Warn") 120 | log.Error("Error") 121 | log.Critical("Critical") 122 | }, 123 | expectedLog: `[TRC]: Trace 124 | [DBG]: Debug 125 | [INF]: Info 126 | [WRN]: Warn 127 | [ERR]: Error 128 | [CRT]: Critical 129 | `, 130 | }, 131 | { 132 | name: "Structured Logs", 133 | handlerOpts: []HandlerOption{WithNoTimestamp()}, 134 | level: LevelInfo, 135 | logFunc: func(t *testing.T, log Logger) { 136 | ctx := context.Background() 137 | log.InfoS(ctx, "No attributes") 138 | log.InfoS(ctx, "Single word attribute", "key", "value") 139 | log.InfoS(ctx, "Multi word string value", "key with spaces", "value") 140 | log.InfoS(ctx, "Number attribute", "key", 5) 141 | log.InfoS(ctx, "Bad key", "key") 142 | log.InfoS(ctx, "Log with new line", "key", "value\nvalue") 143 | 144 | type b struct { 145 | name string 146 | age int 147 | address *string 148 | } 149 | 150 | var c *b 151 | log.InfoS(ctx, "Nil pointer value", "key", c) 152 | 153 | c = &b{name: "Bob", age: 5} 154 | log.InfoS(ctx, "Struct values", "key", c) 155 | 156 | ctx = WithCtx(ctx, "request_id", 5, "user_name", "alice") 157 | log.InfoS(ctx, "Test context attributes", "key", "value") 158 | }, 159 | expectedLog: `[INF]: No attributes 160 | [INF]: Single word attribute key=value 161 | [INF]: Multi word string value "key with spaces"=value 162 | [INF]: Number attribute key=5 163 | [INF]: Bad key !BADKEY=key 164 | [INF]: Log with new line key=value 165 | value 166 | [INF]: Nil pointer value key= 167 | [INF]: Struct values key="&{name:Bob age:5 address:}" 168 | [INF]: Test context attributes request_id=5 user_name=alice key=value 169 | `, 170 | }, 171 | { 172 | name: "Error logs", 173 | handlerOpts: []HandlerOption{WithNoTimestamp()}, 174 | level: LevelInfo, 175 | logFunc: func(t *testing.T, log Logger) { 176 | log.Error("Error string") 177 | log.Errorf("Error formatted string") 178 | 179 | ctx := context.Background() 180 | log.ErrorS(ctx, "Structured error log with nil error", nil) 181 | log.ErrorS(ctx, "Structured error with non-nil error", errors.New("oh no")) 182 | log.ErrorS(ctx, "Structured error with attributes", errors.New("oh no"), "key", "value") 183 | 184 | log.Warn("Warning string") 185 | log.Warnf("Warning formatted string") 186 | 187 | ctx = context.Background() 188 | log.WarnS(ctx, "Structured warning log with nil error", nil) 189 | log.WarnS(ctx, "Structured warning with non-nil error", errors.New("oh no")) 190 | log.WarnS(ctx, "Structured warning with attributes", errors.New("oh no"), "key", "value") 191 | 192 | log.Critical("Critical string") 193 | log.Criticalf("Critical formatted string") 194 | 195 | ctx = context.Background() 196 | log.CriticalS(ctx, "Structured critical log with nil error", nil) 197 | log.CriticalS(ctx, "Structured critical with non-nil error", errors.New("oh no")) 198 | log.CriticalS(ctx, "Structured critical with attributes", errors.New("oh no"), "key", "value") 199 | }, 200 | expectedLog: `[ERR]: Error string 201 | [ERR]: Error formatted string 202 | [ERR]: Structured error log with nil error 203 | [ERR]: Structured error with non-nil error err="oh no" 204 | [ERR]: Structured error with attributes err="oh no" key=value 205 | [WRN]: Warning string 206 | [WRN]: Warning formatted string 207 | [WRN]: Structured warning log with nil error 208 | [WRN]: Structured warning with non-nil error err="oh no" 209 | [WRN]: Structured warning with attributes err="oh no" key=value 210 | [CRT]: Critical string 211 | [CRT]: Critical formatted string 212 | [CRT]: Structured critical log with nil error 213 | [CRT]: Structured critical with non-nil error err="oh no" 214 | [CRT]: Structured critical with attributes err="oh no" key=value 215 | `, 216 | }, 217 | { 218 | name: "Slog Helpers", 219 | handlerOpts: []HandlerOption{WithNoTimestamp()}, 220 | level: LevelInfo, 221 | logFunc: func(t *testing.T, log Logger) { 222 | ctx := context.Background() 223 | log.InfoS(ctx, "msg", Hex("hex_val", []byte{ 224 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 225 | })) 226 | log.InfoS(ctx, "msg", Hex6("hex_val", []byte{ 227 | 0x01, 0x02, 228 | })) 229 | log.InfoS(ctx, "msg", Hex6("hex_val", []byte{ 230 | 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 231 | })) 232 | 233 | log.InfoS(ctx, "msg", "key", Sprintf("%.12f", 3.241)) 234 | log.InfoS(ctx, "msg", Fmt("key", "%.12f", 3.241)) 235 | 236 | // Create a closure that will fail the test if it is 237 | // executed. We log it with the Debug level so that it 238 | // is not executed. 239 | shouldNotRun := ClosureAttr("key", func() string { 240 | t.Fatalf("Should not compute") 241 | return "value" 242 | }) 243 | log.DebugS(ctx, "msg", shouldNotRun) 244 | 245 | // Create a closure that should be executed since it is 246 | // logged with the Info level. 247 | shouldRun := ClosureAttr("key", func() string { 248 | return "lazy compute" 249 | }) 250 | log.InfoS(ctx, "msg", shouldRun) 251 | }, 252 | expectedLog: `[INF]: msg hex_val=0102030405060708 253 | [INF]: msg hex_val=0102 254 | [INF]: msg hex_val=010203040506 255 | [INF]: msg key=3.241000000000 256 | [INF]: msg key=3.241000000000 257 | [INF]: msg key="lazy compute" 258 | `, 259 | }, 260 | { 261 | name: "Styled Outputs", 262 | handlerOpts: []HandlerOption{ 263 | WithNoTimestamp(), 264 | WithCallSiteSkipDepth(6), 265 | WithCallerFlags(Lshortfile), 266 | WithStyledKeys(func(s string) string { 267 | return s 268 | }), 269 | WithStyledCallSite( 270 | func(f string, l int) string { 271 | return fmt.Sprintf( 272 | "%s:%d", f, l, 273 | ) 274 | }, 275 | ), 276 | WithStyledLevel( 277 | func(l btclog.Level) string { 278 | return fmt.Sprintf( 279 | "[%s]", l, 280 | ) 281 | }, 282 | ), 283 | }, 284 | level: LevelInfo, 285 | logFunc: func(t *testing.T, log Logger) { 286 | ctx := context.Background() 287 | log.InfoS(ctx, "No attributes") 288 | log.InfoS(ctx, "Single word attribute", "key", "value") 289 | log.InfoS(ctx, "Multi word string value", "key with spaces", "value") 290 | log.InfoS(ctx, "Number attribute", "key", 5) 291 | log.InfoS(ctx, "Bad key", "key") 292 | }, 293 | expectedLog: `[INF] handler_test.go:30: No attributes 294 | [INF] handler_test.go:30: Single word attribute key=value 295 | [INF] handler_test.go:30: Multi word string value "key with spaces"=value 296 | [INF] handler_test.go:30: Number attribute key=5 297 | [INF] handler_test.go:30: Bad key !BADKEY=key 298 | `, 299 | }, 300 | } 301 | 302 | // TestSubSystemLevelIndependence tests that child loggers created with 303 | // SubSystem have independent level control and don't affect their parent's 304 | // level. 305 | func TestSubSystemLevelIndependence(t *testing.T) { 306 | t.Parallel() 307 | 308 | var ( 309 | buf bytes.Buffer 310 | handler = NewDefaultHandler(&buf) 311 | logger = NewSLogger(handler) 312 | ) 313 | 314 | // Set initial level to Info. 315 | logger.SetLevel(LevelInfo) 316 | 317 | // Create a child logger with subsystem. 318 | childLogger := logger.SubSystem("CHILD") 319 | 320 | // Both loggers should initially have the same level. 321 | if logger.Level() != childLogger.Level() { 322 | t.Fatalf("Child logger level mismatch. Expected %s, got %s", 323 | logger.Level(), childLogger.Level()) 324 | } 325 | 326 | // Debug messages should not appear (level is Info). 327 | logger.Debug("parent debug") 328 | childLogger.Debug("child debug") 329 | 330 | // Assert that neither logger wrote to the buffer. 331 | if buf.String() != "" { 332 | t.Fatalf("Debug messages should not appear. Got: %s", 333 | buf.String()) 334 | } 335 | 336 | // Now, change ONLY child level to Debug. 337 | childLogger.SetLevel(LevelDebug) 338 | 339 | // Loggers should now have different levels. 340 | if logger.Level() == childLogger.Level() { 341 | t.Fatalf("Child logger should have independent level. "+ 342 | "Parent: %s, Child: %s", logger.Level(), 343 | childLogger.Level()) 344 | } 345 | 346 | // Verify parent is still Info and child is Debug. 347 | if logger.Level() != LevelInfo { 348 | t.Fatalf("Parent level should still be Info. Got: %s", 349 | logger.Level()) 350 | } 351 | 352 | if childLogger.Level() != LevelDebug { 353 | t.Fatalf("Child level should be Debug. Got: %s", 354 | childLogger.Level()) 355 | } 356 | 357 | // Reset buffer. 358 | buf.Reset() 359 | 360 | // Debug messages should only appear from child. 361 | logger.Debug("parent debug") 362 | childLogger.Debug("child debug") 363 | 364 | // Parent debug should NOT appear. 365 | if bytes.Contains(buf.Bytes(), []byte("parent debug")) { 366 | t.Fatalf("Parent debug message should not appear. Got: %s", 367 | buf.String()) 368 | } 369 | 370 | // Child debug SHOULD appear. 371 | if !bytes.Contains(buf.Bytes(), []byte("child debug")) { 372 | t.Fatalf("Child debug message should appear. Got: %s", 373 | buf.String()) 374 | } 375 | 376 | // Reset buffer. 377 | buf.Reset() 378 | 379 | // Change parent level to Debug. 380 | logger.SetLevel(LevelDebug) 381 | 382 | // Child level should remain unchanged. 383 | if childLogger.Level() != LevelDebug { 384 | t.Fatalf("Child level should remain Debug. Got: %s", 385 | childLogger.Level()) 386 | } 387 | 388 | // Now both should log debug messages. 389 | logger.Debug("parent debug 2") 390 | childLogger.Debug("child debug 2") 391 | 392 | // Both messages should appear. 393 | if !bytes.Contains(buf.Bytes(), []byte("parent debug 2")) { 394 | t.Fatalf("Parent debug message should appear. Got: %s", 395 | buf.String()) 396 | } 397 | 398 | if !bytes.Contains(buf.Bytes(), []byte("child debug 2")) { 399 | t.Fatalf("Child debug message should appear. Got: %s", 400 | buf.String()) 401 | } 402 | } 403 | 404 | // TestWithPrefixLevelInheritance tests that child loggers created with 405 | // WithPrefix properly inherit level changes from their parent logger. 406 | func TestWithPrefixLevelInheritance(t *testing.T) { 407 | t.Parallel() 408 | 409 | var ( 410 | buf bytes.Buffer 411 | handler = NewDefaultHandler(&buf) 412 | logger = NewSLogger(handler) 413 | ) 414 | 415 | // Set initial level to Info. 416 | logger.SetLevel(LevelInfo) 417 | 418 | // Create a child logger with prefix. 419 | childLogger := logger.WithPrefix("child") 420 | 421 | // Both loggers should have the same level. 422 | if logger.Level() != childLogger.Level() { 423 | t.Fatalf("Child logger level mismatch. Expected %s, got %s", 424 | logger.Level(), childLogger.Level()) 425 | } 426 | 427 | // Debug messages should not appear (level is Info). 428 | logger.Debug("parent debug") 429 | childLogger.Debug("child debug") 430 | 431 | // Assert that neither logger wrote to the buffer. 432 | if buf.String() != "" { 433 | t.Fatalf("Debug messages should not appear. Got: %s", 434 | buf.String()) 435 | } 436 | 437 | // Now, change parent level to Debug. 438 | logger.SetLevel(LevelDebug) 439 | 440 | // Both loggers should have the same level. 441 | if logger.Level() != childLogger.Level() { 442 | t.Fatalf("Child logger level mismatch. Expected %s, got %s", 443 | logger.Level(), childLogger.Level()) 444 | } 445 | 446 | // Reset buffer. 447 | buf.Reset() 448 | 449 | // Now debug messages should appear from both loggers. 450 | logger.Debug("parent debug") 451 | childLogger.Debug("child debug") 452 | 453 | // Show that the buffer contains the parent log. 454 | if !bytes.Contains(buf.Bytes(), []byte("parent debug")) { 455 | t.Fatalf("Parent debug message not found in output: %s", 456 | buf.String()) 457 | } 458 | 459 | // Show that the buffer contains the child log. 460 | if !bytes.Contains(buf.Bytes(), []byte("child debug")) { 461 | t.Fatalf("Child debug message not found in output: %s", 462 | buf.String()) 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /v2/interface.go: -------------------------------------------------------------------------------- 1 | package btclog 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/btcsuite/btclog" 7 | ) 8 | 9 | // Logger is an interface which describes a level-based logger. A default 10 | // implementation of Logger is implemented by this package and can be created 11 | // by calling (*Backend).Logger. 12 | type Logger interface { 13 | // Tracef creates a formatted message from the to format specifier 14 | // along with any parameters then writes it to the logger with 15 | // LevelTrace. 16 | Tracef(format string, params ...any) 17 | 18 | // Debugf creates a formatted message from the to format specifier 19 | // along with any parameters then writes it to the logger with 20 | // LevelDebug. 21 | Debugf(format string, params ...any) 22 | 23 | // Infof creates a formatted message from the to format specifier 24 | // along with any parameters then writes it to the logger with 25 | // LevelInfo. 26 | Infof(format string, params ...any) 27 | 28 | // Warnf creates a formatted message from the to format specifier 29 | // along with any parameters then writes it to the logger with 30 | // LevelWarn. 31 | Warnf(format string, params ...any) 32 | 33 | // Errorf creates a formatted message from the to format specifier 34 | // along with any parameters then writes it to the logger with 35 | // LevelError. 36 | Errorf(format string, params ...any) 37 | 38 | // Criticalf creates a formatted message from the to format specifier 39 | // along with any parameters then writes it to the logger with 40 | // LevelCritical. 41 | Criticalf(format string, params ...any) 42 | 43 | // Trace formats a message using the default formats for its operands 44 | // and writes to log with LevelTrace. 45 | Trace(v ...any) 46 | 47 | // Debug formats a message using the default formats for its operands 48 | // and writes to log with LevelDebug. 49 | Debug(v ...any) 50 | 51 | // Info formats a message using the default formats for its operands 52 | // and writes to log with LevelInfo. 53 | Info(v ...any) 54 | 55 | // Warn formats a message using the default formats for its operands 56 | // and writes to log with LevelWarn. 57 | Warn(v ...any) 58 | 59 | // Error formats a message using the default formats for its operands 60 | // and writes to log with LevelError. 61 | Error(v ...any) 62 | 63 | // Critical formats a message using the default formats for its operands 64 | // and writes to log with LevelCritical. 65 | Critical(v ...any) 66 | 67 | // TraceS writes a structured log with the given message and key-value 68 | // pair attributes with LevelTrace to the log. 69 | TraceS(ctx context.Context, msg string, attrs ...any) 70 | 71 | // DebugS writes a structured log with the given message and key-value 72 | // pair attributes with LevelDebug to the log. 73 | DebugS(ctx context.Context, msg string, attrs ...any) 74 | 75 | // InfoS writes a structured log with the given message and key-value 76 | // pair attributes with LevelInfo to the log. 77 | InfoS(ctx context.Context, msg string, attrs ...any) 78 | 79 | // WarnS writes a structured log with the given message and key-value 80 | // pair attributes with LevelWarn to the log. 81 | WarnS(ctx context.Context, msg string, err error, attrs ...any) 82 | 83 | // ErrorS writes a structured log with the given message and key-value 84 | // pair attributes with LevelError to the log. 85 | ErrorS(ctx context.Context, msg string, err error, attrs ...any) 86 | 87 | // CriticalS writes a structured log with the given message and 88 | // key-value pair attributes with LevelCritical to the log. 89 | CriticalS(ctx context.Context, msg string, err error, attrs ...any) 90 | 91 | // Level returns the current logging level. 92 | Level() btclog.Level 93 | 94 | // SetLevel changes the logging level to the passed level. 95 | SetLevel(level btclog.Level) 96 | 97 | // SubSystem returns a copy of the logger but with the new subsystem 98 | // tag. 99 | // 100 | // NOTE: this creates a new logger with an independent log level. This 101 | // means that SetLevel needs to be called on the new logger to change 102 | // the level as any changes to the parent logger's level after creation 103 | // will not be inherited by the new logger. 104 | SubSystem(tag string) Logger 105 | 106 | // WithPrefix returns a copy of the logger but with the given string 107 | // prefixed to each log message. Note that the subsystem of the original 108 | // logger is kept but any existing prefix is overridden. 109 | // 110 | // NOTE: this creates a new logger with an inherited log level. This 111 | // means that if SetLevel is called on the parent logger, then this new 112 | // level will be inherited by the new logger 113 | WithPrefix(prefix string) Logger 114 | } 115 | 116 | // Ensure that the Logger implements the btclog.Logger interface so that an 117 | // implementation of the new and expanded interface can still be used by older 118 | // code depending on the older interface. 119 | var _ btclog.Logger = (Logger)(nil) 120 | -------------------------------------------------------------------------------- /v2/level.go: -------------------------------------------------------------------------------- 1 | package btclog 2 | 3 | import ( 4 | "log/slog" 5 | "strings" 6 | 7 | "github.com/btcsuite/btclog" 8 | ) 9 | 10 | // Redefine the levels here so that any package importing the original btclog 11 | // Level does not need to import both the old and new modules. 12 | const ( 13 | LevelTrace = btclog.LevelTrace 14 | LevelDebug = btclog.LevelDebug 15 | LevelInfo = btclog.LevelInfo 16 | LevelWarn = btclog.LevelWarn 17 | LevelError = btclog.LevelError 18 | LevelCritical = btclog.LevelCritical 19 | LevelOff = btclog.LevelOff 20 | ) 21 | 22 | // LevelFromString returns a level based on the input string s. If the input 23 | // can't be interpreted as a valid log level, the info level and false is 24 | // returned. 25 | func LevelFromString(s string) (l btclog.Level, ok bool) { 26 | switch strings.ToLower(s) { 27 | case "trace", "trc": 28 | return LevelTrace, true 29 | case "debug", "dbg": 30 | return LevelDebug, true 31 | case "info", "inf": 32 | return LevelInfo, true 33 | case "warn", "wrn": 34 | return LevelWarn, true 35 | case "error", "err": 36 | return LevelError, true 37 | case "critical", "crt": 38 | return LevelCritical, true 39 | case "off": 40 | return LevelOff, true 41 | default: 42 | return LevelInfo, false 43 | } 44 | } 45 | 46 | // slog uses some pre-defined level integers. So we will need to sometimes map 47 | // between the btclog.Level and the slog level. The slog library defines a few 48 | // of the commonly used levels and allows us to add a few of our own too. 49 | const ( 50 | levelTrace slog.Level = -5 51 | levelDebug = slog.LevelDebug 52 | levelInfo = slog.LevelInfo 53 | levelWarn = slog.LevelWarn 54 | levelError = slog.LevelError 55 | levelCritical slog.Level = 9 56 | levelOff slog.Level = 10 57 | ) 58 | 59 | // toSlogLevel converts a btclog.Level to the associated slog.Level type. 60 | func toSlogLevel(l btclog.Level) slog.Level { 61 | switch l { 62 | case LevelTrace: 63 | return levelTrace 64 | case LevelDebug: 65 | return levelDebug 66 | case LevelInfo: 67 | return levelInfo 68 | case LevelWarn: 69 | return levelWarn 70 | case LevelError: 71 | return levelError 72 | case LevelCritical: 73 | return levelCritical 74 | default: 75 | return levelOff 76 | } 77 | } 78 | 79 | // fromSlogLevel converts an slog.Level type to the associated btclog.Level 80 | // type. 81 | func fromSlogLevel(l slog.Level) btclog.Level { 82 | switch l { 83 | case levelTrace: 84 | return LevelTrace 85 | case levelDebug: 86 | return LevelDebug 87 | case levelInfo: 88 | return LevelInfo 89 | case levelWarn: 90 | return LevelWarn 91 | case levelError: 92 | return LevelError 93 | case levelCritical: 94 | return LevelCritical 95 | default: 96 | return LevelOff 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /v2/log.go: -------------------------------------------------------------------------------- 1 | package btclog 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | 9 | "github.com/btcsuite/btclog" 10 | ) 11 | 12 | // Disabled is a Logger that will never output anything. 13 | var Disabled Logger 14 | 15 | // Handler wraps the slog.Handler interface with a few more methods that we 16 | // need in order to satisfy the Logger interface. 17 | type Handler interface { 18 | slog.Handler 19 | 20 | // Level returns the current logging level of the Handler. 21 | Level() btclog.Level 22 | 23 | // SetLevel changes the logging level of the Handler to the passed 24 | // level. 25 | SetLevel(level btclog.Level) 26 | 27 | // SubSystem returns a copy of the given handler but with the new tag. 28 | // 29 | // NOTE: this creates a new logger with an independent log level. This 30 | // means that SetLevel needs to be called on the new logger to change 31 | // the level as any changes to the parent logger's level after creation 32 | // will not be inherited by the new logger. 33 | SubSystem(tag string) Handler 34 | 35 | // WithPrefix returns a copy of the Handler but with the given string 36 | // prefixed to each log message. Note that the subsystem of the original 37 | // logger is kept but any existing prefix is overridden. 38 | // 39 | // NOTE: this creates a new logger with an inherited log level. This 40 | // means that if SetLevel is called on the parent logger, then this new 41 | // level will be inherited by the new logger 42 | WithPrefix(prefix string) Handler 43 | } 44 | 45 | // sLogger is an implementation of Logger backed by a structured sLogger. 46 | type sLogger struct { 47 | handler Handler 48 | logger *slog.Logger 49 | 50 | // unusedCtx is a context that will be passed to the non-structured 51 | // logging calls for backwards compatibility with the old v1 Logger 52 | // interface. Transporting a context in a struct is an anti-pattern but 53 | // this is purely used for backwards compatibility and to prevent 54 | // needing to create a fresh context for each call to the old interface 55 | // methods. This is ok to do since the slog package does not use this 56 | // context for cancellation or deadlines. It purely uses it to extract 57 | // any slog attributes that have been added as values to the context. 58 | unusedCtx context.Context 59 | } 60 | 61 | // NewSLogger constructs a new structured logger from the given Handler. 62 | func NewSLogger(handler Handler) Logger { 63 | l := &sLogger{ 64 | handler: handler, 65 | logger: slog.New(handler), 66 | unusedCtx: context.Background(), 67 | } 68 | 69 | return l 70 | } 71 | 72 | // Tracef creates a formatted message from the to format specifier along with 73 | // any parameters then writes it to the logger with LevelTrace. 74 | // 75 | // This is part of the Logger interface implementation. 76 | func (l *sLogger) Tracef(format string, params ...any) { 77 | if !l.handler.Enabled(l.unusedCtx, levelTrace) { 78 | return 79 | } 80 | 81 | l.logger.Log(l.unusedCtx, levelTrace, fmt.Sprintf(format, params...)) 82 | } 83 | 84 | // Debugf creates a formatted message from the to format specifier along with 85 | // any parameters then writes it to the logger with LevelDebug. 86 | // 87 | // This is part of the Logger interface implementation. 88 | func (l *sLogger) Debugf(format string, params ...any) { 89 | if !l.handler.Enabled(l.unusedCtx, levelDebug) { 90 | return 91 | } 92 | 93 | l.logger.Log(l.unusedCtx, levelDebug, fmt.Sprintf(format, params...)) 94 | } 95 | 96 | // Infof creates a formatted message from the to format specifier along with 97 | // any parameters then writes it to the logger with LevelInfo. 98 | // 99 | // This is part of the Logger interface implementation. 100 | func (l *sLogger) Infof(format string, params ...any) { 101 | if !l.handler.Enabled(l.unusedCtx, levelInfo) { 102 | return 103 | } 104 | 105 | l.logger.Log(l.unusedCtx, levelInfo, fmt.Sprintf(format, params...)) 106 | } 107 | 108 | // Warnf creates a formatted message from the to format specifier along with 109 | // any parameters then writes it to the logger with LevelWarn. 110 | // 111 | // This is part of the Logger interface implementation. 112 | func (l *sLogger) Warnf(format string, params ...any) { 113 | if !l.handler.Enabled(l.unusedCtx, levelWarn) { 114 | return 115 | } 116 | 117 | l.logger.Log(l.unusedCtx, levelWarn, fmt.Sprintf(format, params...)) 118 | } 119 | 120 | // Errorf creates a formatted message from the to format specifier along with 121 | // any parameters then writes it to the logger with LevelError. 122 | // 123 | // This is part of the Logger interface implementation. 124 | func (l *sLogger) Errorf(format string, params ...any) { 125 | if !l.handler.Enabled(l.unusedCtx, levelError) { 126 | return 127 | } 128 | 129 | l.logger.Log(l.unusedCtx, levelError, fmt.Sprintf(format, params...)) 130 | } 131 | 132 | // Criticalf creates a formatted message from the to format specifier along 133 | // with any parameters then writes it to the logger with LevelCritical. 134 | // 135 | // This is part of the Logger interface implementation. 136 | func (l *sLogger) Criticalf(format string, params ...any) { 137 | if !l.handler.Enabled(l.unusedCtx, levelCritical) { 138 | return 139 | } 140 | 141 | l.logger.Log(l.unusedCtx, levelCritical, fmt.Sprintf(format, params...)) 142 | } 143 | 144 | // Trace formats a message using the default formats for its operands, prepends 145 | // the prefix as necessary, and writes to log with LevelTrace. 146 | // 147 | // This is part of the Logger interface implementation. 148 | func (l *sLogger) Trace(v ...any) { 149 | if !l.handler.Enabled(l.unusedCtx, levelTrace) { 150 | return 151 | } 152 | 153 | l.logger.Log(l.unusedCtx, levelTrace, fmt.Sprint(v...)) 154 | } 155 | 156 | // Debug formats a message using the default formats for its operands, prepends 157 | // the prefix as necessary, and writes to log with LevelDebug. 158 | // 159 | // This is part of the Logger interface implementation. 160 | func (l *sLogger) Debug(v ...any) { 161 | if !l.handler.Enabled(l.unusedCtx, levelDebug) { 162 | return 163 | } 164 | 165 | l.logger.Log(l.unusedCtx, levelDebug, fmt.Sprint(v...)) 166 | } 167 | 168 | // Info formats a message using the default formats for its operands, prepends 169 | // the prefix as necessary, and writes to log with LevelInfo. 170 | // 171 | // This is part of the Logger interface implementation. 172 | func (l *sLogger) Info(v ...any) { 173 | if !l.handler.Enabled(l.unusedCtx, levelInfo) { 174 | return 175 | } 176 | 177 | l.logger.Log(l.unusedCtx, levelInfo, fmt.Sprint(v...)) 178 | } 179 | 180 | // Warn formats a message using the default formats for its operands, prepends 181 | // the prefix as necessary, and writes to log with LevelWarn. 182 | // 183 | // This is part of the Logger interface implementation. 184 | func (l *sLogger) Warn(v ...any) { 185 | if !l.handler.Enabled(l.unusedCtx, levelWarn) { 186 | return 187 | } 188 | 189 | l.logger.Log(l.unusedCtx, levelWarn, fmt.Sprint(v...)) 190 | } 191 | 192 | // Error formats a message using the default formats for its operands, prepends 193 | // the prefix as necessary, and writes to log with LevelError. 194 | // 195 | // This is part of the Logger interface implementation. 196 | func (l *sLogger) Error(v ...any) { 197 | if !l.handler.Enabled(l.unusedCtx, levelError) { 198 | return 199 | } 200 | 201 | l.logger.Log(l.unusedCtx, levelError, fmt.Sprint(v...)) 202 | } 203 | 204 | // Critical formats a message using the default formats for its operands, 205 | // prepends the prefix as necessary, and writes to log with LevelCritical. 206 | // 207 | // This is part of the Logger interface implementation. 208 | func (l *sLogger) Critical(v ...any) { 209 | if !l.handler.Enabled(l.unusedCtx, levelCritical) { 210 | return 211 | } 212 | 213 | l.logger.Log(l.unusedCtx, levelCritical, fmt.Sprint(v...)) 214 | } 215 | 216 | // TraceS writes a structured log with the given message and key-value pair 217 | // attributes with LevelTrace to the log. 218 | // 219 | // This is part of the Logger interface implementation. 220 | func (l *sLogger) TraceS(ctx context.Context, msg string, attrs ...any) { 221 | if !l.handler.Enabled(ctx, levelTrace) { 222 | return 223 | } 224 | 225 | l.logger.Log(ctx, levelTrace, msg, mergeAttrs(ctx, attrs)...) 226 | } 227 | 228 | // DebugS writes a structured log with the given message and key-value pair 229 | // attributes with LevelDebug to the log. 230 | // 231 | // This is part of the Logger interface implementation. 232 | func (l *sLogger) DebugS(ctx context.Context, msg string, attrs ...any) { 233 | if !l.handler.Enabled(ctx, levelDebug) { 234 | return 235 | } 236 | 237 | l.logger.Log(ctx, levelDebug, msg, mergeAttrs(ctx, attrs)...) 238 | } 239 | 240 | // InfoS writes a structured log with the given message and key-value pair 241 | // attributes with LevelInfo to the log. 242 | // 243 | // This is part of the Logger interface implementation. 244 | func (l *sLogger) InfoS(ctx context.Context, msg string, attrs ...any) { 245 | if !l.handler.Enabled(ctx, levelInfo) { 246 | return 247 | } 248 | 249 | l.logger.Log(ctx, levelInfo, msg, mergeAttrs(ctx, attrs)...) 250 | } 251 | 252 | // WarnS writes a structured log with the given message and key-value pair 253 | // attributes with LevelWarn to the log. 254 | // 255 | // This is part of the Logger interface implementation. 256 | func (l *sLogger) WarnS(ctx context.Context, msg string, err error, 257 | attrs ...any) { 258 | 259 | if !l.handler.Enabled(ctx, levelWarn) { 260 | return 261 | } 262 | 263 | if err != nil { 264 | attrs = append([]any{slog.String("err", err.Error())}, attrs...) 265 | } 266 | 267 | l.logger.Log(ctx, levelWarn, msg, mergeAttrs(ctx, attrs)...) 268 | } 269 | 270 | // ErrorS writes a structured log with the given message and key-value pair 271 | // attributes with LevelError to the log. 272 | // 273 | // This is part of the Logger interface implementation. 274 | func (l *sLogger) ErrorS(ctx context.Context, msg string, err error, 275 | attrs ...any) { 276 | 277 | if !l.handler.Enabled(ctx, levelError) { 278 | return 279 | } 280 | 281 | if err != nil { 282 | attrs = append([]any{slog.String("err", err.Error())}, attrs...) 283 | } 284 | 285 | l.logger.Log(ctx, levelError, msg, mergeAttrs(ctx, attrs)...) 286 | } 287 | 288 | // CriticalS writes a structured log with the given message and key-value pair 289 | // attributes with LevelCritical to the log. 290 | // 291 | // This is part of the Logger interface implementation. 292 | func (l *sLogger) CriticalS(ctx context.Context, msg string, err error, 293 | attrs ...any) { 294 | 295 | if !l.handler.Enabled(ctx, levelCritical) { 296 | return 297 | } 298 | 299 | if err != nil { 300 | attrs = append([]any{slog.String("err", err.Error())}, attrs...) 301 | } 302 | 303 | l.logger.Log(ctx, levelCritical, msg, mergeAttrs(ctx, attrs)...) 304 | } 305 | 306 | // Level returns the current logging level of the Handler. 307 | // 308 | // This is part of the Logger interface implementation. 309 | func (l *sLogger) Level() btclog.Level { 310 | return l.handler.Level() 311 | } 312 | 313 | // SetLevel changes the logging level of the Handler to the passed level. 314 | // 315 | // This is part of the Logger interface implementation. 316 | func (l *sLogger) SetLevel(level btclog.Level) { 317 | l.handler.SetLevel(level) 318 | } 319 | 320 | // SubSystem returns a copy of the logger but with the new subsystem tag. 321 | // 322 | // This is part of the Logger interface implementation. 323 | func (l *sLogger) SubSystem(tag string) Logger { 324 | return NewSLogger(l.handler.SubSystem(tag)) 325 | } 326 | 327 | // WithPrefix returns a copy of the logger but with the given string prefixed to 328 | // each log message. Note that the subsystem of the original logger is kept but 329 | // any existing prefix is overridden. 330 | // 331 | // This is part of the Logger interface implementation. 332 | func (l *sLogger) WithPrefix(prefix string) Logger { 333 | return NewSLogger(l.handler.WithPrefix(prefix)) 334 | } 335 | 336 | var _ Logger = (*sLogger)(nil) 337 | 338 | func init() { 339 | // Initialise the Disabled logger. 340 | Disabled = NewSLogger(NewDefaultHandler(io.Discard)) 341 | Disabled.SetLevel(btclog.LevelOff) 342 | } 343 | -------------------------------------------------------------------------------- /v2/log_test.go: -------------------------------------------------------------------------------- 1 | package btclog 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "testing" 7 | 8 | "github.com/btcsuite/btclog" 9 | ) 10 | 11 | type complexType struct { 12 | m map[string]string 13 | s []string 14 | } 15 | 16 | var testType = complexType{ 17 | m: map[string]string{ 18 | "key1": "value1", 19 | "key2": "value2", 20 | }, 21 | s: []string{"a", "b", "c"}, 22 | } 23 | 24 | // BenchmarkLogger benchmarks the performance of the default v2 logger for each 25 | // logging level. This helps evaluate the effect of any change to the v2 logger. 26 | func BenchmarkLogger(b *testing.B) { 27 | ctx := context.Background() 28 | log := NewSLogger(NewDefaultHandler(io.Discard)) 29 | 30 | // Set the level to Info so that Debug logs are skipped. 31 | log.SetLevel(LevelInfo) 32 | 33 | tests := []struct { 34 | name string 35 | logFunc func() 36 | }{ 37 | { 38 | name: "Skipped Simple `f` Log", 39 | logFunc: func() { 40 | log.Debugf("msg") 41 | }, 42 | }, 43 | { 44 | name: "Skipped Simple `S` Log", 45 | logFunc: func() { 46 | log.DebugS(ctx, "msg") 47 | }, 48 | }, 49 | { 50 | name: "Simple `f` Log", 51 | logFunc: func() { 52 | log.Infof("msg") 53 | }, 54 | }, 55 | { 56 | name: "Simple `S` Log", 57 | logFunc: func() { 58 | log.InfoS(ctx, "msg") 59 | }, 60 | }, 61 | { 62 | name: "Skipped Complex `f` Log", 63 | logFunc: func() { 64 | log.Debugf("Debugf "+ 65 | "request_id=%d, "+ 66 | "complex_type=%v, "+ 67 | "type=%T, "+ 68 | "floating_point=%.12f, "+ 69 | "hex_value=%x, "+ 70 | "fmt_string=%s", 71 | 5, testType, testType, 72 | 3.141592653589793, []byte{0x01, 0x02}, 73 | Sprintf("%s, %v, %T, %.12f", 74 | "string", testType, testType, 75 | 3.141592653589793)) 76 | }, 77 | }, 78 | { 79 | name: "Skipped Complex `S` Log", 80 | logFunc: func() { 81 | log.DebugS(ctx, "InfoS", 82 | "request_id", 5, 83 | "complex_type", testType, 84 | Fmt("type", "%T", testType), 85 | Fmt("floating_point", "%.12f", 3.141592653589793), 86 | Hex("hex_value", []byte{0x01, 0x02}), 87 | Fmt("fmt_string", "%s, %v, %T, %.12f", 88 | "string", testType, testType, 89 | 3.141592653589793)) 90 | }, 91 | }, 92 | { 93 | name: "Complex `f` Log", 94 | logFunc: func() { 95 | log.Infof("Infof "+ 96 | "request_id=%d, "+ 97 | "complex_type=%v, "+ 98 | "type=%T, "+ 99 | "floating_point=%.12f, "+ 100 | "hex_value=%x, "+ 101 | "fmt_string=%s", 102 | 5, testType, testType, 103 | 3.141592653589793, []byte{0x01, 0x02}, 104 | Sprintf("%s, %v, %T, %.12f", 105 | "string", testType, testType, 106 | 3.141592653589793)) 107 | }, 108 | }, 109 | { 110 | name: "Complex `S` Log", 111 | logFunc: func() { 112 | log.InfoS(ctx, "InfoS", 113 | "request_id", 5, 114 | "complex_type", testType, 115 | Fmt("type", "%T", testType), 116 | Fmt("floating_point", "%.12f", 3.141592653589793), 117 | Hex("hex_value", []byte{0x01, 0x02}), 118 | Fmt("fmt_string", "%s, %v, %T, %.12f", 119 | "string", testType, testType, 120 | 3.141592653589793)) 121 | }, 122 | }, 123 | } 124 | 125 | for _, test := range tests { 126 | b.Run(test.name, func(b *testing.B) { 127 | for i := 0; i < b.N; i++ { 128 | test.logFunc() 129 | } 130 | }) 131 | } 132 | } 133 | 134 | // BenchmarkV1vsV2Loggers compares the performance of the btclog V1 logger to 135 | // the btclog V2 logger in various logs that can be used with both the legacy 136 | // Logger interface along with the new Logger interface. This, in other words, 137 | // therefore compares the performance change when the V1 logger is swapped out 138 | // for the V2 logger. 139 | func BenchmarkV1vsV2Loggers(b *testing.B) { 140 | loggers := []struct { 141 | name string 142 | btclog.Logger 143 | }{ 144 | { 145 | name: "btclog v1", 146 | Logger: btclog.NewBackend(io.Discard).Logger("V1"), 147 | }, 148 | { 149 | name: "btclog v2", 150 | Logger: NewSLogger(NewDefaultHandler(io.Discard)), 151 | }, 152 | } 153 | 154 | for _, logger := range loggers { 155 | // Test a basic message log with no formatted strings. 156 | b.Run(logger.name+" simple", func(b *testing.B) { 157 | for i := 0; i < b.N; i++ { 158 | logger.Infof("Basic") 159 | } 160 | }) 161 | 162 | // Test a basic message log with no formatted strings that gets 163 | // skipped due to the current log level. 164 | b.Run(logger.name+" skipped simple", func(b *testing.B) { 165 | for i := 0; i < b.N; i++ { 166 | logger.Debugf("Basic") 167 | } 168 | }) 169 | 170 | // Test a log line with a variety of different types and 171 | // formats. 172 | b.Run(logger.name+" complex", func(b *testing.B) { 173 | for i := 0; i < b.N; i++ { 174 | logger.Infof("Infof "+ 175 | "request_id=%d, "+ 176 | "complex_type=%v, "+ 177 | "type=%T, "+ 178 | "floating_point=%.12f, "+ 179 | "hex_value=%x, "+ 180 | "fmt_string=%s", 181 | 5, testType, testType, 182 | 3.141592653589793, []byte{0x01, 0x02}, 183 | Sprintf("%s, %v, %T, %.12f", 184 | "string", testType, testType, 185 | 3.141592653589793)) 186 | } 187 | }) 188 | 189 | // Test a log line with a variety of different types and formats 190 | // that then gets skipped due to the current log level. 191 | b.Run(logger.name+" skipped complex", func(b *testing.B) { 192 | for i := 0; i < b.N; i++ { 193 | logger.Debugf("Infof "+ 194 | "request_id=%d, "+ 195 | "complex_type=%v, "+ 196 | "type=%T, "+ 197 | "floating_point=%.12f, "+ 198 | "hex_value=%x, "+ 199 | "fmt_string=%s", 200 | 5, testType, testType, 201 | 3.141592653589793, []byte{0x01, 0x02}, 202 | Sprintf("%s, %v, %T, %.12f", 203 | "string", testType, testType, 204 | 3.141592653589793)) 205 | } 206 | }) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /v2/utils.go: -------------------------------------------------------------------------------- 1 | package btclog 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | "strings" 7 | "time" 8 | "unicode" 9 | "unicode/utf8" 10 | ) 11 | 12 | // defaultFlags specifies changes to the default logger behavior. It is set 13 | // during package init and configured using the LOGFLAGS environment variable. 14 | // New logger backends can override these default flags using WithFlags. 15 | var defaultFlags uint32 16 | 17 | // Flags to modify Backend's behavior. 18 | const ( 19 | // Llongfile modifies the logger output to include full path and line number 20 | // of the logging callsite, e.g. /a/b/c/main.go:123. 21 | Llongfile uint32 = 1 << iota 22 | 23 | // Lshortfile modifies the logger output to include filename and line number 24 | // of the logging callsite, e.g. main.go:123. Overrides Llongfile. 25 | Lshortfile 26 | ) 27 | 28 | // From stdlib log package. 29 | // Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid 30 | // zero-padding. 31 | func itoa(buf *buffer, i int, wid int) { 32 | // Assemble decimal in reverse order. 33 | var b [20]byte 34 | bp := len(b) - 1 35 | for i >= 10 || wid > 1 { 36 | wid-- 37 | q := i / 10 38 | b[bp] = byte('0' + i - q*10) 39 | bp-- 40 | i = q 41 | } 42 | // i < 10 43 | b[bp] = byte('0' + i) 44 | buf.writeBytes(b[bp:]) 45 | } 46 | 47 | // writeTimestamp writes the date in the format 'YYYY-MM-DD hh:mm:ss.sss' to the 48 | // buffer. 49 | func writeTimestamp(buf *buffer, t time.Time) { 50 | year, month, day := t.Date() 51 | hour, min, sec := t.Clock() 52 | ms := t.Nanosecond() / 1e6 53 | 54 | itoa(buf, year, 4) 55 | buf.writeByte('-') 56 | itoa(buf, int(month), 2) 57 | buf.writeByte('-') 58 | itoa(buf, day, 2) 59 | buf.writeByte(' ') 60 | itoa(buf, hour, 2) 61 | buf.writeByte(':') 62 | itoa(buf, min, 2) 63 | buf.writeByte(':') 64 | itoa(buf, sec, 2) 65 | buf.writeByte('.') 66 | itoa(buf, ms, 3) 67 | buf.writeByte(' ') 68 | } 69 | 70 | // callsite returns the file name and line number of the callsite to the 71 | // subsystem logger. 72 | func callsite(flag uint32, skipDepth int) (string, int) { 73 | _, file, line, ok := runtime.Caller(skipDepth) 74 | if !ok { 75 | return "???", 0 76 | } 77 | if flag&Lshortfile != 0 { 78 | short := file 79 | for i := len(file) - 1; i > 0; i-- { 80 | if os.IsPathSeparator(file[i]) { 81 | short = file[i+1:] 82 | break 83 | } 84 | } 85 | file = short 86 | } 87 | return file, line 88 | } 89 | 90 | // Copied from log/slog/text_handler.go. 91 | // 92 | // needsQuoting returns true if the given strings should be wrapped in quotes. 93 | func needsQuoting(s string) bool { 94 | if len(s) == 0 { 95 | return true 96 | } 97 | if strings.ContainsRune(s, '\n') { 98 | return false 99 | } 100 | for i := 0; i < len(s); { 101 | b := s[i] 102 | if b < utf8.RuneSelf { 103 | // Quote anything except a backslash that would need 104 | // quoting in a JSON string, as well as space and '='. 105 | if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) { 106 | return true 107 | } 108 | i++ 109 | continue 110 | } 111 | r, size := utf8.DecodeRuneInString(s[i:]) 112 | if r == utf8.RuneError || unicode.IsSpace(r) || 113 | !unicode.IsPrint(r) { 114 | 115 | return true 116 | } 117 | i += size 118 | } 119 | return false 120 | } 121 | 122 | // Copied from encoding/json/tables.go. 123 | // 124 | // safeSet holds the value true if the ASCII character with the given array 125 | // position can be represented inside a JSON string without any further 126 | // escaping. 127 | // 128 | // All values are true except for the ASCII control characters (0-31), the 129 | // double quote ("), and the backslash character ("\"). 130 | var safeSet = [utf8.RuneSelf]bool{ 131 | ' ': true, 132 | '!': true, 133 | '"': false, 134 | '#': true, 135 | '$': true, 136 | '%': true, 137 | '&': true, 138 | '\'': true, 139 | '(': true, 140 | ')': true, 141 | '*': true, 142 | '+': true, 143 | ',': true, 144 | '-': true, 145 | '.': true, 146 | '/': true, 147 | '0': true, 148 | '1': true, 149 | '2': true, 150 | '3': true, 151 | '4': true, 152 | '5': true, 153 | '6': true, 154 | '7': true, 155 | '8': true, 156 | '9': true, 157 | ':': true, 158 | ';': true, 159 | '<': true, 160 | '=': true, 161 | '>': true, 162 | '?': true, 163 | '@': true, 164 | 'A': true, 165 | 'B': true, 166 | 'C': true, 167 | 'D': true, 168 | 'E': true, 169 | 'F': true, 170 | 'G': true, 171 | 'H': true, 172 | 'I': true, 173 | 'J': true, 174 | 'K': true, 175 | 'L': true, 176 | 'M': true, 177 | 'N': true, 178 | 'O': true, 179 | 'P': true, 180 | 'Q': true, 181 | 'R': true, 182 | 'S': true, 183 | 'T': true, 184 | 'U': true, 185 | 'V': true, 186 | 'W': true, 187 | 'X': true, 188 | 'Y': true, 189 | 'Z': true, 190 | '[': true, 191 | '\\': false, 192 | ']': true, 193 | '^': true, 194 | '_': true, 195 | '`': true, 196 | 'a': true, 197 | 'b': true, 198 | 'c': true, 199 | 'd': true, 200 | 'e': true, 201 | 'f': true, 202 | 'g': true, 203 | 'h': true, 204 | 'i': true, 205 | 'j': true, 206 | 'k': true, 207 | 'l': true, 208 | 'm': true, 209 | 'n': true, 210 | 'o': true, 211 | 'p': true, 212 | 'q': true, 213 | 'r': true, 214 | 's': true, 215 | 't': true, 216 | 'u': true, 217 | 'v': true, 218 | 'w': true, 219 | 'x': true, 220 | 'y': true, 221 | 'z': true, 222 | '{': true, 223 | '|': true, 224 | '}': true, 225 | '~': true, 226 | '\u007f': true, 227 | } 228 | 229 | func init() { 230 | // Read logger flags from the LOGFLAGS environment variable. Multiple 231 | // flags can be set at once, separated by commas. 232 | for _, f := range strings.Split(os.Getenv("LOGFLAGS"), ",") { 233 | switch f { 234 | case "longfile": 235 | defaultFlags |= Llongfile 236 | case "shortfile": 237 | defaultFlags |= Lshortfile 238 | } 239 | } 240 | } 241 | --------------------------------------------------------------------------------