Compare commits

..

No commits in common. "master" and "v0.3.0" have entirely different histories.

12 changed files with 129 additions and 1939 deletions

View file

@ -1,31 +1,21 @@
# This file is a template, and might need editing before it works on your project.
image: golang:latest image: golang:latest
stages: stages:
- test - test
#- build #- build
- deploy
test: test:
stage: test stage: test
before_script:
- export PATH=$PATH:$GOPATH/bin
- go install gotest.tools/gotestsum@latest
script: script:
- gotestsum --junitfile tests.xml -- -coverprofile=coverage.txt -covermode atomic . - go test -race
after_script:
- export PATH=$PATH:$GOPATH/bin
- go install github.com/boumenot/gocover-cobertura@latest
- gocover-cobertura < coverage.txt > coverage.xml
- go tool cover -func=coverage.txt | grep "total:"
coverage: '/total:\s+\(statements\)\s+(\d+.\d+\%)/'
artifacts:
reports:
junit: tests.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
lint: pages:
stage: test stage: deploy
image: golangci/golangci-lint
script: script:
- golangci-lint run - mkdir public
- echo "<body>go/log page</body>" >public/index.html
artifacts:
paths:
- public

File diff suppressed because it is too large Load diff

View file

@ -1,33 +1,11 @@
# go/logging Changelog # go/logging
## Unreleased ## Unreleased
## v0.4.1 (2022-06-03) - Uncompatible: log level names hace changed. They were fully
capitalized, only their first letter is capitalized now: DEBUG -> Debug,
- Ensure all backends implement the interface `BACKEND`. INFO -> Info, etc.
- `FileBackend` and `SyslogBackend` always returned errors for `Write` - Uncompatible NoopBackend.Level() now returns DefaultLevel instead of Fatal
operations. - Fix: creates new logger with level DefaultLevel instead of Debug
- Fix: FileBackend now properly closes the file befor reopening it (fixes a
## v0.4.0 (2022-05-31)
- Add three new log levels: `Trace`, `Notice` and `Alert` with the following
order: `Trace`, `Debug`, `Info`, `Notice`, `Warning`, `Error`, `Critical`,
`Alert`, `Fatal`.
As Syslog has no equivalent of `Trace`, it is mapped to `Debug`.
## v0.3.0 (2020-05-17)
### Incompatible Changes
- Log level names have changed. They were fully capitalized, only their first
letter is capitalized now: DEBUG -> Debug, INFO -> Info, etc.
- NoopBackend.Level() now returns DefaultLevel instead of Fatal
- New loggers are created with level `DefaultLevel` instead of `Debug`
- The `Backend` interface now has a `Close()` method, so that backends can free
the resources they use
### Fixes
- FileBackend now properly closes the file before reopening it (fixes a
potential file descriptor leak) potential file descriptor leak)
- Logger methods did not always acquire locks, causing race conditions

View file

