From 7f1a89745905b42dcda0a67a465f1e6b59f1003b Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 18 Sep 2024 11:35:28 +0200 Subject: [PATCH 1/3] README: update --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ff6b713..5d98c8b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ btclog ====== -[![Build Status](http://img.shields.io/travis/btcsuite/btclog.svg)](https://travis-ci.org/btcsuite/btclog) -[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) -[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/btcsuite/btclog) +Forked and adapted from https://github.com/btcsuite/btclog. Package btclog defines a logger interface and provides a default implementation of a subsystem-aware leveled logger implementing the same interface. @@ -11,28 +9,71 @@ of a subsystem-aware leveled logger implementing the same interface. ## Installation ```bash -$ go get github.com/btcsuite/btclog +$ go get github.com/lightninglabs/btclog ``` -## GPG Verification Key +## Usage -All official release tags are signed by Conformal so users can ensure the code -has not been tampered with and is coming from the btcsuite developers. To -verify the signature perform the following: +`btclog.NewSLogger` can be used to construct a new `btclog.Logger` type which +can then be used for logging calls. The `NewSLogger` function expects to be +initialised with a type that implements the `btclog.Handler` interface which is +responsible for writing logging records to a backend writer. Callers may provide +their own `Handler` implementations (for example, the standard library `slog` +package provides some handler implementations such as a JSON Handler and a Text +Handler) or else they may use the default one provided with this package: +`DefaultHandler`. -- Download the public key from the Conformal website at - https://opensource.conformal.com/GIT-GPG-KEY-conformal.txt +Example Usage: -- Import the public key into your GPG keyring: - ```bash - gpg --import GIT-GPG-KEY-conformal.txt - ``` +``` + // Create a new DefaultHandler that writes to stdout and set the + // logging level to Trace. + handler := btclog.NewDefaultHandler(os.Stdout) + handler.SetLevel(btclog.LevelTrace) + + // Use the above handler to construct a new logger. + log := btclog.NewSLogger(handler) + + /* + 2024-09-18 11:53:03.287 [INF]: An info level log + */ + log.Info("An info level log") + + // Create a subsystem logger with no timestamps. + handler = btclog.NewDefaultHandler(os.Stdout, btclog.WithNoTimestamp()) + log = btclog.NewSLogger(handler.SubSystem("SUBS")) + + /* + [INF] SUBS: An info level log + */ + log.Info("An info level log") -- Verify the release tag with the following command where `TAG_NAME` is a - placeholder for the specific tag: - ```bash - git tag -v TAG_NAME - ``` + // Include log source. + handler = btclog.NewDefaultHandler( + os.Stdout, + btclog.WithCallerFlags(btclog.Lshortfile), + btclog.WithNoTimestamp(), + ) + log = btclog.NewSLogger(handler.SubSystem("SUBS")) + + /* + [INF] SUBS main.go:36: An info level log + */ + log.Info("An info level log") + + // Attach attributes to a context. This will result in log lines + // including these attributes if the context is passed to them. Also + // pass in an attribute at log time. + log = btclog.NewSLogger(btclog.NewDefaultHandler( + os.Stdout, btclog.WithNoTimestamp(), + ).SubSystem("SUBS")) + ctx := btclog.WithCtx(context.Background(), "request_id", 5) + + /* + [INF] SUBS: A log line with context values request_id=5 another_key=another_value + */ + log.InfoS(ctx, "A log line with context values", "another_key", "another_value") +``` ## License From dca03d1798e8ac428138f75a7ecdc4e1bfb9fd9b Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Wed, 18 Sep 2024 12:13:48 +0200 Subject: [PATCH 2/3] multi: remove old logger & restructure - Remove the old logger implementation. - Move various helper methods to util.go - Rename slog.go to log.go - remove travis config file --- doc.go | 27 --- handler.go | 138 ---------------- log.go | 474 +++++++++++++---------------------------------------- slog.go | 223 ------------------------- util.go | 271 ++++++++++++++++++++++++++++++ 5 files changed, 383 insertions(+), 750 deletions(-) delete mode 100644 doc.go delete mode 100644 slog.go create mode 100644 util.go diff --git a/doc.go b/doc.go deleted file mode 100644 index b611497..0000000 --- a/doc.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2013-2017 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. - -/* -Package btclog defines an interface and default implementation for subsystem -logging. - -Log level verbosity may be modified at runtime for each individual subsystem -logger. - -The default implementation in this package must be created by the Backend type. -Backends can write to any io.Writer, including multi-writers created by -io.MultiWriter. Multi-writers allow log output to be written to many writers, -including standard output and log files. - -Optional logging behavior can be specified by using the LOGFLAGS environment -variable and overridden per-Backend by using the WithFlags call option. Multiple -LOGFLAGS options can be specified, separated by commas. The following options -are recognized: - - longfile: Include the full filepath and line number in all log messages - - shortfile: Include the filename and line number in all log messages. - Overrides longfile. -*/ -package btclog diff --git a/handler.go b/handler.go index aa64523..2c9f83f 100644 --- a/handler.go +++ b/handler.go @@ -10,8 +10,6 @@ import ( "sync" "sync/atomic" "time" - "unicode" - "unicode/utf8" ) const ( @@ -336,139 +334,3 @@ func appendTextValue(buf *buffer, v slog.Value) { appendString(buf, fmt.Sprintf("%s", v)) } } - -// Copied from log/slog/text_handler.go. -// -// needsQuoting returns true if the given strings should be wrapped in quotes. -func needsQuoting(s string) bool { - if len(s) == 0 { - return true - } - for i := 0; i < len(s); { - b := s[i] - if b < utf8.RuneSelf { - // Quote anything except a backslash that would need - // quoting in a JSON string, as well as space and '='. - if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) { - return true - } - i++ - continue - } - r, size := utf8.DecodeRuneInString(s[i:]) - if r == utf8.RuneError || unicode.IsSpace(r) || - !unicode.IsPrint(r) { - - return true - } - i += size - } - return false -} - -// Copied from encoding/json/tables.go. -// -// safeSet holds the value true if the ASCII character with the given array -// position can be represented inside a JSON string without any further -// escaping. -// -// All values are true except for the ASCII control characters (0-31), the -// double quote ("), and the backslash character ("\"). -var safeSet = [utf8.RuneSelf]bool{ - ' ': true, - '!': true, - '"': false, - '#': true, - '$': true, - '%': true, - '&': true, - '\'': true, - '(': true, - ')': true, - '*': true, - '+': true, - ',': true, - '-': true, - '.': true, - '/': true, - '0': true, - '1': true, - '2': true, - '3': true, - '4': true, - '5': true, - '6': true, - '7': true, - '8': true, - '9': true, - ':': true, - ';': true, - '<': true, - '=': true, - '>': true, - '?': true, - '@': true, - 'A': true, - 'B': true, - 'C': true, - 'D': true, - 'E': true, - 'F': true, - 'G': true, - 'H': true, - 'I': true, - 'J': true, - 'K': true, - 'L': true, - 'M': true, - 'N': true, - 'O': true, - 'P': true, - 'Q': true, - 'R': true, - 'S': true, - 'T': true, - 'U': true, - 'V': true, - 'W': true, - 'X': true, - 'Y': true, - 'Z': true, - '[': true, - '\\': false, - ']': true, - '^': true, - '_': true, - '`': true, - 'a': true, - 'b': true, - 'c': true, - 'd': true, - 'e': true, - 'f': true, - 'g': true, - 'h': true, - 'i': true, - 'j': true, - 'k': true, - 'l': true, - 'm': true, - 'n': true, - 'o': true, - 'p': true, - 'q': true, - 'r': true, - 's': true, - 't': true, - 'u': true, - 'v': true, - 'w': true, - 'x': true, - 'y': true, - 'z': true, - '{': true, - '|': true, - '}': true, - '~': true, - '\u007f': true, -} diff --git a/log.go b/log.go index b9bd1c7..9cbd133 100644 --- a/log.go +++ b/log.go @@ -1,482 +1,232 @@ -// Copyright (c) 2017 The btcsuite developers -// Use of this source code is governed by an ISC -// license that can be found in the LICENSE file. -// -// Copyright (c) 2009 The Go Authors. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - package btclog import ( - "bytes" "context" "fmt" "io" "log/slog" - "os" - "runtime" - "strings" - "sync" - "sync/atomic" - "time" ) -// defaultFlags specifies changes to the default logger behavior. It is set -// during package init and configured using the LOGFLAGS environment variable. -// New logger backends can override these default flags using WithFlags. -var defaultFlags uint32 - -// Flags to modify Backend's behavior. -const ( - // Llongfile modifies the logger output to include full path and line number - // of the logging callsite, e.g. /a/b/c/main.go:123. - Llongfile uint32 = 1 << iota - - // Lshortfile modifies the logger output to include filename and line number - // of the logging callsite, e.g. main.go:123. Overrides Llongfile. - Lshortfile -) - -// Read logger flags from the LOGFLAGS environment variable. Multiple flags can -// be set at once, separated by commas. -func init() { - for _, f := range strings.Split(os.Getenv("LOGFLAGS"), ",") { - switch f { - case "longfile": - defaultFlags |= Llongfile - case "shortfile": - defaultFlags |= Lshortfile - } - } -} - -// NewBackend creates a logger backend from a Writer. -func NewBackend(w io.Writer, opts ...BackendOption) *Backend { - b := &Backend{w: w, flag: defaultFlags} - for _, o := range opts { - o(b) - } - return b -} - -// Backend is a logging backend. Subsystems created from the backend write to -// the backend's Writer. Backend provides atomic writes to the Writer from all -// subsystems. -type Backend struct { - w io.Writer - mu sync.Mutex // ensures atomic writes - flag uint32 -} - -// BackendOption is a function used to modify the behavior of a Backend. -type BackendOption func(b *Backend) - -// WithFlags configures a Backend to use the specified flags rather than using -// the package's defaults as determined through the LOGFLAGS environment -// variable. -func WithFlags(flags uint32) BackendOption { - return func(b *Backend) { - b.flag = flags - } -} - -// From stdlib log package. -// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid -// zero-padding. -func itoa(buf *buffer, i int, wid int) { - // Assemble decimal in reverse order. - var b [20]byte - bp := len(b) - 1 - for i >= 10 || wid > 1 { - wid-- - q := i / 10 - b[bp] = byte('0' + i - q*10) - bp-- - i = q - } - // i < 10 - b[bp] = byte('0' + i) - buf.writeBytes(b[bp:]) -} - -// Appends a header in the default format 'YYYY-MM-DD hh:mm:ss.sss [LVL] TAG: '. -// If either of the Lshortfile or Llongfile flags are specified, the file named -// and line number are included after the tag and before the final colon. -func formatHeader(buf *buffer, t time.Time, lvl, tag string, file string, line int) { - writeTimestamp(buf, t) - - *buf = append(*buf, " ["...) - *buf = append(*buf, lvl...) - *buf = append(*buf, "] "...) - *buf = append(*buf, tag...) - if file != "" { - *buf = append(*buf, ' ') - *buf = append(*buf, file...) - *buf = append(*buf, ':') - itoa(buf, line, -1) - } - *buf = append(*buf, ": "...) -} - -// writeTimestamp writes the date in the format 'YYYY-MM-DD hh:mm:ss.sss' to the -// buffer. -func writeTimestamp(buf *buffer, t time.Time) { - year, month, day := t.Date() - hour, min, sec := t.Clock() - ms := t.Nanosecond() / 1e6 - - itoa(buf, year, 4) - buf.writeByte('-') - itoa(buf, int(month), 2) - buf.writeByte('-') - itoa(buf, day, 2) - buf.writeByte(' ') - itoa(buf, hour, 2) - buf.writeByte(':') - itoa(buf, min, 2) - buf.writeByte(':') - itoa(buf, sec, 2) - buf.writeByte('.') - itoa(buf, ms, 3) - buf.writeByte(' ') -} - -// calldepth is the call depth of the callsite function relative to the -// caller of the subsystem logger. It is used to recover the filename and line -// number of the logging call if either the short or long file flags are -// specified. -const calldepth = 3 - -// callsite returns the file name and line number of the callsite to the -// subsystem logger. -func callsite(flag uint32, skipDepth int) (string, int) { - _, file, line, ok := runtime.Caller(skipDepth) - if !ok { - return "???", 0 - } - if flag&Lshortfile != 0 { - short := file - for i := len(file) - 1; i > 0; i-- { - if os.IsPathSeparator(file[i]) { - short = file[i+1:] - break - } - } - file = short - } - return file, line -} - -// print outputs a log message to the writer associated with the backend after -// creating a prefix for the given level and tag according to the formatHeader -// function and formatting the provided arguments using the default formatting -// rules. -func (b *Backend) print(lvl, tag string, args ...any) { - t := time.Now() // get as early as possible - - bytebuf := newBuffer() - - var file string - var line int - if b.flag&(Lshortfile|Llongfile) != 0 { - file, line = callsite(b.flag, calldepth) - } - - formatHeader(bytebuf, t, lvl, tag, file, line) - buf := bytes.NewBuffer(*bytebuf) - fmt.Fprintln(buf, args...) - *bytebuf = buf.Bytes() - - b.mu.Lock() - b.w.Write(*bytebuf) - b.mu.Unlock() - - bytebuf.free() -} - -// printf outputs a log message to the writer associated with the backend after -// creating a prefix for the given level and tag according to the formatHeader -// function and formatting the provided arguments according to the given format -// specifier. -func (b *Backend) printf(lvl, tag string, format string, args ...any) { - t := time.Now() // get as early as possible - - bytebuf := newBuffer() - - var file string - var line int - if b.flag&(Lshortfile|Llongfile) != 0 { - file, line = callsite(b.flag, calldepth) - } +// Disabled is a Logger that will never output anything. +var Disabled Logger - formatHeader(bytebuf, t, lvl, tag, file, line) - buf := bytes.NewBuffer(*bytebuf) - fmt.Fprintf(buf, format, args...) - *bytebuf = append(buf.Bytes(), '\n') +// Handler wraps the slog.Handler interface with a few more methods that we +// need in order to satisfy the Logger interface. +type Handler interface { + slog.Handler - b.mu.Lock() - b.w.Write(*bytebuf) - b.mu.Unlock() + // Level returns the current logging level of the Handler. + Level() Level - bytebuf.free() -} + // SetLevel changes the logging level of the Handler to the passed + // level. + SetLevel(level Level) -// Logger returns a new logger for a particular subsystem that writes to the -// Backend b. A tag describes the subsystem and is included in all log -// messages. The logger uses the info verbosity level by default. -func (b *Backend) Logger(subsystemTag string) Logger { - return newSubLog(subsystemTag, b, LevelInfo) + // SubSystem returns a copy of the given handler but with the new tag. + SubSystem(tag string) Handler } -// subLog is a subsystem logger for a Backend. Implements the Logger interface. -type subLog struct { - lvl atomic.Int32 - tag string - b *Backend +// sLogger is an implementation of Logger backed by a structured sLogger. +type sLogger struct { + Handler + logger *slog.Logger } -// newSubLog constructs a new subLog instance. -func newSubLog(tag string, b *Backend, level Level) *subLog { - s := &subLog{ - tag: tag, - b: b, +// NewSLogger constructs a new structured logger from the given Handler. +func NewSLogger(handler Handler) Logger { + return &sLogger{ + Handler: handler, + logger: slog.New(handler), } - s.lvl.Store(int32(level)) - - return s } -// Trace formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelTrace. +// Tracef formats message according to format specifier, prepends the prefix as +// necessary, and writes to log with LevelTrace. // // This is part of the Logger interface implementation. -func (l *subLog) Trace(args ...any) { - lvl := l.Level() - if lvl <= LevelTrace { - l.b.print("TRC", l.tag, args...) - } +func (l *sLogger) Tracef(format string, params ...any) { + l.toSlogf(LevelTrace, format, params...) } -// Tracef formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelTrace. +// Debugf formats message according to format specifier, prepends the prefix as +// necessary, and writes to log with LevelDebug. // // This is part of the Logger interface implementation. -func (l *subLog) Tracef(format string, args ...any) { - lvl := l.Level() - if lvl <= LevelTrace { - l.b.printf("TRC", l.tag, format, args...) - } +func (l *sLogger) Debugf(format string, params ...any) { + l.toSlogf(LevelDebug, format, params...) } -// Debug formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelDebug. +// Infof formats message according to format specifier, prepends the prefix as +// necessary, and writes to log with LevelInfo. // // This is part of the Logger interface implementation. -func (l *subLog) Debug(args ...any) { - lvl := l.Level() - if lvl <= LevelDebug { - l.b.print("DBG", l.tag, args...) - } +func (l *sLogger) Infof(format string, params ...any) { + l.toSlogf(LevelInfo, format, params...) } -// Debugf formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelDebug. +// Warnf formats message according to format specifier, prepends the prefix as +// necessary, and writes to log with LevelWarn. // // This is part of the Logger interface implementation. -func (l *subLog) Debugf(format string, args ...any) { - lvl := l.Level() - if lvl <= LevelDebug { - l.b.printf("DBG", l.tag, format, args...) - } +func (l *sLogger) Warnf(format string, params ...any) { + l.toSlogf(LevelWarn, format, params...) } -// Info formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelInfo. +// Errorf formats message according to format specifier, prepends the prefix as +// necessary, and writes to log with LevelError. // // This is part of the Logger interface implementation. -func (l *subLog) Info(args ...any) { - lvl := l.Level() - if lvl <= LevelInfo { - l.b.print("INF", l.tag, args...) - } +func (l *sLogger) Errorf(format string, params ...any) { + l.toSlogf(LevelError, format, params...) } -// Infof formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelInfo. +// Criticalf formats message according to format specifier, prepends the prefix as +// necessary, and writes to log with LevelCritical. // // This is part of the Logger interface implementation. -func (l *subLog) Infof(format string, args ...any) { - lvl := l.Level() - if lvl <= LevelInfo { - l.b.printf("INF", l.tag, format, args...) - } +func (l *sLogger) Criticalf(format string, params ...any) { + l.toSlogf(LevelCritical, format, params...) } -// Warn formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelWarn. +// Trace formats message using the default formats for its operands, prepends +// the prefix as necessary, and writes to log with LevelTrace. // // This is part of the Logger interface implementation. -func (l *subLog) Warn(args ...any) { - lvl := l.Level() - if lvl <= LevelWarn { - l.b.print("WRN", l.tag, args...) - } +func (l *sLogger) Trace(v ...any) { + l.toSlog(LevelTrace, v...) } -// Warnf formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelWarn. +// Debug formats message using the default formats for its operands, prepends +// the prefix as necessary, and writes to log with LevelDebug. // // This is part of the Logger interface implementation. -func (l *subLog) Warnf(format string, args ...any) { - lvl := l.Level() - if lvl <= LevelWarn { - l.b.printf("WRN", l.tag, format, args...) - } +func (l *sLogger) Debug(v ...any) { + l.toSlog(LevelDebug, v...) } -// Error formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelError. +// Info formats message using the default formats for its operands, prepends +// the prefix as necessary, and writes to log with LevelInfo. // // This is part of the Logger interface implementation. -func (l *subLog) Error(args ...any) { - lvl := l.Level() - if lvl <= LevelError { - l.b.print("ERR", l.tag, args...) - } +func (l *sLogger) Info(v ...any) { + l.toSlog(LevelInfo, v...) } -// Errorf formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelError. +// Warn formats message using the default formats for its operands, prepends +// the prefix as necessary, and writes to log with LevelWarn. // // This is part of the Logger interface implementation. -func (l *subLog) Errorf(format string, args ...any) { - lvl := l.Level() - if lvl <= LevelError { - l.b.printf("ERR", l.tag, format, args...) - } +func (l *sLogger) Warn(v ...any) { + l.toSlog(LevelWarn, v...) } -// Critical formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelCritical. +// Error formats message using the default formats for its operands, prepends +// the prefix as necessary, and writes to log with LevelError. // // This is part of the Logger interface implementation. -func (l *subLog) Critical(args ...any) { - lvl := l.Level() - if lvl <= LevelCritical { - l.b.print("CRT", l.tag, args...) - } +func (l *sLogger) Error(v ...any) { + l.toSlog(LevelError, v...) } -// Criticalf formats message according to format specifier, prepends the prefix -// as necessary, and writes to log with LevelCritical. +// Critical formats message using the default formats for its operands, prepends +// the prefix as necessary, and writes to log with LevelCritical. // // This is part of the Logger interface implementation. -func (l *subLog) Criticalf(format string, args ...any) { - lvl := l.Level() - if lvl <= LevelCritical { - l.b.printf("CRT", l.tag, format, args...) - } +func (l *sLogger) Critical(v ...any) { + l.toSlog(LevelCritical, v...) } // TraceS writes a structured log with the given message and key-value pair // attributes with LevelTrace to the log. // // This is part of the Logger interface implementation. -func (l *subLog) TraceS(_ context.Context, msg string, attrs ...any) { - l.Tracef(msg, attrs...) +func (l *sLogger) TraceS(ctx context.Context, msg string, attrs ...any) { + l.toSlogS(ctx, LevelTrace, msg, attrs...) } // DebugS writes a structured log with the given message and key-value pair // attributes with LevelDebug to the log. // // This is part of the Logger interface implementation. -func (l *subLog) DebugS(_ context.Context, msg string, attrs ...any) { - l.Debugf(msg, attrs...) +func (l *sLogger) DebugS(ctx context.Context, msg string, attrs ...any) { + l.toSlogS(ctx, LevelDebug, msg, attrs...) } // InfoS writes a structured log with the given message and key-value pair // attributes with LevelInfo to the log. // // This is part of the Logger interface implementation. -func (l *subLog) InfoS(_ context.Context, msg string, attrs ...any) { - l.Infof(msg, attrs...) +func (l *sLogger) InfoS(ctx context.Context, msg string, attrs ...any) { + l.toSlogS(ctx, LevelInfo, msg, attrs...) } // WarnS writes a structured log with the given message and key-value pair // attributes with LevelWarn to the log. // // This is part of the Logger interface implementation. -func (l *subLog) WarnS(_ context.Context, msg string, err error, attrs ...any) { +func (l *sLogger) WarnS(ctx context.Context, msg string, err error, + attrs ...any) { + if err != nil { attrs = append([]any{slog.String("err", err.Error())}, attrs...) } - l.Warnf(msg, attrs...) + + l.toSlogS(ctx, LevelWarn, msg, attrs...) } // ErrorS writes a structured log with the given message and key-value pair // attributes with LevelError to the log. // // This is part of the Logger interface implementation. -func (l *subLog) ErrorS(_ context.Context, msg string, err error, attrs ...any) { +func (l *sLogger) ErrorS(ctx context.Context, msg string, err error, + attrs ...any) { + if err != nil { attrs = append([]any{slog.String("err", err.Error())}, attrs...) } - l.Errorf(msg, attrs...) + + l.toSlogS(ctx, LevelError, msg, attrs...) } // CriticalS writes a structured log with the given message and key-value pair // attributes with LevelCritical to the log. // // This is part of the Logger interface implementation. -func (l *subLog) CriticalS(_ context.Context, msg string, err error, attrs ...any) { +func (l *sLogger) CriticalS(ctx context.Context, msg string, err error, + attrs ...any) { if err != nil { attrs = append([]any{slog.String("err", err.Error())}, attrs...) } - l.Criticalf(msg, attrs...) + + l.toSlogS(ctx, LevelCritical, msg, attrs...) } -// Level returns the current logging level -// -// This is part of the Logger interface implementation. -func (l *subLog) Level() Level { - return Level(l.lvl.Load()) +// toSlogf is a helper method that converts an unstructured log call that +// contains a format string and parameters for the string into the appropriate +// form expected by the structured logger. +func (l *sLogger) toSlogf(level Level, format string, params ...any) { + l.logger.Log(context.Background(), slog.Level(level), + fmt.Sprintf(format, params...)) } -// SetLevel changes the logging level to the passed level. -// -// This is part of the Logger interface implementation. -func (l *subLog) SetLevel(level Level) { - l.lvl.Store(int32(level)) +// toSlog is a helper method that converts an unstructured log call that +// contains a number of parameters into the appropriate form expected by the +// structured logger. +func (l *sLogger) toSlog(level Level, v ...any) { + l.logger.Log(context.Background(), slog.Level(level), fmt.Sprint(v...)) } -// Disabled is a Logger that will never output anything. -var Disabled Logger +// toSlogS is a helper method that can be used by all the structured log calls +// to access the underlying logger. +func (l *sLogger) toSlogS(ctx context.Context, level Level, msg string, + attrs ...any) { + + l.logger.Log(ctx, slog.Level(level), msg, mergeAttrs(ctx, attrs)...) +} + +var _ Logger = (*sLogger)(nil) func init() { - Disabled = newSubLog("", NewBackend(io.Discard), LevelOff) + Disabled = NewSLogger(NewDefaultHandler(io.Discard)) + Disabled.SetLevel(LevelOff) } diff --git a/slog.go b/slog.go deleted file mode 100644 index 9e90a88..0000000 --- a/slog.go +++ /dev/null @@ -1,223 +0,0 @@ -package btclog - -import ( - "context" - "fmt" - "log/slog" -) - -// Handler wraps the slog.Handler interface with a few more methods that we -// need in order to satisfy the Logger interface. -type Handler interface { - slog.Handler - - // Level returns the current logging level of the Handler. - Level() Level - - // SetLevel changes the logging level of the Handler to the passed - // level. - SetLevel(level Level) - - // SubSystem returns a copy of the given handler but with the new tag. - SubSystem(tag string) Handler -} - -// sLogger is an implementation of Logger backed by a structured sLogger. -type sLogger struct { - Handler - logger *slog.Logger -} - -// NewSLogger constructs a new structured logger from the given Handler. -func NewSLogger(handler Handler) Logger { - return &sLogger{ - Handler: handler, - logger: slog.New(handler), - } -} - -// Tracef formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelTrace. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Tracef(format string, params ...any) { - l.toSlogf(LevelTrace, format, params...) -} - -// Debugf formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelDebug. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Debugf(format string, params ...any) { - l.toSlogf(LevelDebug, format, params...) -} - -// Infof formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelInfo. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Infof(format string, params ...any) { - l.toSlogf(LevelInfo, format, params...) -} - -// Warnf formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelWarn. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Warnf(format string, params ...any) { - l.toSlogf(LevelWarn, format, params...) -} - -// Errorf formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelError. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Errorf(format string, params ...any) { - l.toSlogf(LevelError, format, params...) -} - -// Criticalf formats message according to format specifier, prepends the prefix as -// necessary, and writes to log with LevelCritical. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Criticalf(format string, params ...any) { - l.toSlogf(LevelCritical, format, params...) -} - -// Trace formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelTrace. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Trace(v ...any) { - l.toSlog(LevelTrace, v...) -} - -// Debug formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelDebug. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Debug(v ...any) { - l.toSlog(LevelDebug, v...) -} - -// Info formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelInfo. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Info(v ...any) { - l.toSlog(LevelInfo, v...) -} - -// Warn formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelWarn. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Warn(v ...any) { - l.toSlog(LevelWarn, v...) -} - -// Error formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelError. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Error(v ...any) { - l.toSlog(LevelError, v...) -} - -// Critical formats message using the default formats for its operands, prepends -// the prefix as necessary, and writes to log with LevelCritical. -// -// This is part of the Logger interface implementation. -func (l *sLogger) Critical(v ...any) { - l.toSlog(LevelCritical, v...) -} - -// TraceS writes a structured log with the given message and key-value pair -// attributes with LevelTrace to the log. -// -// This is part of the Logger interface implementation. -func (l *sLogger) TraceS(ctx context.Context, msg string, attrs ...any) { - l.toSlogS(ctx, LevelTrace, msg, attrs...) -} - -// DebugS writes a structured log with the given message and key-value pair -// attributes with LevelDebug to the log. -// -// This is part of the Logger interface implementation. -func (l *sLogger) DebugS(ctx context.Context, msg string, attrs ...any) { - l.toSlogS(ctx, LevelDebug, msg, attrs...) -} - -// InfoS writes a structured log with the given message and key-value pair -// attributes with LevelInfo to the log. -// -// This is part of the Logger interface implementation. -func (l *sLogger) InfoS(ctx context.Context, msg string, attrs ...any) { - l.toSlogS(ctx, LevelInfo, msg, attrs...) -} - -// WarnS writes a structured log with the given message and key-value pair -// attributes with LevelWarn to the log. -// -// This is part of the Logger interface implementation. -func (l *sLogger) WarnS(ctx context.Context, msg string, err error, - attrs ...any) { - - if err != nil { - attrs = append([]any{slog.String("err", err.Error())}, attrs...) - } - - l.toSlogS(ctx, LevelWarn, msg, attrs...) -} - -// ErrorS writes a structured log with the given message and key-value pair -// attributes with LevelError to the log. -// -// This is part of the Logger interface implementation. -func (l *sLogger) ErrorS(ctx context.Context, msg string, err error, - attrs ...any) { - - if err != nil { - attrs = append([]any{slog.String("err", err.Error())}, attrs...) - } - - l.toSlogS(ctx, LevelError, msg, attrs...) -} - -// CriticalS writes a structured log with the given message and key-value pair -// attributes with LevelCritical to the log. -// -// This is part of the Logger interface implementation. -func (l *sLogger) CriticalS(ctx context.Context, msg string, err error, - attrs ...any) { - if err != nil { - attrs = append([]any{slog.String("err", err.Error())}, attrs...) - } - - l.toSlogS(ctx, LevelCritical, msg, attrs...) -} - -// toSlogf is a helper method that converts an unstructured log call that -// contains a format string and parameters for the string into the appropriate -// form expected by the structured logger. -func (l *sLogger) toSlogf(level Level, format string, params ...any) { - l.logger.Log(context.Background(), slog.Level(level), - fmt.Sprintf(format, params...)) -} - -// toSlog is a helper method that converts an unstructured log call that -// contains a number of parameters into the appropriate form expected by the -// structured logger. -func (l *sLogger) toSlog(level Level, v ...any) { - l.logger.Log(context.Background(), slog.Level(level), fmt.Sprint(v...)) -} - -// toSlogS is a helper method that can be used by all the structured log calls -// to access the underlying logger. -func (l *sLogger) toSlogS(ctx context.Context, level Level, msg string, - attrs ...any) { - - l.logger.Log(ctx, slog.Level(level), msg, mergeAttrs(ctx, attrs)...) -} - -var _ Logger = (*sLogger)(nil) diff --git a/util.go b/util.go new file mode 100644 index 0000000..2e0e398 --- /dev/null +++ b/util.go @@ -0,0 +1,271 @@ +// Copyright (c) 2017 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. +// +// Copyright (c) 2009 The Go Authors. All rights reserved. +// +// Copyright (c) 2024 The Lightning Network Daemon developers. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package btclog + +import ( + "os" + "runtime" + "strings" + "time" + "unicode" + "unicode/utf8" +) + +// defaultFlags specifies changes to the default logger behavior. It is set +// during package init and configured using the LOGFLAGS environment variable. +// New logger backends can override these default flags using WithFlags. +var defaultFlags uint32 + +// Flags to modify Backend's behavior. +const ( + // Llongfile modifies the logger output to include full path and line number + // of the logging callsite, e.g. /a/b/c/main.go:123. + Llongfile uint32 = 1 << iota + + // Lshortfile modifies the logger output to include filename and line number + // of the logging callsite, e.g. main.go:123. Overrides Llongfile. + Lshortfile +) + +// Read logger flags from the LOGFLAGS environment variable. Multiple flags can +// be set at once, separated by commas. +func init() { + for _, f := range strings.Split(os.Getenv("LOGFLAGS"), ",") { + switch f { + case "longfile": + defaultFlags |= Llongfile + case "shortfile": + defaultFlags |= Lshortfile + } + } +} + +// From stdlib log package. +// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid +// zero-padding. +func itoa(buf *buffer, i int, wid int) { + // Assemble decimal in reverse order. + var b [20]byte + bp := len(b) - 1 + for i >= 10 || wid > 1 { + wid-- + q := i / 10 + b[bp] = byte('0' + i - q*10) + bp-- + i = q + } + // i < 10 + b[bp] = byte('0' + i) + buf.writeBytes(b[bp:]) +} + +// writeTimestamp writes the date in the format 'YYYY-MM-DD hh:mm:ss.sss' to the +// buffer. +func writeTimestamp(buf *buffer, t time.Time) { + year, month, day := t.Date() + hour, min, sec := t.Clock() + ms := t.Nanosecond() / 1e6 + + itoa(buf, year, 4) + buf.writeByte('-') + itoa(buf, int(month), 2) + buf.writeByte('-') + itoa(buf, day, 2) + buf.writeByte(' ') + itoa(buf, hour, 2) + buf.writeByte(':') + itoa(buf, min, 2) + buf.writeByte(':') + itoa(buf, sec, 2) + buf.writeByte('.') + itoa(buf, ms, 3) + buf.writeByte(' ') +} + +// callsite returns the file name and line number of the callsite to the +// subsystem logger. +func callsite(flag uint32, skipDepth int) (string, int) { + _, file, line, ok := runtime.Caller(skipDepth) + if !ok { + return "???", 0 + } + if flag&Lshortfile != 0 { + short := file + for i := len(file) - 1; i > 0; i-- { + if os.IsPathSeparator(file[i]) { + short = file[i+1:] + break + } + } + file = short + } + return file, line +} + +// Copied from log/slog/text_handler.go. +// +// needsQuoting returns true if the given strings should be wrapped in quotes. +func needsQuoting(s string) bool { + if len(s) == 0 { + return true + } + for i := 0; i < len(s); { + b := s[i] + if b < utf8.RuneSelf { + // Quote anything except a backslash that would need + // quoting in a JSON string, as well as space and '='. + if b != '\\' && (b == ' ' || b == '=' || !safeSet[b]) { + return true + } + i++ + continue + } + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError || unicode.IsSpace(r) || + !unicode.IsPrint(r) { + + return true + } + i += size + } + return false +} + +// Copied from encoding/json/tables.go. +// +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} From 6cec8f2504ef190158a3aedb4748843e110f4cb3 Mon Sep 17 00:00:00 2001 From: Elle Mouton Date: Mon, 23 Sep 2024 12:42:59 +0900 Subject: [PATCH 3/3] make+gh: run unit tests --- .github/workflows/main.yml | 49 ++++++++++++++++++++++++++++++++++++++ Makefile | 11 +++++++++ 2 files changed, 60 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 Makefile diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..8406a4b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: + - "master" + pull_request: + branches: + - "*" + +defaults: + run: + shell: bash + +env: + # go needs absolute directories, using the $HOME variable doesn't work here. + GOCACHE: /home/runner/work/go/pkg/build + GOPATH: /home/runner/work/go + GO_VERSION: 1.22.3 + +jobs: + ######################## + # run unit tests + ######################## + unit-test: + name: run unit tests + runs-on: ubuntu-latest + steps: + - name: git checkout + uses: actions/checkout@v3 + + - name: go cache + uses: actions/cache@v3 + with: + path: /home/runner/work/go + key: btclog-${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ github.job }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + btclog-${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ github.job }}-${{ hashFiles('**/go.sum') }} + btclog-${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ github.job }}- + btclog-${{ runner.os }}-go-${{ env.GO_VERSION }}- + btclog-${{ runner.os }}-go- + + - name: setup go ${{ env.GO_VERSION }} + uses: actions/setup-go@v3 + with: + go-version: '~${{ env.GO_VERSION }}' + + - name: run unit tests + run: make unit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..52fb787 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +PKG := github.com/lightninglabs/btclog + +GOLIST := go list -deps $(PKG)/... | grep '$(PKG)'| grep -v '/vendor/' +GOTEST := go test -v + +XARGS := xargs -L 1 +UNIT := $(GOLIST) | $(XARGS) env $(GOTEST) $(TEST_FLAGS) + +unit: + @$(call print, "Running unit tests.") + $(UNIT) \ No newline at end of file