README

Logberry Picture of a strawberry

Logberry is a structured logging package for Go services and applications. It is focused on generating logs, rather than managing them, and tries to be lightweight while capturing more semantics and structure than is typical, in both readable and easily parsed forms.

Build Status GoDoc

Introduction

Most log output libraries fall into one of two camps:

  • Super simple, with a global API that's really easy to use but has no structure beyond a component label and message level or type;
  • More complex, but focused on extensive formatting controls and/or various output serializers, pipes, aggregators, and managers.

Logberry is a bit different, and places more focus on what you're logging, rather than how. At the core, its log events are based around key/value pairs rather than arbitrary strings, much like Logrus. On top of that is a very light, optional structure for capturing execution stacks, delineating concurrent output, basic task timing, and other generic semantics that encourage better, more useful event log structure.

Supporting all that are some very simple concrete output options, much like many other libraries. In fact, those tools can be easily dropped in at this layer. Although it stands alone just fine, there's a good chance Logberry is complementary to, rather than competing with, your preferred log output engine.

Installation

Logberry can be installed directly through Go, like most Go libraries:

go get github.com/BellerophonMobile/logberry

Minimal Example

At its most minimal, it can be used much like most logging interfaces, except with structured data:

package main

import (
	"github.com/BellerophonMobile/logberry"
)

func main() {
	
	logberry.Main.Info("Demo is functional")

	logberry.Main.Info("Computed data", logberry.D{"X": 23, "Label": "priority"})

	logberry.Main.Failure("Arbritrary failure")

  logberry.Std.Stop()

}

In a terminal this produces the output:

Colored Logberry terminal output.

Note that the output has been spaced by default to work reasonably on both wide and standard terminals, in the latter case implicitly placing the identifiers and structured data on the line following the primary human text.

A simple switch to JSON output produces:

JSON Logberry terminal output.

This is already significant as the structured output promotes better reporting and easier log extraction of critical data. Also note that the error has automatically included the source code location.

Small Example

Besides structured event data, Logberry has a very basic notion of program structure, represented by Task objects. A small example:

package main

import (
	"github.com/BellerophonMobile/logberry"
//	"os"
)

func main() {

	// Uncomment this and "os" import for JSON output
	// logberry.Std.SetOutputDriver(logberry.NewJSONOutput(os.Stdout))

	// Report build information; a script generates buildmetadata
	logberry.BuildMetadataEvent(logberry.Main, buildmetadata)

	// Report that the program is initialized & running
	logberry.Main.Ready()

	// Create some structured application data and log it
	var data = struct {
		DataLabel string
		DataInt   int
	}{"alpha", 9}

	logberry.Main.Info("Reporting some data", data)

	// Create a program component---a long-running, multi-use entity.
	computerlog := logberry.Main.Component("computer")

	// Execute a task within that component, which may fail
	task := computerlog.Task("Compute numbers", &data)
	res, err := somecomputation()
	if err != nil {
		task.Error(err)
		return
	}
	task.Success(logberry.D{"Result": res})

	// Generate an application specific event reporting some other data
	var req = struct {
		User string
	}{"tjkopena"}

	computerlog.Event("request", "Received request", req)

	// Run a function under the component
	if e := arbitraryfunc(computerlog); e != nil {
		// Handle the error here
	}

	// The component ends
	computerlog.Finalized()

	// The program shuts down
	logberry.Main.Stopped()

	// Wait for all log messages to be output
	logberry.Std.Stop()

}

func somecomputation() (int, error) {
	return 7, nil // errors.New("Could not compute")
	// return 7, errors.New("Could not compute")
}

func arbitraryfunc(component *logberry.Task) error {

	// Start a long-running task, using Begin() to log start & begin timer
	task := component.Task("Arbitrary computation")

	// Report some intermediate progress
	task.Info("Intermediate progress", logberry.D{"Best": 9})

	// An error has occurred out of nowhere!  Log & return an error
	// noting that this task has failed, data associated with the error,
	// wrapping the underlying cause, and noting this source location
	return task.Failure("Random unrecoverable error",
		logberry.D{"Bounds": "x-axis"})

}

Note that the buildmetadata object is in a separate file, generated by a Logberry utility script.

In a terminal this produces the output:

Colored Logberry terminal output.

In the JSON output this looks as follows:

JSON Logberry terminal output.

