From 62af906123ef184c65e833ab095fbc6494d6104f Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Thu, 22 Oct 2026 06:42:27 +0200 Subject: [PATCH] chore: improve documentation --- context.go | 24 +++++++------ ctx/ctx.go | 74 ++++++++++++++++++++++++-------------- doc.go | 41 ++++++++++++++------- event.go | 32 +++++++++-------- example_test.go | 44 +++++++++++++++++++++++ format.go | 57 +++++++++++++++++++++++++---- logger.go | 95 +++++++++++++++++++++++++++++++++++-------------- writer.go | 9 +++-- 8 files changed, 275 insertions(+), 101 deletions(-) diff --git a/context.go b/context.go index 36d4338..5562e68 100644 --- a/context.go +++ b/context.go @@ -8,13 +8,15 @@ type ctxFields struct { b []byte } -// WithContext returns a new context carrying the given fields. -// Any fields previously attached via WithContext are preserved; -// the new fields are appended after them. Any fields already -// added to the logger are not attached to the context. +// WithContext returns a copy of ctx carrying the given fields. +// Fields already attached to ctx by WithContext are kept and the new fields +// follow them. Fields added to log with [Logger.With] are not attached. // -// If the logger discards output or no fields are given, ctx is returned -// unchanged. +// The fields are evaluated once, when WithContext is called, and formatted +// with the [Formatter] of log. They must only be retrieved with +// [Logger.FromContext] on a [Logger] that uses the same kind of Formatter. +// +// If log discards output or no fields are given, ctx is returned unchanged. func WithContext(ctx context.Context, log *Logger, fields ...Field) context.Context { if log.isDiscard || len(fields) == 0 { return ctx @@ -37,12 +39,12 @@ func WithContext(ctx context.Context, log *Logger, fields ...Field) context.Cont return context.WithValue(ctx, contextKey{}, &ctxFields{b: b}) } -// FromContext returns a new Logger extended with any fields attached to ctx -// via WithContext. The context fields appear after the logger's own pre-rendered -// fields (set via With) and before per-call fields. +// FromContext returns a [Logger] extended with the fields attached to ctx by +// [WithContext]. The fields follow those added with [Logger.With] and precede +// those passed to the log methods. // -// If the logger discards output or ctx carries no fields, the receiver is -// returned unchanged. +// If l discards output or ctx carries no fields, FromContext returns l. +// The receiver is never modified. func (l *Logger) FromContext(ctx context.Context) *Logger { if l.isDiscard { return l diff --git a/ctx/ctx.go b/ctx/ctx.go index 7b00d53..288ed78 100644 --- a/ctx/ctx.go +++ b/ctx/ctx.go @@ -1,4 +1,5 @@ -// Package ctx implements log context convenience functions. +// Package ctx provides functions that create the [logger.Field] values passed +// to the log methods and [logger.Logger.With] of a [logger.Logger]. package ctx import ( @@ -12,140 +13,150 @@ import ( "go.opentelemetry.io/otel/trace" ) -// Str returns a string context field. +// Str returns a [logger.Field] with key k and the string value s. func Str(k, s string) logger.Field { return func(e *logger.Event) { e.AppendString(k, s) } } -// Strs returns a string slice context field. +// Strs returns a [logger.Field] with key k and the strings s as an array. func Strs(k string, s []string) logger.Field { return func(e *logger.Event) { e.AppendStrings(k, s) } } -// Bytes returns a byte slice context field. +// Bytes returns a [logger.Field] with key k and the bytes p as an array of +// integers, not as a string. func Bytes(k string, p []byte) logger.Field { return func(e *logger.Event) { e.AppendBytes(k, p) } } -// Bool returns a boolean context field. +// Bool returns a [logger.Field] with key k and the bool value b. func Bool(k string, b bool) logger.Field { return func(e *logger.Event) { e.AppendBool(k, b) } } -// Int returns an int context field. +// Int returns a [logger.Field] with key k and the int value i. func Int(k string, i int) logger.Field { return func(e *logger.Event) { e.AppendInt(k, int64(i)) } } -// Ints returns an int slice context field. +// Ints returns a [logger.Field] with key k and the ints a as an array. func Ints(k string, a []int) logger.Field { return func(e *logger.Event) { e.AppendInts(k, a) } } -// Int8 returns an int8 context field. +// Int8 returns a [logger.Field] with key k and the int8 value i. func Int8(k string, i int8) logger.Field { return func(e *logger.Event) { e.AppendInt(k, int64(i)) } } -// Int16 returns an int16 context field. +// Int16 returns a [logger.Field] with key k and the int16 value i. func Int16(k string, i int16) logger.Field { return func(e *logger.Event) { e.AppendInt(k, int64(i)) } } -// Int32 returns an int32 context field. +// Int32 returns a [logger.Field] with key k and the int32 value i. func Int32(k string, i int32) logger.Field { return func(e *logger.Event) { e.AppendInt(k, int64(i)) } } -// Int64 returns an int64 context field. +// Int64 returns a [logger.Field] with key k and the int64 value i. func Int64(k string, i int64) logger.Field { return func(e *logger.Event) { e.AppendInt(k, i) } } -// Uint returns a uint context field. +// Uint returns a [logger.Field] with key k and the uint value i. func Uint(k string, i uint) logger.Field { return func(e *logger.Event) { e.AppendUint(k, uint64(i)) } } -// Uint8 returns a uint8 context field. +// Uint8 returns a [logger.Field] with key k and the uint8 value i. func Uint8(k string, i uint8) logger.Field { return func(e *logger.Event) { e.AppendUint(k, uint64(i)) } } -// Uint16 returns a uint16 context field. +// Uint16 returns a [logger.Field] with key k and the uint16 value i. func Uint16(k string, i uint16) logger.Field { return func(e *logger.Event) { e.AppendUint(k, uint64(i)) } } -// Uint32 returns a uint32 context field. +// Uint32 returns a [logger.Field] with key k and the uint32 value i. func Uint32(k string, i uint32) logger.Field { return func(e *logger.Event) { e.AppendUint(k, uint64(i)) } } -// Uint64 returns a uint64 context field. +// Uint64 returns a [logger.Field] with key k and the uint64 value i. func Uint64(k string, i uint64) logger.Field { return func(e *logger.Event) { e.AppendUint(k, i) } } -// Float32 returns a float32 context field. +// Float32 returns a [logger.Field] with key k and the float32 value f. +// The value is converted to a float64 before it is rendered, so the output +// may show digits that are not significant in f. func Float32(k string, f float32) logger.Field { return func(e *logger.Event) { e.AppendFloat(k, float64(f)) } } -// Float64 returns a float64 context field. +// Float64 returns a [logger.Field] with key k and the float64 value f. func Float64(k string, f float64) logger.Field { return func(e *logger.Event) { e.AppendFloat(k, f) } } -// Error returns an error context field. +// Error returns a [logger.Field] with key k and the text of err. +// The field panics when it is applied if err is nil. func Error(k string, err error) logger.Field { return func(e *logger.Event) { e.AppendString(k, err.Error()) } } -// Err returns an error context field with the key set to "error". +// Err returns a [logger.Field] with key "error" and the text of err. +// The field panics when it is applied if err is nil. func Err(err error) logger.Field { return func(e *logger.Event) { e.AppendString("error", err.Error()) } } -// Stack return a stack string context field. +// Stack returns a [logger.Field] with key k and the call stack of the code +// that logged the message. +// +// Stack must be passed directly to a log method of a [logger.Logger]. Used +// in other ways, such as with [logger.Logger.With] or inside another +// [logger.Field], it does not report the caller's stack. func Stack(k string) logger.Field { return func(e *logger.Event) { cs := stack.Trace().TrimRuntime() @@ -157,7 +168,13 @@ func Stack(k string) logger.Field { } } -// Caller returns a caller string context field. +// Caller returns a [logger.Field] with key k and the "file:line" of the code +// that logged the message. The field is omitted if the location cannot be +// determined. +// +// Caller must be passed directly to a log method of a [logger.Logger]. Used +// in other ways, such as with [logger.Logger.With] or inside another +// [logger.Field], it does not report the caller's location. func Caller(k string) logger.Field { return func(e *logger.Event) { _, file, line, ok := runtime.Caller(3) @@ -167,34 +184,37 @@ func Caller(k string) logger.Field { } } -// Time returns a time context field. +// Time returns a [logger.Field] with key k and the time value t. func Time(k string, t time.Time) logger.Field { return func(e *logger.Event) { e.AppendTime(k, t) } } -// Duration returns a duration context field. +// Duration returns a [logger.Field] with key k and the duration value d. func Duration(k string, d time.Duration) logger.Field { return func(e *logger.Event) { e.AppendDuration(k, d) } } -// Interface returns an interface context field. +// Interface returns a [logger.Field] with key k and the value v, rendered as +// text. A nil v renders as null in JSON and as nothing in other formats. func Interface(k string, v any) logger.Field { return func(e *logger.Event) { e.AppendInterface(k, v) } } -// Span represents an open telemetry span. +// Span is the subset of the OpenTelemetry [trace.Span] interface used by +// [TraceID]. type Span interface { IsRecording() bool SpanContext() trace.SpanContext } -// TraceID returns an open telemetry trace ID context field. +// TraceID returns a [logger.Field] with key k and the OpenTelemetry trace ID +// of span. The field is omitted if span is not recording or has no trace ID. func TraceID(k string, span Span) logger.Field { if !span.IsRecording() || !span.SpanContext().HasTraceID() { return func(*logger.Event) {} diff --git a/doc.go b/doc.go index ec536fa..74dc8dd 100644 --- a/doc.go +++ b/doc.go @@ -1,14 +1,29 @@ -/* -Package logger implements a logging package. - -Example usage: - - log := logger.New(os.Stdout, logger.LogfmtFormat(), logger.Info) - - // Logger can have scoped context - log = log.With(ctx.Str("env", "prod")) - - // All messages can have a context - log.Error("connection error", ctx.Str("redis", conn.Name()), ctx.Int("timeout", conn.Timeout())) -*/ +// Package logger implements leveled, structured logging. +// +// A [Logger] writes one message per line to an [io.Writer], rendered by a +// [Formatter]: [JSONFormat], [LogfmtFormat] or [ConsoleFormat]. Messages carry +// key-value fields, created with the functions in the ctx subpackage, either +// attached to the Logger with [Logger.With] or passed with each message. +// Messages more verbose than the Logger's [Level] are dropped. +// +// A Logger may be used by multiple goroutines if its writer allows it; wrap +// writers that do not with [NewSyncWriter]. Fields can also travel in a +// [context.Context]; see [WithContext] and [Logger.FromContext]. +// +// Example usage: +// +// import ( +// "os" +// +// "github.com/hamba/logger/v2" +// "github.com/hamba/logger/v2/ctx" +// ) +// +// log := logger.New(os.Stdout, logger.LogfmtFormat(), logger.Info) +// +// // Logger can have scoped context +// log = log.With(ctx.Str("env", "prod")) +// +// // All messages can have a context +// log.Error("connection error", ctx.Str("redis", conn.Name()), ctx.Int("timeout", conn.Timeout())) package logger diff --git a/event.go b/event.go index 20307b8..d1b83f7 100644 --- a/event.go +++ b/event.go @@ -15,7 +15,10 @@ var eventPool = &sync.Pool{ }, } -// Event is a log event. +// Event is the log message that a [Field] adds its key-value pairs to. +// +// An Event is only valid during the call to the Field that receives it and +// must not be retained. type Event struct { fmtr Formatter buf *bytes.Buffer @@ -32,13 +35,13 @@ func putEvent(e *Event) { eventPool.Put(e) } -// AppendString appends a string to the event. +// AppendString appends the string s under key k. func (e *Event) AppendString(k, s string) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendString(e.buf, s) } -// AppendStrings appends strings to the event. +// AppendStrings appends the strings s as an array under key k. func (e *Event) AppendStrings(k string, s []string) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendArrayStart(e.buf) @@ -51,7 +54,7 @@ func (e *Event) AppendStrings(k string, s []string) { e.fmtr.AppendArrayEnd(e.buf) } -// AppendBytes appends bytes to the event. +// AppendBytes appends p as an array of integers under key k, not as a string. func (e *Event) AppendBytes(k string, p []byte) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendArrayStart(e.buf) @@ -64,19 +67,19 @@ func (e *Event) AppendBytes(k string, p []byte) { e.fmtr.AppendArrayEnd(e.buf) } -// AppendBool appends a bool to the event. +// AppendBool appends b under key k. func (e *Event) AppendBool(k string, b bool) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendBool(e.buf, b) } -// AppendInt appends an int to the event. +// AppendInt appends i under key k. func (e *Event) AppendInt(k string, i int64) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendInt(e.buf, i) } -// AppendInts appends ints to the event. +// AppendInts appends a as an array of integers under key k. func (e *Event) AppendInts(k string, a []int) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendArrayStart(e.buf) @@ -89,31 +92,32 @@ func (e *Event) AppendInts(k string, a []int) { e.fmtr.AppendArrayEnd(e.buf) } -// AppendUint appends a uint to the event. +// AppendUint appends i under key k. func (e *Event) AppendUint(k string, i uint64) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendUint(e.buf, i) } -// AppendFloat appends a float to the event. +// AppendFloat appends f under key k. func (e *Event) AppendFloat(k string, f float64) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendFloat(e.buf, f) } -// AppendTime appends a time to the event. -func (e *Event) AppendTime(k string, d time.Time) { +// AppendTime appends t under key k, rendered according to [TimeFormat] or +// [TimeFormatConsole]. +func (e *Event) AppendTime(k string, t time.Time) { e.fmtr.AppendKey(e.buf, k) - e.fmtr.AppendTime(e.buf, d) + e.fmtr.AppendTime(e.buf, t) } -// AppendDuration appends a duration to the event. +// AppendDuration appends d under key k. func (e *Event) AppendDuration(k string, d time.Duration) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendDuration(e.buf, d) } -// AppendInterface appends a interface to the event. +// AppendInterface appends the text form of v under key k. func (e *Event) AppendInterface(k string, v any) { e.fmtr.AppendKey(e.buf, k) e.fmtr.AppendInterface(e.buf, v) diff --git a/example_test.go b/example_test.go index a308943..85a421f 100644 --- a/example_test.go +++ b/example_test.go @@ -2,6 +2,8 @@ package logger_test import ( "context" + "fmt" + stdlog "log" "os" "github.com/hamba/logger/v2" @@ -12,12 +14,19 @@ func ExampleNew() { log := logger.New(os.Stdout, logger.LogfmtFormat(), logger.Info).With(ctx.Str("env", "prod")) log.Info("redis connection", ctx.Str("redis", "some redis name"), ctx.Int("timeout", 10)) + log.Debug("dropped, more verbose than Info") + + // Output: + // lvl=info msg="redis connection" env=prod redis="some redis name" timeout=10 } func ExampleSyncWriter() { log := logger.New(logger.NewSyncWriter(os.Stdout), logger.LogfmtFormat(), logger.Info).With(ctx.Str("env", "prod")) log.Info("redis connection", ctx.Str("redis", "some redis name"), ctx.Int("timeout", 10)) + + // Output: + // lvl=info msg="redis connection" env=prod redis="some redis name" timeout=10 } func ExampleWithContext() { @@ -27,4 +36,39 @@ func ExampleWithContext() { reqLog := log.FromContext(reqCtx) reqLog.Info("request handled", ctx.Int("status", 200)) + + // Output: + // lvl=info msg="request handled" svc=api req_id=abc-123 method=GET status=200 +} + +func ExampleLogger_Writer() { + log := logger.New(os.Stdout, logger.LogfmtFormat(), logger.Info) + + std := stdlog.New(log.Writer(logger.Warn), "", 0) + std.Println("disk almost full") + + // Output: + // lvl=warn msg="disk almost full" +} + +func ExampleLogger_WithTimestamp() { + log := logger.New(os.Stdout, logger.LogfmtFormat(), logger.Info) + + // The cancel function must be called to stop the clock. + cancel := log.WithTimestamp() + defer cancel() + + log.Info("started") +} + +func ExampleLevelFromString() { + lvl, err := logger.LevelFromString("dbug") + fmt.Println(lvl, err) + + _, err = logger.LevelFromString("verbose") + fmt.Println(err) + + // Output: + // dbug + // unknown level verbose } diff --git a/format.go b/format.go index c89bdd5..076272e 100644 --- a/format.go +++ b/format.go @@ -10,37 +10,70 @@ import ( ) const ( - // TimestampKey is the key used for timestamps. + // TimestampKey is the key of the timestamp in the output of [JSONFormat] + // and [LogfmtFormat]. TimestampKey = "ts" - // LevelKey is the key used for message levels. + // LevelKey is the key of the level in the output of [JSONFormat] + // and [LogfmtFormat]. LevelKey = "lvl" - // MessageKey is the key used for message descriptions. + // MessageKey is the key of the message in the output of [JSONFormat] + // and [LogfmtFormat]. MessageKey = "msg" ) -// Formatter represents a log message formatter. +// Formatter renders log messages into a buffer. +// +// For each message, a [Logger] calls AppendBeginMarker and WriteMessage, then +// AppendKey followed by one value method for each field, and finally +// AppendEndMarker and AppendLineBreak. +// +// Formatter uses a type from an internal package, so it can only be +// implemented within this module. Use [JSONFormat], [LogfmtFormat] or +// [ConsoleFormat] to obtain one. type Formatter interface { + // WriteMessage writes the timestamp, level and message that start a log + // line. A zero ts means the Logger has no timestamp and none is written. WriteMessage(buf *bytes.Buffer, ts time.Time, lvl Level, msg string) + // AppendBeginMarker writes any text that opens a log line. AppendBeginMarker(buf *bytes.Buffer) + // AppendEndMarker writes any text that closes a log line. AppendEndMarker(buf *bytes.Buffer) + // AppendLineBreak writes the text that terminates a log line. AppendLineBreak(buf *bytes.Buffer) + // AppendArrayStart writes any text that opens an array value. AppendArrayStart(buf *bytes.Buffer) + // AppendArraySep writes the text between two elements of an array value. AppendArraySep(buf *bytes.Buffer) + // AppendArrayEnd writes any text that closes an array value. AppendArrayEnd(buf *bytes.Buffer) + // AppendKey writes the separator before a field and the field key. + // It is followed by a call that writes the value. AppendKey(buf *bytes.Buffer, key string) + // AppendString writes s as a field value, escaping it as required. AppendString(buf *bytes.Buffer, s string) + // AppendBool writes b as a field value. AppendBool(buf *bytes.Buffer, b bool) + // AppendInt writes i as a field value. AppendInt(buf *bytes.Buffer, i int64) + // AppendUint writes i as a field value. AppendUint(buf *bytes.Buffer, i uint64) + // AppendFloat writes f as a field value. AppendFloat(buf *bytes.Buffer, f float64) + // AppendTime writes t as a field value. AppendTime(buf *bytes.Buffer, t time.Time) + // AppendDuration writes d as a field value. AppendDuration(buf *bytes.Buffer, d time.Duration) + // AppendInterface writes the text form of v as a field value. AppendInterface(buf *bytes.Buffer, v any) } type json struct{} -// JSONFormat formats a log line in json format. +// JSONFormat returns a [Formatter] that writes each message as a single-line +// JSON object. Times are rendered according to [TimeFormat], and values +// passed to AppendInterface are rendered as strings, or null for nil. +// +// Field keys are written as given and are not escaped. func JSONFormat() Formatter { return &json{} } @@ -136,7 +169,12 @@ func (j *json) AppendInterface(buf *bytes.Buffer, v any) { type logfmt struct{} -// LogfmtFormat formats a log line in logfmt format. +// LogfmtFormat returns a [Formatter] that writes each message as a line of +// space-separated key=value pairs. Times are rendered according to +// [TimeFormat]. Strings are quoted when they contain spaces, control +// characters, '=' or '"'. +// +// Field keys are written as given and are not escaped. func LogfmtFormat() Formatter { return &logfmt{} } @@ -270,7 +308,12 @@ func withColor(c color, buf *bytes.Buffer, fn func()) { type console struct{} -// ConsoleFormat formats a log line in a console format. +// ConsoleFormat returns a [Formatter] for reading in a terminal. Each message +// is a line with the time, the upper-case level and the message, followed by +// key=value pairs, coloured with ANSI escape codes. Times are rendered +// according to [TimeFormatConsole]. Strings are never quoted. +// +// Field keys are written as given and are not escaped. func ConsoleFormat() Formatter { return &console{} } diff --git a/logger.go b/logger.go index 5bc3842..7805dd5 100644 --- a/logger.go +++ b/logger.go @@ -8,22 +8,37 @@ import ( "time" ) -// TimeFormat is the format that times will be added in. +// TimeFormat is the layout used by [JSONFormat] and [LogfmtFormat] to render +// timestamps and time fields. The default, [TimeFormatUnix], renders integer +// seconds since the Unix epoch. Any other value is a layout as described in +// [time.Time.Format]. // -// TimeFormat defaults to unix time. +// TimeFormat is read on every write, so it must be set before any [Logger] +// is used. var TimeFormat = TimeFormatUnix -// TimeFormatConsole is the default format for the console formatter. +// TimeFormatConsole is the layout used by [ConsoleFormat] to render +// timestamps and time fields. It defaults to [time.Kitchen]. // -// TimeFormatConsole defaults to kitchen time. +// TimeFormatConsole is read on every write, so it must be set before any +// [Logger] is used. var TimeFormatConsole = time.Kitchen -// Time formats. const ( - TimeFormatUnix = "" + // TimeFormatUnix selects integer seconds since the Unix epoch. + TimeFormatUnix = "" + // TimeFormatISO8601 is the layout "2006-01-02T15:04:05-0700". TimeFormatISO8601 = "2006-01-02T15:04:05-0700" ) +// Level is the severity of a log message and the verbosity threshold of a +// [Logger]. +// +// Levels are ordered by verbosity: [Crit] is the least verbose and [Trace] the +// most. A Logger writes a message only if its level is no more verbose than +// the Logger's own level. +type Level int + // List of predefined log Levels. const ( Disabled Level = iota @@ -35,10 +50,11 @@ const ( Trace ) -// Level represents the predefined log level. -type Level int - -// LevelFromString converts a string to Level. +// LevelFromString returns the [Level] named by lvl. The names are "trace", +// "debug", "info", "warn", "error" and "crit", plus the short forms "trce", +// "dbug" and "eror" returned by [Level.String]. Names are case-sensitive. +// +// It returns an error for any other name, including "disabled". func LevelFromString(lvl string) (Level, error) { switch lvl { case "trace", "trce": @@ -58,7 +74,8 @@ func LevelFromString(lvl string) (Level, error) { } } -// String returns the string representation of the level. +// String returns the four-letter name of the level, such as "info" or "dbug". +// Disabled and undefined levels return "unkn". func (l Level) String() string { switch l { case Trace: @@ -78,10 +95,22 @@ func (l Level) String() string { } } -// Field is a context field. +// Field adds a key and value to an [Event]. The ctx package provides +// functions that create Fields, which are passed to [Logger.With] and the +// log methods of [Logger]. type Field func(*Event) -// Logger is a logger. +// Logger writes leveled, structured messages to an [io.Writer]. +// +// Each message is written with a single call to the writer's Write method. +// Messages more verbose than the Logger's [Level] are dropped, and errors +// returned by the writer are ignored. +// +// Methods may be called concurrently by multiple goroutines, provided the +// writer allows concurrent calls to Write (see [SyncWriter]), with the +// exception of [Logger.WithTimestamp], which must not be called concurrently +// with any other method. The zero value is not usable; create a Logger with +// [New]. type Logger struct { w io.Writer isDiscard bool @@ -91,7 +120,9 @@ type Logger struct { lvl Level } -// New creates a new Logger. +// New returns a [Logger] that writes messages formatted by fmtr to w and +// drops messages more verbose than lvl. If w is [io.Discard], the Logger +// does no work. func New(w io.Writer, fmtr Formatter, lvl Level) *Logger { isDiscard := w == io.Discard @@ -103,10 +134,16 @@ func New(w io.Writer, fmtr Formatter, lvl Level) *Logger { } } -// WithTimestamp adds a timestamp to each log lone. Sub-loggers -// will inherit the timestamp. +// WithTimestamp makes the Logger add a timestamp to each message. +// Loggers derived from it afterwards inherit the timestamp; those derived +// earlier do not. // -// WithTimestamp is not thread safe. +// Timestamps are refreshed periodically, so they may be slightly behind the +// time of the call. The returned cancel function stops the refreshing and +// must be called when the Logger is no longer needed; timestamps stop +// advancing once it has been called. If the Logger already has a timestamp, +// WithTimestamp changes nothing and returns a cancel function that does +// nothing. func (l *Logger) WithTimestamp() (cancel func()) { if l.timeFn != nil { return func() {} @@ -139,7 +176,10 @@ func (l *Logger) WithTimestamp() (cancel func()) { return cancel } -// With returns a new Logger with the given context. +// With returns a new [Logger] that adds ctx to every message, after any fields +// already added to l. The receiver is not modified. +// +// The fields are evaluated once, when With is called, not on each message. func (l *Logger) With(ctx ...Field) *Logger { e := newEvent(l.fmtr) defer putEvent(e) @@ -163,32 +203,33 @@ func (l *Logger) With(ctx ...Field) *Logger { } } -// Trace logs a trace message, intended for fine grained debug messages. +// Trace logs msg and the fields in ctx at the [Trace] level. +// The fields follow those added with [Logger.With]. func (l *Logger) Trace(msg string, ctx ...Field) { l.write(msg, Trace, ctx) } -// Debug logs a debug message. +// Debug logs msg and the fields in ctx at the [Debug] level. func (l *Logger) Debug(msg string, ctx ...Field) { l.write(msg, Debug, ctx) } -// Info logs an informational message. +// Info logs msg and the fields in ctx at the [Info] level. func (l *Logger) Info(msg string, ctx ...Field) { l.write(msg, Info, ctx) } -// Warn logs a warning message. +// Warn logs msg and the fields in ctx at the [Warn] level. func (l *Logger) Warn(msg string, ctx ...Field) { l.write(msg, Warn, ctx) } -// Error logs an error message. +// Error logs msg and the fields in ctx at the [Error] level. func (l *Logger) Error(msg string, ctx ...Field) { l.write(msg, Error, ctx) } -// Crit logs a critical message. +// Crit logs msg and the fields in ctx at the [Crit] level. func (l *Logger) Crit(msg string, ctx ...Field) { l.write(msg, Crit, ctx) } @@ -199,8 +240,10 @@ func (fn writerFunc) Write(p []byte) (n int, err error) { return fn(p) } -// Writer returns an io.Writer that writes at the given level. -// This can be used as a writer with the standard log library. +// Writer returns an [io.Writer] that logs each call to Write as one message at +// level lvl, for use with packages such as [log]. A single trailing newline +// is removed from the message. Write always reports len(p) bytes written and +// a nil error, including when the message is dropped. func (l *Logger) Writer(lvl Level) io.Writer { return writerFunc(func(p []byte) (n int, err error) { n = len(p) diff --git a/writer.go b/writer.go index 6df81a0..ab88121 100644 --- a/writer.go +++ b/writer.go @@ -5,18 +5,21 @@ import ( "sync" ) -// SyncWriter implements a writer that is synchronised with a lock. +// SyncWriter is an [io.Writer] that serialises calls to Write on the wrapped +// writer, so it is safe for concurrent use by multiple goroutines. The zero +// value is not usable; create a SyncWriter with [NewSyncWriter]. type SyncWriter struct { mu sync.Mutex w io.Writer } -// NewSyncWriter returns a synchronised writer. +// NewSyncWriter returns a [SyncWriter] that writes to w. func NewSyncWriter(w io.Writer) *SyncWriter { return &SyncWriter{w: w} } -// Write writes to the writer. +// Write writes p to the wrapped writer, allowing one call at a time, and +// returns the byte count and error of the wrapped writer. func (w *SyncWriter) Write(p []byte) (n int, err error) { w.mu.Lock() n, err = w.w.Write(p)