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
stages:
- test
#- build
- deploy
test:
stage: test
before_script:
- export PATH=$PATH:$GOPATH/bin
- go install gotest.tools/gotestsum@latest
script:
- gotestsum --junitfile tests.xml -- -coverprofile=coverage.txt -covermode atomic .
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
- go test -race
lint:
stage: test
image: golangci/golangci-lint
pages:
stage: deploy
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
## v0.4.1 (2022-06-03)
- Ensure all backends implement the interface `BACKEND`.
- `FileBackend` and `SyslogBackend` always returned errors for `Write`
operations.
## 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
- Uncompatible: log level names hace changed. They were fully
capitalized, only their first letter is capitalized now: DEBUG -> Debug,
INFO -> Info, etc.
- Uncompatible NoopBackend.Level() now returns DefaultLevel instead of Fatal
- Fix: creates new logger with level DefaultLevel instead of Debug
- Fix: FileBackend now properly closes the file befor reopening it (fixes a
potential file descriptor leak)
- Logger methods did not always acquire locks, causing race conditions

View file

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

View file

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

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
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"
)
const exitCodeFatal = 100
// Logger is a facility that writes logs to one or more backands (files,
// stdout/stderr, syslog, etc.) which can be configured independently
//
// Loggers are concurrent-safe.
type Logger struct {
mutex sync.Mutex
sync.Mutex
name string
backends []Backend
}
// NewLogger initializes a new Logger with no backend and with the default log level.
func NewLogger(name string) *Logger {
l := &Logger{
func NewLogger(name string) (l *Logger) {
l = &Logger{
name: name,
}
return l
return
}
// AddBackend add a new Backend to the logger. All set backends are kept.
func (l *Logger) AddBackend(b Backend) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.Lock()
defer l.Unlock()
l.backends = append(l.backends, b)
}
// SetBackend sets the backend list to the logger. Any existing backend will be lost.
func (l *Logger) SetBackend(b ...Backend) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.Lock()
defer l.Unlock()
l.backends = b
}
@ -52,11 +50,11 @@ func (l *Logger) SetLevel(level Level) {
}
type buffer struct {
logger *Logger
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))
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
// whose level is at least `level`.
// whose level is at least `level`
func (l *Logger) Log(level Level, m string) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.Lock()
defer l.Unlock()
r := NewRecord(l.name, level, m)
for _, backend := range l.backends {
if r.Level >= backend.Level() {
if err := backend.Write(r); err != nil {
//revive:disable-next-line:unhandled-error stop error handling recursion!
fmt.Fprintf(os.Stderr, "Cannot write logs: %v", err)
}
_ = backend.Write(r)
}
}
}
// Trace logs a message with the Trace 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.
// Debug logs a message with the Debug level
func (l *Logger) Debug(text string) {
l.Log(Debug, text)
}
// 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{}) {
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) {
l.Log(Info, text)
}
// 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{}) {
l.Info(fmt.Sprintf(text, args...))
}
// Notice logs a message with the Notice 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.
// Warning logs a message with the Warning level
func (l *Logger) Warning(text string) {
l.Log(Warning, text)
}
// 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{}) {
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) {
l.Log(Error, text)
}
// 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{}) {
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) {
l.Log(Critical, text)
}
// Criticalf formats the message with given args and logs the result with the.
// Critical level.
// Criticalf formats the message with given args and logs the result with the
// Critical level
func (l *Logger) Criticalf(text string, args ...interface{}) {
l.Critical(fmt.Sprintf(text, args...))
}
// Alert logs a message with the Alert 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.
// Fatal logs a message with the Fatal level
func (l *Logger) Fatal(text string) {
l.Log(Debug, text)
os.Exit(exitCodeFatal) //nolint:revive // This is wanted if fatal is called
os.Exit(100)
}
// 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{}) {
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.
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.
*/
package logging
import (
"errors"
"fmt"
"strings"
"sync"
)
//nolint:gochecknoglobals // designed this way
var (
loggers map[string]*Logger
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
//revive:disable:exported
//nolint: golint
const (
Trace Level = iota
Debug
Debug Level = iota
Info
Notice
Warning
Error
Critical
Alert
Fatal
DefaultLevel = Info
)
//revive:enable:exported
var levelNames = [6]string{"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"}
//nolint:gochecknoglobals // designed this way
var levelNames = [...]string{
"TRACE", "DEBUG", "INFO", "NOTICE", "WARNING",
"ERROR", "CRITICAL", "ALERT", "FATAL",
}
// Name returns the name of the log level.
// Name returns the name of the log level
func (l Level) Name() string {
return levelNames[l]
}
@ -71,8 +58,7 @@ func LevelByName(l string) (Level, error) {
return Level(pos), nil
}
}
return Debug, fmt.Errorf("unknown log level %q: %w", l, errInvalidLogLevel)
return Debug, fmt.Errorf("Invalid log level %s", l)
}
// 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
// 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).
func GetLogger(name string) *Logger {
// initializes one with the defaults (it logs to stdout with level debug)
func GetLogger(name string) (l *Logger) {
lock.Lock()
defer lock.Unlock()
if name == "" {
name = "default"
}
l, ok := loggers[name]
if !ok {
l = NewLogger(name)
@ -97,23 +82,21 @@ func GetLogger(name string) *Logger {
l.SetLevel(DefaultLevel)
loggers[name] = l
}
return l
}
func defaultFormatter(r *Record) string {
var defaultFormatter Formatter = func(r *Record) string {
return fmt.Sprintf("%s [%-8s] %s: %s\n",
r.Timestamp.Format("2006/01/02 15:04:05"), r.Level.Name(), r.Logger,
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))
}
//nolint:gochecknoinits // init is used by design
func init() {
loggers = map[string]*Logger{}
loggers = make(map[string]*Logger, 3)
logger := NewLogger("default")
backend := NewStdoutBackend()

View file

@ -3,14 +3,11 @@ package logging
import "testing"
func Test_LevelByName(t *testing.T) {
t.Parallel()
for _, levelName := range levelNames {
l, e := LevelByName(levelName)
if e != nil {
t.Errorf("level %s not recognized", levelName)
}
if l.Name() != levelName {
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
// generate the logged message.
// generate the logged message
type Record struct {
Logger string
Timestamp time.Time
@ -13,12 +13,13 @@ type Record struct {
Message string
}
// NewRecord creates a new record, setting its timestamp to time.Now().
func NewRecord(name string, l Level, m string) *Record {
return &Record{
// NewRecord creates a new record, setting its timestamp to time.Now()
func NewRecord(name string, l Level, m string) (r *Record) {
r = &Record{
Logger: name,
Level: l,
Message: m,
Timestamp: time.Now(),
}
return
}