@ -4,14 +4,13 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
) )
// Backend is the interface that specifies the methods that a backend must // Backend is the interface that specifies the methods that a backend must
// implement. // implement
type Backend interface { type Backend interface {
Write(*Record) error Write(*Record) error
SetFormatter(Formatter) SetFormatter(*Formatter)
SetLevel(Level) SetLevel(Level)
Level() Level Level() Level
Reopen() error Reopen() error
@ -22,94 +21,88 @@ type Backend interface {
// Backend to write in file-like objects // Backend to write in file-like objects
// //
var _ Backend = &FileBackend{}
// FileBackend is a backend that writes to a file. // FileBackend is a backend that writes to a file.
type FileBackend struct { type FileBackend struct {
formatter Formatter
l io.Writer l io.Writer
filepath string formatter *Formatter
level Level level Level
filepath string
} }
// NewStdoutBackend creates a new backend to write the logs on the standard // NewStdoutBackend creates a new backend to write the logs on the standard
// output. // output
func NewStdoutBackend() *FileBackend { func NewStdoutBackend() (b *FileBackend) {
return &FileBackend{ b = &FileBackend{
l: os.Stdout, l: os.Stdout,
formatter: defaultFormatter, formatter: &defaultFormatter,
} }
return
} }
// NewStderrBackend creates a new backend to write the logs on the error output. // NewStderrBackend creates a new backend to write the logs on the error output
func NewStderrBackend() *FileBackend { func NewStderrBackend() (b *FileBackend) {
return &FileBackend{ b = &FileBackend{
l: os.Stderr, l: os.Stderr,
formatter: defaultFormatter, formatter: &defaultFormatter,
} }
return
} }
// NewFileBackend creates a new backend to write the logs in a given file. // NewFileBackend creates a new backend to write the logs in a given file
func NewFileBackend(filename string) (*FileBackend, error) { func NewFileBackend(filename string) (*FileBackend, error) {
fd, err := os.OpenFile(filepath.Clean(filename), fd, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) //nolint: gosec
os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot open log file %s: %w", filename, err) return nil, fmt.Errorf("Cannot open log file %s: %w", filename, err)
} }
b := &FileBackend{ b := &FileBackend{
l: fd, l: fd,
formatter: defaultFormatter, formatter: &defaultFormatter,
filepath: filename, filepath: filename,
} }
return b, nil return b, nil
} }
// NewIoBackend creates a new backend to write the logs in a given io.Writer. // NewIoBackend creates a new backend to write the logs in a given io.Writer
func NewIoBackend(buf io.Writer) *FileBackend { func NewIoBackend(buf io.Writer) (b *FileBackend) {
return &FileBackend{ return &FileBackend{
l: buf, l: buf,
formatter: defaultFormatter, formatter: &defaultFormatter,
} }
} }
func (b FileBackend) Write(r *Record) error { func (b FileBackend) Write(r *Record) error {
text := b.formatter(r) text := (*b.formatter)(r)
_, err := io.WriteString(b.l, text) _, err := io.WriteString(b.l, text)
if err != nil { return err
return fmt.Errorf("cannot write logs: %w", err)
}
return nil
} }
// SetLevel changes the log level of the backend. // SetLevel changes the log level of the backend
func (b *FileBackend) SetLevel(l Level) { func (b *FileBackend) SetLevel(l Level) {
b.level = l b.level = l
} }
// Level returns the log level set for this backend. // Level returns the log level set for this backend
func (b *FileBackend) Level() Level { func (b *FileBackend) Level() Level {
return b.level return b.level
} }
// SetFormatter defines the formatter for this backend. // SetFormatter defines the formatter for this backend
func (b *FileBackend) SetFormatter(f Formatter) { func (b *FileBackend) SetFormatter(f *Formatter) {
b.formatter = f b.formatter = f
} }
// Reopen closes and reopens the file it writes to. It should be used after log // Reopen closes and reopens the file it writes to. It should be used after log
// rotation. // rotation
func (b *FileBackend) Reopen() error { func (b *FileBackend) Reopen() error {
if err := b.Close(); err != nil { if err := b.Close(); err != nil {
return err return err
} }
fd, err := os.OpenFile(b.filepath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600) fd, err := os.OpenFile(b.filepath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) //nolint: gosec
if err != nil { if err != nil {
return fmt.Errorf("cannot open log file %s: %w", b.filepath, err) return fmt.Errorf("Cannot open log file %s: %w", b.filepath, err)
} }
b.l = fd b.l = fd
@ -117,7 +110,7 @@ func (b *FileBackend) Reopen() error {
return nil return nil
} }
// Close closes the underlying file used by the backend. // Close closes the underlying file used by the backend
func (b *FileBackend) Close() error { func (b *FileBackend) Close() error {
if b.filepath == "" { if b.filepath == "" {
return nil return nil
@ -125,7 +118,7 @@ func (b *FileBackend) Close() error {
if c, ok := b.l.(io.Closer); ok { if c, ok := b.l.(io.Closer); ok {
if err := c.Close(); err != nil { if err := c.Close(); err != nil {
return fmt.Errorf("cannot close log file: %w", err) return err
} }
} }
@ -136,38 +129,36 @@ func (b *FileBackend) Close() error {
// Noop Backend // Noop Backend
// //
var _ Backend = &NoopBackend{} // NoopBackend does nothing and discards all log entries without writing them anywhere
// NoopBackend does nothing and discards all log entries without writing them anywhere.
type NoopBackend struct{} type NoopBackend struct{}
// NewNoopBackend creates a noop backend. // NewNoopBackend creates a noop backend
func NewNoopBackend() (*NoopBackend, error) { func NewNoopBackend() (Backend, error) {
return &NoopBackend{}, nil return &NoopBackend{}, nil
} }
// Write is a noop. // Write is a noop
func (*NoopBackend) Write(_ *Record) error { func (nb *NoopBackend) Write(r *Record) error {
return nil return nil
} }
// SetFormatter is a noop. // SetFormatter is a noop
func (*NoopBackend) SetFormatter(_ Formatter) {} func (nb *NoopBackend) SetFormatter(f *Formatter) {}
// SetLevel is a noop. // SetLevel is a noop
func (*NoopBackend) SetLevel(_ Level) {} func (nb *NoopBackend) SetLevel(level Level) {}
// Level always returns DefeultLevel. // Level always returns DefeultLevel
func (*NoopBackend) Level() Level { func (nb *NoopBackend) Level() Level {
return DefaultLevel return DefaultLevel
} }
// Reopen is a noop. // Reopen is a noop
func (*NoopBackend) Reopen() error { func (nb *NoopBackend) Reopen() error {
return nil return nil
} }
// Close is a noop. // Close is a noop
func (*NoopBackend) Close() error { func (nb *NoopBackend) Close() error {
return nil return nil
} }

View file

@ -1,114 +1,88 @@
//go:build !windows && !nacl && !plan9
// +build !windows,!nacl,!plan9 // +build !windows,!nacl,!plan9
package logging package logging
import ( import (
"errors"
"fmt" "fmt"
"log/syslog" "log/syslog"
"strings" "strings"
) )
var errUnknownFacility = errors.New("unknown facility")
// //
// Syslog Backend // Syslog Backend
// //
var _ Backend = &SyslogBackend{} // SyslogBackend writes the logs to a syslog system
// SyslogBackend writes the logs to a syslog system.
type SyslogBackend struct { type SyslogBackend struct {
w *syslog.Writer w *syslog.Writer
formatter Formatter formatter *Formatter
level Level level Level
} }
// NewSyslogBackend initializes a new connection to a syslog server with the // NewSyslogBackend initializes a new connection to a syslog server with the
// given facility. // given facility.
// tag can contain an identifier for the log stream. It defaults to os.Arg[0]. // tag can contain an identifier for the log stream. It defaults to os.Arg[0].
func NewSyslogBackend(facilityName, tag string) (*SyslogBackend, error) { func NewSyslogBackend(facilityName string, tag string) (Backend, error) {
f, err := facility(facilityName) f, err := facility(facilityName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
w, err := syslog.New(f, tag) w, err := syslog.New(f, tag)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot initialize syslog: %w", err) return nil, err
} }
sb := &SyslogBackend{ sb := &SyslogBackend{
w: w, w: w,
formatter: basicFormatter, formatter: &basicFormatter,
} }
return sb, nil return sb, nil
} }
// Write sends an entry to the syslog server. // Write sends an entry to the syslog server
func (sb *SyslogBackend) Write(r *Record) error { func (sb *SyslogBackend) Write(r *Record) (err error) {
var err error text := (*sb.formatter)(r)
text := sb.formatter(r)
switch r.Level { switch r.Level {
case Trace, Debug: case Debug:
err = sb.w.Debug(text) err = sb.w.Debug(text)
case Info: case Info:
err = sb.w.Info(text) err = sb.w.Info(text)
case Notice:
err = sb.w.Notice(text)
case Warning: case Warning:
err = sb.w.Warning(text) err = sb.w.Warning(text)
case Error: case Error:
err = sb.w.Err(text) err = sb.w.Err(text)
case Critical: case Critical:
err = sb.w.Crit(text) err = sb.w.Crit(text)
case Alert:
err = sb.w.Alert(text)
case Fatal: case Fatal:
err = sb.w.Emerg(text) err = sb.w.Emerg(text)
} }
return err
if err != nil {
return fmt.Errorf("cannot log to syslog: %w", err)
}
return nil
} }
// SetFormatter defines the formatter for this backend. // SetFormatter defines the formatter for this backend
func (sb *SyslogBackend) SetFormatter(f Formatter) { func (sb *SyslogBackend) SetFormatter(f *Formatter) {
sb.formatter = f sb.formatter = f
} }
// SetLevel changes the log level of the backend. // SetLevel changes the log level of the backend
func (sb *SyslogBackend) SetLevel(level Level) { func (sb *SyslogBackend) SetLevel(level Level) {
sb.level = level sb.level = level
} }
// Level returns the log level set for this backend. // Level returns the log level set for this backend
func (sb *SyslogBackend) Level() Level { func (sb *SyslogBackend) Level() Level {
return sb.level return sb.level
} }
// Reopen is a no-op. // Reopen is a no-op
func (*SyslogBackend) Reopen() error { func (sb *SyslogBackend) Reopen() error {
return nil return nil
} }
// Close closes the connection to the syslog daemon. // Close closes the connection to the syslog daemon
func (sb *SyslogBackend) Close() error { func (sb *SyslogBackend) Close() error {
if err := sb.w.Close(); err != nil { return sb.w.Close()
return fmt.Errorf("cannot close syslog: %w", err)
}
return nil
} }
//nolint:gochecknoglobals // global var is used by design
var facilities = map[string]syslog.Priority{ var facilities = map[string]syslog.Priority{
"kern": syslog.LOG_KERN, "kern": syslog.LOG_KERN,
"user": syslog.LOG_USER, "user": syslog.LOG_USER,
@ -135,7 +109,7 @@ var facilities = map[string]syslog.Priority{
func facility(name string) (syslog.Priority, error) { func facility(name string) (syslog.Priority, error) {
p, ok := facilities[strings.ToLower(name)] p, ok := facilities[strings.ToLower(name)]
if !ok { if !ok {
return 0, fmt.Errorf("facility '%s' does not exist: %w", name, errUnknownFacility) return 0, fmt.Errorf("Facility '%s' does not exist", name)
} }
return p, nil return p, nil

View file

@ -1,48 +0,0 @@
package logging
import (
"bytes"
"errors"
"testing"
"github.com/stretchr/testify/require"
)
type WriteErrorBuffer struct{}
func (*WriteErrorBuffer) Write(_ []byte) (int, error) {
return 0, errors.New("cannot write")
}
func TestFileBackendWrite(t *testing.T) {
t.Parallel()
t.Run("It should write the logs to the buffer", func(t *testing.T) {
t.Parallel()
buf := new(bytes.Buffer)
b := NewIoBackend(buf)
err := b.Write(&Record{
Level: Info,
Message: "my log line",
})
require.NoError(t, err)
require.Contains(t, buf.String(), "my log line")
})
t.Run("It should return an error if it cannot write the log", func(t *testing.T) {
t.Parallel()
buf := new(WriteErrorBuffer)
b := NewIoBackend(buf)
err := b.Write(&Record{
Level: Info,
Message: "my log line",
})
require.Error(t, err)
})
}

2
go.mod
View file

@ -1,5 +1,3 @@
module code.bcarlin.xyz/go/logging module code.bcarlin.xyz/go/logging
go 1.13 go 1.13
require github.com/stretchr/testify v1.7.1 // indirect

10
go.sum
View file

@ -1,10 +0,0 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -7,39 +7,37 @@ import (
"sync" "sync"
) )
const exitCodeFatal = 100
// Logger is a facility that writes logs to one or more backands (files, // Logger is a facility that writes logs to one or more backands (files,
// stdout/stderr, syslog, etc.) which can be configured independently // stdout/stderr, syslog, etc.) which can be configured independently
// //
// Loggers are concurrent-safe. // Loggers are concurrent-safe.
type Logger struct { type Logger struct {
mutex sync.Mutex sync.Mutex
name string name string
backends []Backend backends []Backend
} }
// NewLogger initializes a new Logger with no backend and with the default log level. // NewLogger initializes a new Logger with no backend and with the default log level.
func NewLogger(name string) *Logger { func NewLogger(name string) (l *Logger) {
l := &Logger{ l = &Logger{
name: name, name: name,
} }
return
return l
} }
// AddBackend add a new Backend to the logger. All set backends are kept. // AddBackend add a new Backend to the logger. All set backends are kept.
func (l *Logger) AddBackend(b Backend) { func (l *Logger) AddBackend(b Backend) {
l.mutex.Lock() l.Lock()
defer l.mutex.Unlock() defer l.Unlock()
l.backends = append(l.backends, b) l.backends = append(l.backends, b)
} }
// SetBackend sets the backend list to the logger. Any existing backend will be lost. // SetBackend sets the backend list to the logger. Any existing backend will be lost.
func (l *Logger) SetBackend(b ...Backend) { func (l *Logger) SetBackend(b ...Backend) {
l.mutex.Lock() l.Lock()
defer l.mutex.Unlock() defer l.Unlock()
l.backends = b l.backends = b
} }
@ -52,11 +50,11 @@ func (l *Logger) SetLevel(level Level) {
} }
type buffer struct { type buffer struct {
logger *Logger
level Level level Level
logger *Logger
} }
func (b *buffer) Write(p []byte) (int, error) { func (b *buffer) Write(p []byte) (n int, err error) {
b.logger.Log(b.level, string(p)) b.logger.Log(b.level, string(p))
return len(p), nil return len(p), nil
} }
@ -72,119 +70,82 @@ func (l *Logger) AsStdLog(level Level) *log.Logger {
} }
// Log sends a record containing the message `m` to the registered backends // Log sends a record containing the message `m` to the registered backends
// whose level is at least `level`. // whose level is at least `level`
func (l *Logger) Log(level Level, m string) { func (l *Logger) Log(level Level, m string) {
l.mutex.Lock() l.Lock()
defer l.mutex.Unlock() defer l.Unlock()
r := NewRecord(l.name, level, m) r := NewRecord(l.name, level, m)
for _, backend := range l.backends { for _, backend := range l.backends {
if r.Level >= backend.Level() { if r.Level >= backend.Level() {
if err := backend.Write(r); err != nil { _ = backend.Write(r)
//revive:disable-next-line:unhandled-error stop error handling recursion!
fmt.Fprintf(os.Stderr, "Cannot write logs: %v", err)
}
} }
} }
} }
// Trace logs a message with the Trace level. // Debug logs a message with the Debug level
func (l *Logger) Trace(text string) {
l.Log(Trace, text)
}
// Tracef formats the message with given args and logs the result with the
// Trace level.
func (l *Logger) Tracef(text string, args ...interface{}) {
l.Trace(fmt.Sprintf(text, args...))
}
// Debug logs a message with the Debug level.
func (l *Logger) Debug(text string) { func (l *Logger) Debug(text string) {
l.Log(Debug, text) l.Log(Debug, text)
} }
// Debugf formats the message with given args and logs the result with the // Debugf formats the message with given args and logs the result with the
// Debug level. // Debug level
func (l *Logger) Debugf(text string, args ...interface{}) { func (l *Logger) Debugf(text string, args ...interface{}) {
l.Debug(fmt.Sprintf(text, args...)) l.Debug(fmt.Sprintf(text, args...))
} }
// Info logs a message with the Info level. // Info logs a message with the Info level
func (l *Logger) Info(text string) { func (l *Logger) Info(text string) {
l.Log(Info, text) l.Log(Info, text)
} }
// Infof formats the message with given args and logs the result with the // Infof formats the message with given args and logs the result with the
// Info level. // Info level
func (l *Logger) Infof(text string, args ...interface{}) { func (l *Logger) Infof(text string, args ...interface{}) {
l.Info(fmt.Sprintf(text, args...)) l.Info(fmt.Sprintf(text, args...))
} }
// Notice logs a message with the Notice level. // Warning logs a message with the Warning level
func (l *Logger) Notice(text string) {
l.Log(Notice, text)
}
// Noticef formats the message with given args and logs the result with the
// Notice level.
func (l *Logger) Noticef(text string, args ...interface{}) {
l.Notice(fmt.Sprintf(text, args...))
}
// Warning logs a message with the Warning level.
func (l *Logger) Warning(text string) { func (l *Logger) Warning(text string) {
l.Log(Warning, text) l.Log(Warning, text)
} }
// Warningf formats the message with given args and logs the result with the // Warningf formats the message with given args and logs the result with the
// Warning level. // Warning level
func (l *Logger) Warningf(text string, args ...interface{}) { func (l *Logger) Warningf(text string, args ...interface{}) {
l.Warning(fmt.Sprintf(text, args...)) l.Warning(fmt.Sprintf(text, args...))
} }
// Error logs a message with the Error level. // Error logs a message with the Error level
func (l *Logger) Error(text string) { func (l *Logger) Error(text string) {
l.Log(Error, text) l.Log(Error, text)
} }
// Errorf formats the message with given args and logs the result with the // Errorf formats the message with given args and logs the result with the
// Error level. // Error level
func (l *Logger) Errorf(text string, args ...interface{}) { func (l *Logger) Errorf(text string, args ...interface{}) {
l.Error(fmt.Sprintf(text, args...)) l.Error(fmt.Sprintf(text, args...))
} }
// Critical logs a message with the Critical level. // Critical logs a message with the Critical level
func (l *Logger) Critical(text string) { func (l *Logger) Critical(text string) {
l.Log(Critical, text) l.Log(Critical, text)
} }
// Criticalf formats the message with given args and logs the result with the. // Criticalf formats the message with given args and logs the result with the
// Critical level. // Critical level
func (l *Logger) Criticalf(text string, args ...interface{}) { func (l *Logger) Criticalf(text string, args ...interface{}) {
l.Critical(fmt.Sprintf(text, args...)) l.Critical(fmt.Sprintf(text, args...))
} }
// Alert logs a message with the Alert level. // Fatal logs a message with the Fatal level
func (l *Logger) Alert(text string) {
l.Log(Alert, text)
}
// Alertf formats the message with given args and logs the result with the.
// Alert level.
func (l *Logger) Alertf(text string, args ...interface{}) {
l.Alert(fmt.Sprintf(text, args...))
}
// Fatal logs a message with the Fatal level.
func (l *Logger) Fatal(text string) { func (l *Logger) Fatal(text string) {
l.Log(Debug, text) l.Log(Debug, text)
os.Exit(100)
os.Exit(exitCodeFatal) //nolint:revive // This is wanted if fatal is called
} }
// Fatalf formats the message with given args and logs the result with the // Fatalf formats the message with given args and logs the result with the
// Fatal level. // Fatal level
func (l *Logger) Fatalf(text string, args ...interface{}) { func (l *Logger) Fatalf(text string, args ...interface{}) {
l.Fatal(fmt.Sprintf(text, args...)) l.Fatal(fmt.Sprintf(text, args...))
} }

View file

@ -12,53 +12,40 @@ package logging defines the following builtin backends:
A backend can safely be used by multiple loggers. A backend can safely be used by multiple loggers.
It is the caller's responsibility to call Close on backends when they are not It is the caller's responsability to call Close on backends when they are not
used anymore to free their resources. used anymore to free their resources.
*/ */
package logging package logging
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
) )
//nolint:gochecknoglobals // designed this way
var ( var (
loggers map[string]*Logger loggers map[string]*Logger
lock sync.Mutex lock sync.Mutex
errInvalidLogLevel = errors.New("invalid log level")
) )
// Level is the type of log levels. // Level is the type of log levels
type Level byte type Level byte
//revive:disable:exported //nolint: golint
const ( const (
Trace Level = iota Debug Level = iota
Debug
Info Info
Notice
Warning Warning
Error Error
Critical Critical
Alert
Fatal Fatal
DefaultLevel = Info DefaultLevel = Info
) )
//revive:enable:exported var levelNames = [6]string{"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"}
//nolint:gochecknoglobals // designed this way // Name returns the name of the log level
var levelNames = [...]string{
"TRACE", "DEBUG", "INFO", "NOTICE", "WARNING",
"ERROR", "CRITICAL", "ALERT", "FATAL",
}
// Name returns the name of the log level.
func (l Level) Name() string { func (l Level) Name() string {
return levelNames[l] return levelNames[l]
} }
@ -71,8 +58,7 @@ func LevelByName(l string) (Level, error) {
return Level(pos), nil return Level(pos), nil
} }
} }
return Debug, fmt.Errorf("Invalid log level %s", l)
return Debug, fmt.Errorf("unknown log level %q: %w", l, errInvalidLogLevel)
} }
// Formatter is the types of the functions that can be used to format a log // Formatter is the types of the functions that can be used to format a log
@ -80,15 +66,14 @@ func LevelByName(l string) (Level, error) {
type Formatter func(*Record) string type Formatter func(*Record) string
// GetLogger returns a logger given its name. if the logger does not exist, it // GetLogger returns a logger given its name. if the logger does not exist, it
// initializes one with the defaults (it logs to stdout with level INFO). // initializes one with the defaults (it logs to stdout with level debug)
func GetLogger(name string) *Logger { func GetLogger(name string) (l *Logger) {
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
if name == "" { if name == "" {
name = "default" name = "default"
} }
l, ok := loggers[name] l, ok := loggers[name]
if !ok { if !ok {
l = NewLogger(name) l = NewLogger(name)
@ -97,23 +82,21 @@ func GetLogger(name string) *Logger {
l.SetLevel(DefaultLevel) l.SetLevel(DefaultLevel)
loggers[name] = l loggers[name] = l
} }
return l return l
} }
func defaultFormatter(r *Record) string { var defaultFormatter Formatter = func(r *Record) string {
return fmt.Sprintf("%s [%-8s] %s: %s\n", return fmt.Sprintf("%s [%-8s] %s: %s\n",
r.Timestamp.Format("2006/01/02 15:04:05"), r.Level.Name(), r.Logger, r.Timestamp.Format("2006/01/02 15:04:05"), r.Level.Name(), r.Logger,
strings.TrimSpace(r.Message)) strings.TrimSpace(r.Message))
} }
func basicFormatter(r *Record) string { var basicFormatter Formatter = func(r *Record) string {
return fmt.Sprintf("%s: %s", r.Logger, strings.TrimSpace(r.Message)) return fmt.Sprintf("%s: %s", r.Logger, strings.TrimSpace(r.Message))
} }
//nolint:gochecknoinits // init is used by design
func init() { func init() {
loggers = map[string]*Logger{} loggers = make(map[string]*Logger, 3)
logger := NewLogger("default") logger := NewLogger("default")
backend := NewStdoutBackend() backend := NewStdoutBackend()

View file

@ -3,14 +3,11 @@ package logging
import "testing" import "testing"
func Test_LevelByName(t *testing.T) { func Test_LevelByName(t *testing.T) {
t.Parallel()
for _, levelName := range levelNames { for _, levelName := range levelNames {
l, e := LevelByName(levelName) l, e := LevelByName(levelName)
if e != nil { if e != nil {
t.Errorf("level %s not recognized", levelName) t.Errorf("level %s not recognized", levelName)
} }
if l.Name() != levelName { if l.Name() != levelName {
t.Errorf("expected '%s', got '%s'", levelName, l.Name()) t.Errorf("expected '%s', got '%s'", levelName, l.Name())
} }

View file

@ -5,7 +5,7 @@ import (
) )
// Record contains the data to be logged. It is passed to a formatter to // Record contains the data to be logged. It is passed to a formatter to
// generate the logged message. // generate the logged message
type Record struct { type Record struct {
Logger string Logger string
Timestamp time.Time Timestamp time.Time
@ -13,12 +13,13 @@ type Record struct {
Message string Message string
} }
// NewRecord creates a new record, setting its timestamp to time.Now(). // NewRecord creates a new record, setting its timestamp to time.Now()
func NewRecord(name string, l Level, m string) *Record { func NewRecord(name string, l Level, m string) (r *Record) {
return &Record{ r = &Record{
Logger: name, Logger: name,
Level: l, Level: l,
Message: m, Message: m,
Timestamp: time.Now(), Timestamp: time.Now(),
} }
return
} }