Of note in this log:

  • Verbose, automatically generated build information identifies all (Git) repositories found in the host project folder as well as when, where, and by who the binary was built.
  • Every event is situated within a uniquely identified Task, and the hierarchical relationship between Tasks is recorded. Together these enable the ready decoupling of interleaved events generated by parallel goroutines and the reconstruction of a causal chain of computation.
  • Events are generated in a systematized fashion that promotes outputting all relevant data for either errors or success at its conclusion, without messy and duplicative marshaling code.
  • Long running tasks are automatically timed.
  • Common program events such as configuration, start, and errors are all identified, as well as application specific event types. Associated data is captured and provided in structured form.

Read More

Links to documentation:

  • Release Blog Post --- Quick introduction; use the comments here for questions
  • Concepts --- Top level concepts and use of Logberry.
  • GoDocs --- Automatically generated API docs.
  • Motivations --- Lengthy discussion on the design rationale behind Logberry.
  • Related Work --- Links to and notes on some other logging packages.
  • Examples
    • Minimal --- Standard flat Info(), etc. calls
    • Small --- Basic use of Tasks, D, JSON output

Changelog

  • 2016/11/01: Release 2.5 After a fair bit of use and thought, we decided Logberry was still pretty solid and just needed some cleanup. So the API was trimmed down, structure consolidated to a single threadsafe Root, errors made less verbose, and some other fixes and tweaks made.
  • 2015/06/22: Release 2.0! The API has been conceptually simplified, errors structured, and underlying code improved.
  • 2014/10/30: Release 1.0! Though definitely not mature at all, we consider Logberry to be usable.

License

Logberry is provided under the open source MIT license:

The MIT License (MIT)

Copyright (c) 2014, 2015, 2016 Bellerophon Mobile

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Documentation

Overview

Package logberry implements a structured logging framework. It is focused on generating logs, rather than managing them, and tries to be lightweight while capturing more semantics and structure than is typical, in readable and easily parsed forms.

There are four central concepts/objects/interfaces:

D              - Data to be published with an event.
Task           - A component, function, or logic path generating events.
OutputDriver   - Serializer for publishing events.
Root           - An interface between Tasks and OutputDrivers.

Also important are two less fundamental but included concepts/objects:

Error          - A generic structured error report.
BuildMetadata  - A simple representation of the build environment.

Higher level documentation is available from the repository and README:

https://github.com/BellerophonMobile/logberry

Index

Constants

View Source
const (
	BEGIN         string = "begin"
	END           string = "end"
	CONFIGURATION string = "configuration"
	READY         string = "ready"
	STOPPED       string = "stopped"
	INFO          string = "info"
	SUCCESS       string = "success"
	WARNING       string = "warning"
	ERROR         string = "error"
)

These strings are common classes of events, used in the Task event generation functions to identify types of occurrences.

Variables

This section is empty.

Functions

func IsError

func IsError(e error, code ...string) bool

IsError checks if the given error is a Logberry Error tagged with any of the given codes, returning true if so and false otherwise.

Types

type D

type D map[string]interface{}

D is a convenience type to be used for quickly structuring simple data to be logged.

type DBuilder

type DBuilder interface {
	D() D
}

A DBuilder is a type that can return logberry data when logged.

type Error

type Error struct {

	// An optional identifier for differentiating classes of errors
	Code string

	// Human-oriented description of the fault
	Message string

	// Inputs, parameters, and other data associated with the fault
	Data EventDataMap

	// The source code file and line number where the error occurred
	Source *Position

	// Optional link to a preceding error underlying the fault
	Cause error `logberry:"quiet"`

	// Whether or not this error has already been reported
	Reported bool `logberry:"quiet"`
}

Error captures structured information about a fault.

func NewError

func NewError(msg string, data ...interface{}) *Error

NewError generates a new Error capturing the given human-oriented message and optionally structured data associated with this fault. The source code position to be reported by this Error is the point at which NewError was called.

func WrapError

func WrapError(msg string, err error, data ...interface{}) *Error

WrapError generates a new Error capturing the given human-oriented message, a preceding error which caused this higher level fault, and optionally structured data associated with this fault. The source code position to be reported by this Error is the point at which WrapError was called.

func (*Error) Error

func (e *Error) Error() string

Error returns a human-oriented serialization of the error. It does not report the wrapped cause, if any. That must be retrieved and reported manually.

func (*Error) Locate

func (e *Error) Locate(skip int)

