Dead simple, super fast, zero allocation and modular logger for Golang

Build Status codecov Go Report Card Go doc MIT License

Onelog

Onelog is a dead simple but very efficient JSON logger. It is one of the fastest JSON logger out there. Also, it is one of the logger with the lowest allocation.

It gives more control over log levels enabled by using bitwise operation for setting levels on a logger.

It is also modular as you can add a custom hook, define level text values, level and message keys.

Go 1.9 is required as it uses a type alias over gojay.Encoder.

It is named onelog as a reference to zerolog and because it sounds like One Love song from Bob Marley :)

Get Started

go get github.com/francoispqt/onelog

Basic usage:

import "github.com/francoispqt/onelog"

func main() {
    // create a new Logger
    // first argument is an io.Writer
    // second argument is the level, which is an integer
    logger := onelog.New(
        os.Stdout, 
        onelog.ALL, // shortcut for onelog.DEBUG|onelog.INFO|onelog.WARN|onelog.ERROR|onelog.FATAL,
    )
    logger.Info("hello world !") // {"level":"info","message":"hello world"}
}

Levels

Levels are ints mapped to a string. The logger will check if level is enabled with an efficient bitwise &(AND), if disabled, it returns right away which makes onelog the fastest when running disabled logging with 0 allocs and less than 1ns/op. See benchmarks

When creating a logger you must use the | operator with different levels to toggle bytes.

Example if you want levels INFO and WARN:

logger := onelog.New(
    os.Stdout, 
    onelog.INFO|onelog.WARN,
)

This allows you to have a logger with different levels, for example you can do:

var logger *onelog.Logger

func init() {
    // if we are in debug mode, enable DEBUG lvl
    if os.Getenv("DEBUG") != "" {
        logger = onelog.New(
            os.Stdout, 
            onelog.ALL, // shortcut for onelog.DEBUG|onelog.INFO|onelog.WARN|onelog.ERROR|onelog.FATAL
        )
        return
    }
    logger = onelog.New(
        os.Stdout, 
        onelog.INFO|onelog.WARN|onelog.ERROR|onelog.FATAL,
    )
}

Available levels:

  • onelog.DEBUG
  • onelog.INFO
  • onelog.WARN
  • onelog.ERROR
  • onelog.FATAL

You can change their textual values by doing, do this only once at runtime as it is not thread safe:

onelog.LevelText(onelog.INFO, "INFO")

Hook

You can define a hook which will be run for every log message.

Example:

logger := onelog.New(
    os.Stdout, 
    onelog.ALL,
)
logger.Hook(func(e onelog.Entry) {
    e.String("time", time.Now().Format(time.RFC3339))
})
logger.Info("hello world !") // {"level":"info","message":"hello world","time":"2018-05-06T02:21:01+08:00"}

Context

Context allows enforcing a grouping format where all logs fields key-values pairs from all logging methods (With, Info, Debug, InfoWith, InfoWithEntry, ...etc) except for values from using logger.Hook, will be enclosed in giving context name provided as it's key. For example using a context key "params" as below

logger := onelog.NewContext(
    os.Stdout, 
    onelog.INFO|onelog.WARN,
    "params"
)

logger.InfoWithFields("breaking news !", func(e onelog.Entry) {
    e.String("userID", "123455")
}) 

// {"level":"info","message":"breaking news !", "params":{"userID":"123456"}}

This principle also applies when inheriting from a previous created logger as below

parentLogger := onelog.New(
    os.Stdout, 
    onelog.INFO|onelog.WARN,
)


logger := parentLogger.WithContext("params")
logger.InfoWithFields("breaking news !", func(e onelog.Entry) {
    e.String("userID", "123455")
}) 

// {"level":"info","message":"breaking news !", "params":{"userID":"123456"}}

You can always reset the context by calling WithContext("") to create a no-context logger from a context logger parent.

Logging

Without extra fields

Logging without extra fields is easy as:

logger := onelog.New(
    os.Stdout, 
    onelog.ALL,
)
logger.Debug("i'm not sure what's going on") // {"level":"debug","message":"i'm not sure what's going on"}
logger.Info("breaking news !") // {"level":"info","message":"breaking news !"}
logger.Warn("beware !") // {"level":"warn","message":"beware !"}
logger.Error("my printer is on fire") // {"level":"error","message":"my printer is on fire"}
logger.Fatal("oh my...") // {"level":"fatal","message":"oh my..."}

With extra fields

Logging with extra fields is quite simple, specially if you have used gojay:

logger := onelog.New(
    os.Stdout, 
    onelog.ALL,
)