Locate sets the source code position to be reported with this error as that point where the Locate call is made. It should not generally be necessary to invoke this manually when using Logberry.

func (*Error) SetCode

func (e *Error) SetCode(code string) *Error

SetCode associates the error with a particular error class string.

func (*Error) String

func (e *Error) String() string

String returns a human-oriented serialization of the error. It is the same as Error().

type ErrorListener

type ErrorListener interface {
	Error(err error)
}

An ErrorListener is registered to Roots and notified of internal logging errors. Examples include an inability to write to disk, or contact a logging server. That notification could be utilized to prompt the administrator in some way or take other action. It is an error with unspecified behavior to add an ErrorListener to more than one Root simultaneously.

type Event

type Event struct {
	TaskID   uint64
	ParentID uint64

	Component string

	Event   string
	Message string
	Data    EventDataMap

	Timestamp time.Time
}

Event captures an annotated occurrence or message, a log entry.

type EventData

type EventData interface {
	WriteTo(io.Writer)
}

func Copy

func Copy(data interface{}) EventData

type EventDataBool

type EventDataBool bool

func (EventDataBool) WriteTo

func (x EventDataBool) WriteTo(out io.Writer)

type EventDataFloat64

type EventDataFloat64 float64

func (EventDataFloat64) WriteTo

func (x EventDataFloat64) WriteTo(out io.Writer)

type EventDataInt64

type EventDataInt64 int64

func (EventDataInt64) WriteTo

func (x EventDataInt64) WriteTo(out io.Writer)

type EventDataMap

type EventDataMap map[string]EventData

func Aggregate

func Aggregate(data []interface{}) EventDataMap

func (EventDataMap) Aggregate

func (x EventDataMap) Aggregate(data interface{}) EventDataMap

func (EventDataMap) String

func (x EventDataMap) String() string

func (EventDataMap) WriteTo

func (x EventDataMap) WriteTo(out io.Writer)

type EventDataSlice

type EventDataSlice []EventData

func (EventDataSlice) WriteTo

func (x EventDataSlice) WriteTo(out io.Writer)

type EventDataString

type EventDataString string

func (EventDataString) WriteTo

func (x EventDataString) WriteTo(out io.Writer)

type EventDataUInt64

type EventDataUInt64 uint64

func (EventDataUInt64) WriteTo

func (x EventDataUInt64) WriteTo(out io.Writer)

type JSONOutput

type JSONOutput struct {
	// contains filtered or unexported fields
}

JSONOutput is an OutputDriver that writes log events in JSON.

func NewJSONOutput

func NewJSONOutput(w io.Writer) *JSONOutput

NewJSONOutput creates a new JSONOutput targeted at the given Writer. DifferentialTime defaults to false.

func (*JSONOutput) Attach

func (x *JSONOutput) Attach(root *Root)

Attach notifies the OutputDriver of its Root. It should only be called by a Root.

func (*JSONOutput) Detach

func (x *JSONOutput) Detach()

Detach notifies the OutputDriver that it has been removed from its Root. It should only be called by a root.

func (*JSONOutput) Event

func (x *JSONOutput) Event(event *Event)

Event outputs a generated log entry, as called by a Root or a chaining OutputDriver.

type OutputDriver

type OutputDriver interface {
	Attach(root *Root)
	Detach()
	Event(event *Event)
}

An OutputDriver is registered to Roots and receives log events to export, e.g., writing to disk, screen, or sending to a server. To do so, an OutputDriver is created and then passed to the AddOutputDriver function of a Root. That Root will then call the OutputDriver's Attach() function to notify it of its context. Unless specifically noted otherwise by the implementation, it is an error with unspecified behavior to add an OutputDriver instance to more than one Root simultaneously.

type Position

type Position struct {
	File string
	Line int
}

type Root

type Root struct {
	// contains filtered or unexported fields
}

Root pushes events to OutputDrivers in a thread safe and receipt ordered fashion but in a separate, dedicated goroutine. This may be useful for logging outputs that may take some time, e.g., pushing to a logging server. At the conclusion of the program Stop should be called on the root to ensure that all of its events are flushed before terminating.

var Std *Root

Std is the default Root created at startup.

func NewRoot

func NewRoot(buffer int) *Root

NewRoot creates a new Root. The buffer parameter indicates the size of the channel buffer connecting event generation to outputs. The goroutine that creates the Root should defer a call to Stop() to ensure that all events are pushed.

func (*Root) AddErrorListener

func (x *Root) AddErrorListener(listener ErrorListener)

AddErrorListener includes the given listener among those to which internal logging errors are reported.

func (*Root) AddOutputDriver

func (x *Root) AddOutputDriver(driver OutputDriver)

AddOutputDriver includes the given additional output in those to which this Root forwards events. This is not thread safe with event generation, drivers are assumed to be attached in serial at startup.

func (*Root) ClearErrorListeners

func (x *Root) ClearErrorListeners()

ClearErrorListeners removes all of the registered listeners.

func (*Root) ClearOutputDrivers

func (x *Root) ClearOutputDrivers()

ClearOutputDrivers removes all of the currently registered outputs. This is not thread safe with event generation, drivers are assumed to be managed in serial at startup.

func (*Root) Component

func (x *Root) Component(component string, data ...interface{}) *Task

Component creates a new top level Task under this Root, representing a grouping of related functionality.

func (*Root) InternalError

func (x *Root) InternalError(err error)

InternalError reports an internal logging error. It is generally to be used only by OutputDrivers.

func (*Root) SetErrorListener

func (x *Root) SetErrorListener(listener ErrorListener)

SetErrorListener makes the given listener the only one for this Root. It is identical to calling x.ClearErrorListeners() and then x.AddErrorListener(listener).

func (*Root) SetOutputDriver

func (x *Root) SetOutputDriver(driver OutputDriver)

SetOutputDriver makes the given driver the only output for this root. It is identical to calling x.ClearOutputDrivers() and then x.AddOutputDriver(driver).

func (*Root) Stop

func (x *Root) Stop()

Stop shuts down the Root. Its internal channel is closed, and newly generated log events no longer forwarded to output drivers. Any previously buffered events are processed before Stop exits.

func (*Root) Task

func (x *Root) Task(activity string, data ...interface{}) *Task

Task creates a new top level Task under this Root, representing a particular line of activity.

type StdErrorListener

type StdErrorListener struct{}

func (*StdErrorListener) Error

func (x *StdErrorListener) Error(err error)

type Task

type Task struct {
	// contains filtered or unexported fields
}

Task represents a particular component, function, or activity. In general a Task is meant to be used within a single thread of execution, and the calling code is responsible for managing any concurrent manipulation.

var Main *Task

Main is the default Task created at startup, roughly intended to represent main program execution.

func (*Task) AddData

func (x *Task) AddData(data ...interface{}) *Task

AddData incorporates the given data into that associated and reported with this Task. This call does not generate a log event. The host Task is passed through as the return. Among other things, this function is useful to silently accumulate data into the Task as it proceeds, to be reported when it concludes.

func (*Task) Component

func (x *Task) Component(component string, data ...interface{}) *Task

Component creates a new Task object representing related long-lived functionality, rather than a directed, tightly scoped line of computation. Parameter component should be a short lowercase string identifying the class, module, or other component that this Task represents. The activity text of this Task is set to be "Component " + component.

func (*Task) Error

func (x *Task) Error(cause error, data ...interface{}) *Error

Error generates an error log event reporting an unrecoverable fault in an activity or component. If the given error is not a Logberry Error that has already been logged then it will be reported. An error is returned wrapping the original error with a message reporting that the Task's activity has failed. Continuing to use the Task is discouraged. The variadic data parameter is aggregated as a D and embedded in the generated error. It and the data permanently associated with the Task is reported with the event. The reported source code position of the generated task error is adjusted to be the event invocation.

func (*Task) Event

func (x *Task) Event(event string, msg string, data ...interface{})

Event generates a user-specified log event. Parameter event tags the class of the event, generally a short lowercase whitespace-free identifier. A human-oriented text message is given as the msg parameter. This should generally be static, short, use sentence capitalization but no terminating punctuation, and not itself include any data, which is better left to the structured data. The variadic data parameter is aggregated as a D and reported with the event, as is the data permanently associated with the Task. The given data is not associated to the Task permanently.

func (*Task) Failure

func (x *Task) Failure(msg string, data ...interface{}) *Error

Failure generates an error log event reporting an unrecoverable fault. Failure and Error are essentially the same, the difference being that Failure is the first point of fault while Error takes an underlying error typically returned from another function or component. An error is returned reporting that the activity or component represented by the Task has failed due to the underlying cause given in the message. Continuing to use the Task is discouraged. The variadic data parameter is aggregated as a D and embedded in the generated task error. It and the data permanently associated with the Task is reported with the event. The reported source code position of the generated task error is adjusted to be the event invocation.