logger.DebugWithFields("i'm not sure what's going on", func(e onelog.Entry) {
    e.String("string", "foobar")
    e.Int("int", 12345)
    e.Int64("int64", 12345)
    e.Float("float64", 0.15)
    e.Bool("bool", true)
    e.Err("err", errors.New("someError"))
    e.ObjectFunc("user", func(e Entry) {
        e.String("name", "somename")
    })
}) 
// {"level":"debug","message":"i'm not sure what's going on","string":"foobar","int":12345,"int64":12345,"float64":0.15,"bool":true,"err":"someError","user":{"name":"somename"}}

logger.InfoWithFields("breaking news !", func(e onelog.Entry) {
    e.String("userID", "123455")
}) 
// {"level":"info","message":"breaking news !","userID":"123456"}

logger.WarnWithFields("beware !", func(e onelog.Entry) {
    e.String("userID", "123455")
}) 
// {"level":"warn","message":"beware !","userID":"123456"}

logger.ErrorWithFields("my printer is on fire", func(e onelog.Entry) {
    e.String("userID", "123455")
}) 
// {"level":"error","message":"my printer is on fire","userID":"123456"}

logger.FatalWithFields("oh my...", func(e onelog.Entry) {
    e.String("userID", "123455")
}) 
// {"level":"fatal","message":"oh my...","userID":"123456"}

Alternatively, you can use the chain syntax:

logger.InfoWith("foo bar").
    Int("testInt", 1).
    Int64("testInt64", 2).
    Float("testFloat", 1.15234).
    String("testString", "string").
    Bool("testBool", true).
    ObjectFunc("testObj", func(e Entry) {
        e.Int("testInt", 100)
    }).
    Object("testObj2", testObj). // implementation of gojay.MarshalerJSONObject
    Array("testArr", testArr). // implementation of gojay.MarshalerJSONArray
    Err("testErr", errors.New("my printer is on fire !")).
    Write() // don't forget to call this method! 

Accumulate context

You can create get a logger with some accumulated context that will be included on all logs created by this logger.

To do that, you must call the With method on a logger. Internally it creates a copy of the current logger and returns it.

Example:

logger := onelog.New(
    os.Stdout, 
    onelog.ALL,
).With(func(e onelog.Entry) {
    e.String("userID", "123456")
})

logger.Info("user logged in") // {"level":"info","message":"user logged in","userID":"123456"}

logger.Debug("wtf?") // {"level":"debug","message":"wtf?","userID":"123456"}

logger.ErrorWithFields("Oops", func(e onelog.Entry) {
    e.String("error_code", "ROFL")
}) // {"level":"error","message":"oops","userID":"123456","error_code":"ROFL"}

Change levels txt values, message and/or level keys

You can change globally the levels values by calling the function:

onelog.LevelText(onelog.INFO, "INFO")

You can change the key of the message by calling the function:

onelog.MsgKey("msg")

You can change the key of the level by calling the function:

onelog.LevelKey("lvl")

Beware, these changes are global (affects all instances of the logger). Also, these function should be called only once at runtime to avoid any data race issue.

Benchmarks

For thorough benchmarks please see the results in the bench suite created by the author of zerolog here: https://github.com/rs/logbench

The benchmarks data presented below is the one from Uber's benchmark suite where we added onelog.

Benchmarks are here: https://github.com/francoispqt/zap/tree/onelog-bench/benchmarks

Disabled Logging

ns/op bytes/op allocs/op
Zap 8.73 0 0
zerolog 2.45 0 0
logrus 12.1 16 1
onelog 0.74 0 0

Disabled with fields

ns/op bytes/op allocs/op
Zap 208 768 5
zerolog 68.7 128 4
logrus 721 1493 12
onelog 1.31 0 0
onelog-chain 68.2 0 0

Logging basic message

ns/op bytes/op allocs/op
Zap 205 0 0
zerolog 135 0 0
logrus 1256 1554 24
onelog 84.8 0 0

Logging basic message and accumulated context

ns/op bytes/op allocs/op
Zap 276 0 0
zerolog 141 0 0
logrus 1256 1554 24
onelog 82.4 0 0

Logging message with extra fields

ns/op bytes/op allocs/op
Zap 1764 770 5
zerolog 1210 128 4
logrus 13211 13584 129
onelog 971 128 4
onelog-chain 1030 128 4
Owner
Francois Parquet
Site Reliability Engineer
Francois Parquet
Comments
  • feat: exit after Fatal, FatalWithFields, and FatalWith chained entries.

    feat: exit after Fatal, FatalWithFields, and FatalWith chained entries.

    Hey,

    As of now, Fatal calls do not exit from the app.

    Comparing with other loggers mentioned in the benchmark table, I found that all of them have an implementation for the same. Considering this, it would be a great addition to have in onelog as well.

    • Logrus: https://github.com/sirupsen/logrus/blob/7d8d63893b994a654b3bc8d16ffd7fb6b9f884a4/logger.go#L221
    • Zap: https://github.com/uber-go/zap/blob/f4243df2af592a2e0ccdccb77e1ad44f78d735e5/internal/exit/exit.go
    • Zerolog: https://github.com/rs/zerolog/blob/1c6d99b45538810b3e120bc5a1c29fc019918225/log/log.go#L68

    Please see my proposed API for the same.

  • Onelog out of sync with most recent Gojay changes

    Onelog out of sync with most recent Gojay changes

    Bonjour Francois, First off, I like this project! Especially chaining.

    It looks like some recent changes to Gojay broke Onelog.

    Perhaps all that needs to be changed is to update to new names in Onelog?

    Currently what I'm seeing when trying to go get:

    ➜  go get github.com/francoispqt/onelog
    # github.com/francoispqt/onelog
    ../../francoispqt/onelog/entry.go:62:37: undefined: gojay.MarshalerObject
    ../../francoispqt/onelog/entry.go:68:36: undefined: gojay.MarshalerArray
    ../../francoispqt/onelog/entry.go:152:37: undefined: gojay.MarshalerObject
    ../../francoispqt/onelog/entry.go:161:36: undefined: gojay.MarshalerArray
    
  • Default io.Writer output

    Default io.Writer output

    Hello,

    Nice project, I'm using it extensively. I have written a wrapper to go around the New function in order to safely initialise the io.Writer interface.

    Have you thought about checking for a nil io.Writer interface in the constructor, and applying ioutil.Discard as a deafult?

  • Omit Nil Errors

    Omit Nil Errors

    Currently, you get a nil pointer dereference error if you pass a nil error to the Err() functions. Instead, I propose to check for nils in this specific case and omitting nil errors conditionally.

  • Rename entry to ChainEntry.

    Rename entry to ChainEntry.

    The DebugWith and related functions currently return the unexposed entry type. This PR exposes the chain entry as ChainEntry, which allows users to wrap these commands.

    For example the following is now possible:

    type interface MyLogEntry {
        String("key", "value") MyLogEntry
    }
    
    type onelogEntry struct {
        entry onelog.ChainEntry
    }
    
    func (e *onelogEntry) String(k string, v string) MyLogEntry {
        return &onelogEntry{entry: e.entry.String(k, v)}
    }
    

    For users that want to abstract away the actual log implementation that is being used.

  • fix: panic in case NewContext is used to initialize logger with Fatal

    fix: panic in case NewContext is used to initialize logger with Fatal

    Hey,

    I missed adding ExitFn to NewContext() causing a panic.

    Have added a patch to fix it along with a scoped exit function which checks if ExitFn is nil (since it is exposed) and calls os.Exit as a fallback.

    Regards

  • Logger.With behavior

    Logger.With behavior

    Logger.With currently copies some fields of the Logger, does not copy hook or ctx.

    Proposed behavior: copy all fields of the Logger, add to existing ctx. This is how zerolog works. Code:

    func (l *Logger) copy() *Logger {
    	nL := Logger{
    		levels: l.levels,
    		w:      l.w,
    		hook:   l.hook, // ADDED
    	}
    	return &nL
    }
    
    // With copies the current Logger and adds it a given context by running func f.
    func (l *Logger) With(f func(Entry)) *Logger {
    	nL := l.copy()
    	e := Entry{}
    	enc := gojay.NewEncoder(nL.w)
    	e.enc = enc
    	enc.AppendByte(' ')
    	enc.AppendBytes(l.ctx) // ADDED
    	f(e)
    	b := enc.Buf()
    	nL.ctx = make([]byte, len(b[1:]))
    	copy(nL.ctx, b[1:])
    	enc.Release()
    	return nL
    }
    

    It allows you to create multiple levels of derived loggers. Ideally you would also be able to add multiple hook functions, rather than replacing the single one.

  • ORCA-619: Add context based logging to onelog

    ORCA-619: Add context based logging to onelog

    PR adds context logging where grouping of log fields can be done using the WithContext(contextName string) signature. Allowing the following possibilities:

    parentLogger = Log.New(os.Stdout, INFO)
    
    serviceLogger = parentLogger.WithContextKey("context")
    serviceLogger.INFOWithFields("ready to deploy").Int("id", 2343)
    // => { message: "ready to deploy", context: {id: 2343}}
    
    orderLogger = serviceLogger.With(func(e onelog.Entry){ e.Int("r", 20) })
    orderLogger.INFOWithFields("ready to deploy").Int("id", 2343)
    // => { message: "ready to deploy", context:{id: 2343, r:20}}
    
    orderLogger = serviceLogger.Hook(func(e onelog.Entry){ e.Int("r", 20) })
    orderLogger.INFOWithFields("ready to deploy").Int("id", 2343)
    // => { message: "ready to deploy", r:20, context:{id: 2343}}
    
  • default output ioutil.Discard

    default output ioutil.Discard

    if a nil io.Writer is provided, then use ioutil.Discard which will discard all output. This will avoid a panic when writing to a nil interface

    https://github.com/francoispqt/onelog/issues/13

  • Windows color

    Windows color

    Using this file:

    package main
    import (
       "github.com/fatih/color"
       "github.com/francoispqt/onelog"
       "os"
    )
    func main() {
       onelog.LevelText(onelog.INFO, color.CyanString("INFO"))
       aa := onelog.New(os.Stdout, onelog.ALL)
       aa.Info("bbbbb")
    }
    

    I get this result:

    {"level":" [36mINFO [0m","message":"bbbbb"}
    