func (*Task) Finalized

func (x *Task) Finalized(data ...interface{})

Finalized generates an end log event reporting that the component the Task represents has ceased. It is generally intended to be used for components, while Success is used for discrete activities. Continuing to use the Task is discouraged. The variadic data parameter is aggregated as a D and reported with the event, as is the data permanently associated with the Task. The given data is not associated to the Task permanently.

func (*Task) Info

func (x *Task) Info(msg string, data ...interface{})

Info generates an informational log event. A human-oriented text message is given as the msg parameter. This should generally be static, short, use sentence capitalization but no terminating punctuation, and not itself include any data, which is better left to the structured data. The variadic data parameter is aggregated as a D and reported with the event, as is the data permanently associated with the Task. The given data is not associated to the Task permanently.

func (*Task) Ready

func (x *Task) Ready(data ...interface{})

Ready generates a ready log event reporting that the activity or component the Task represents is initialized and prepared to begin. The variadic data parameter is aggregated as a D and reported with the event, as is the data permanently associated with the Task. The given data is not associated to the Task permanently.

func (*Task) Stopped

func (x *Task) Stopped(data ...interface{})

Stopped generates a stopped log event reporting that the activity or component the Task represents has paused or shutdown. The variadic data parameter is aggregated as a D and reported with the event, as is the data permanently associated with the Task. The given data is not associated to the Task permanently.

func (*Task) Success

func (x *Task) Success(data ...interface{}) error

Success generates a success log event reporting that the activity the Task represents has concluded successfully. It always returns nil. Continuing to use the Task is discouraged. The variadic data parameter is aggregated as a D and reported with the event, as is the data permanently associated with the Task. The given data is not associated to the Task permanently.

func (*Task) Task

func (x *Task) Task(activity string, data ...interface{}) *Task

Task creates a new sub-task. Parameter activity should be a short natural language description of the work that the Task represents, without any terminating punctuation.

func (*Task) Warning

func (x *Task) Warning(msg string, data ...interface{})

Warning generates a warning log event indicating that a fault was encountered but the task is proceeding acceptably. This should generally be static, short, use sentence capitalization but no terminating punctuation, and not itself include any data, which is better left to the structured data. The variadic data parameter is aggregated as a D and reported with the event, as is the data permanently associated with the Task. The given data is not associated to the Task permanently.

func (*Task) WrapError

func (x *Task) WrapError(msg string, cause error, data ...interface{}) *Error

type TextOutput

type TextOutput struct {
	Program string

	Color bool

	IDOffset   int
	DataOffset int
	// contains filtered or unexported fields
}

TextOutput is an OutputDriver that writes out log events in a structured but more or less human readable form. It has the following public properties:

Program                    String label of the executing program.

Color                      Set to true/false to enable/disable
                           outputting terminal color codes as
                           part of formatting log entries.
                           Defaults to false except for when
                           constructed via NewStdOutput and
                           NewErrOutput as below, in which case
                           it defaults to true iff the underlying
                           streams are terminals.

IDOffset                   The column at which to start printing
                           identifying information.

DataOffset                 The column at which to start printing
                           event data.

The default offsets are designed to wrap well on either 80 column or very wide terminals, generally putting each event on one or two lines respectively.

func NewErrOutput

func NewErrOutput(program string) *TextOutput

NewErrOutput creates a new TextOutput attached to stderr.

func NewStdOutput

func NewStdOutput(program string) *TextOutput

NewStdOutput creates a new TextOutput attached to stdout.

func NewTextOutput

func NewTextOutput(w io.Writer, program string) *TextOutput

NewTextOutput creates a new TextOutput attached to the given writer.

func (*TextOutput) Attach

func (o *TextOutput) Attach(root *Root)

Attach notifies the OutputDriver of its Root. It should only be called by a Root.

func (*TextOutput) Detach

func (o *TextOutput) Detach()

Detach notifies the OutputDriver that it has been removed from its Root. It should only be called by a root.

func (*TextOutput) Event

func (o *TextOutput) Event(event *Event)

Event outputs a generated log entry, as called by a Root or a chaining OutputDriver.

Directories

Path Synopsis
env
examples
Package terminal provides platform specific terminal detection functions.
Package terminal provides platform specific terminal detection functions.