alog is a dependency free, zero/minimum memory allocation JSON logger with extensions
alog is a dependency free, zero/minimum memory allocation JSON logger with extensions

Alog (c) 2020-2021 Gon Y Yi. https://gonyyi.com. MIT License Version 1.0.0 Intro Alog was built with a very simple goal in mind: Support Tagging (and

Dec 13, 2021
🪵 A dead simple, pretty, and feature-rich logger for golang
🪵 A dead simple, pretty, and feature-rich logger for golang

?? lumber ?? A dead simple, pretty, and feature-rich logger for golang ?? Install ?? Logging Functions lumber.Success() lumber.Info() lumber.Debug() l

Jul 20, 2022
A simple and super power logger for golang
A simple and super power logger for golang

The most powerfull and faster logger for golang powered by DC ?? What is this? W

Oct 18, 2022
Logger - Simple logger without written with std pkg

Go-Logger Simple usage is: package main

Jan 2, 2022
Convenient Logger interface and std logger wrapper

Convenient logger interface and wrapper around std logger Interface type Logger interface { Error(err error) Debugf(format string, args ...interface

Nov 28, 2021
Logger - A thin wrapper of uber-go/zap logger for personal project

a thin wraper of uber-go/zap logger for personal project 0. thanks uber-go/zap B

Sep 17, 2022
A powerful zero-dependency json logger.

ZKits Logger Library About This package is a library of ZKits project. This is a zero-dependency standard JSON log library that supports structured JS

Dec 14, 2022
Golog is a logger which support tracing and other custom behaviors out of the box. Blazing fast and simple to use.

GOLOG Golog is an opinionated Go logger with simple APIs and configurable behavior. Why another logger? Golog is designed to address mainly two issues

Oct 2, 2022
Fast, zero config web endpoint change monitor
Fast, zero config web endpoint change monitor

web monitor fast, zero config web endpoint change monitor. for comparing responses, a selected list of http headers and the full response body is stor

Nov 17, 2022
A Simple logger for golang

go-logger Installation go get github.com/mo-taufiq/go-logger Quickstart package main import ( gologger "github.com/mo-taufiq/go-logger" ) func main

Dec 14, 2022
Cloudprober is a monitoring software that makes it super-easy to monitor availability and performance of various components of your system.

Cloudprober is a monitoring software that makes it super-easy to monitor availability and performance of various components of your system. Cloudprobe

Dec 30, 2022
Simple logger for Go programs. Allows custom formats for messages.
Simple logger for Go programs. Allows custom formats for messages.

go-logger A simple go logger for easy logging in your programs. Allows setting custom format for messages. Preview Install go get github.com/apsdehal/

Dec 17, 2022
Simple Yet Powerful Logger

sypl sypl provides a Simple Yet Powerful Logger built on top of the Golang sypl. A sypl logger can have many Outputs, and each Output is responsible f

Sep 23, 2022
simple concurrent logger

XMUS-LOGGER pure golang logger compatible with golang io standards. USAGE : logOptions := logger.LoggerOptions{ LogLevel: 6, // read more about lo

Aug 1, 2022
A simple logger API.

flog a simple logger API for Go program that save logs into a file. NOTE: This package is provided "as is" with no guarantee. Use it at your own risk

May 14, 2022
A simple Go JSON logger.

logger A simple JSON logger for Go. It uses a context.Context to store values which will then be logged along with each message. It is possible to rec

Jul 25, 2022
Simple Proof of Concept REST event logger.

REST Event Logger PoC I am working on this project intermittently. I have set myself a time limit of ~3hrs which includes the time to acquire and adap

Feb 10, 2022
A feature-rich and easy to use logger for golang
A feature-rich and easy to use logger for golang

A feature-rich and easy to use logger for golang ?? Install ?? Common Logs lumber.Success() lumber.Info() lumber.Debug() lumber.Warning()

Dec 31, 2022
HTTP request logger for Golang
HTTP request logger for Golang

Horus ?? Introduction Horus is a request logger and viewer for Go. It allows developers log and view http requests made to their web application. Inst

Dec 27, 2022