-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
89d47b4
commit 8f740b4
Showing
32 changed files
with
1,942 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,143 @@ | ||
# L | ||
|
||
L is a minimalist project-aware structured logger for Go. | ||
|
||
## Why? | ||
|
||
There are indeed already too many logging libraries out there. It makes a lot | ||
of sense to continue using them if they serve you well, and maybe even if they | ||
don't given the effort of changing something as low level as logging. | ||
|
||
However, logging has a evolved a lot since the standard library logging | ||
interface. First, structured logging has become the norm for newer | ||
applications, perhaps due to the nature of logging in cloud distributed systems | ||
such as Kubernetes. Second, as observed by | ||
[capnslog](https://github.com/coreos/capnslog), logging should serve the project | ||
it uses well: the main entry point should determine the logging configuration of | ||
its imports, and moreover do so in a way that is runtime configurable. | ||
|
||
Unfortunately, all the libraries out there seem to use the idea of the global | ||
logger, where this state can be changed, and often is, inside library code. | ||
|
||
|
||
L provides | ||
|
||
- Efficient structured logging, with reasonable syntax for creating nested structures | ||
incrementally. | ||
- Formatters and a simple formatting interface. | ||
- The ability for the main entry point to determine and set up configuration | ||
for all Loggers in all packages it depends on. | ||
- The ability for a package to define its own default configuration. | ||
- Support for _hooks_ aka MiddleWare. | ||
- An admin interface for manipulating labels in a Go process. | ||
|
||
## Working with L | ||
|
||
To use L in some package, simply import L and call L.New(cfg) with | ||
your desired configuration. This should normally done at the top level | ||
of each package once. | ||
|
||
## Basic structured logging | ||
|
||
Structured logging is about providing structured output, which is | ||
typically fairly unstructured actually and refers to anything that | ||
can be viewed as a `map[string]any`, where `any` is any ground type | ||
or another `map[string]any` or a slice of values, i.e. Go's representation | ||
of free form json objects. | ||
|
||
```go | ||
// log a field {"k": 77} | ||
var myL = L.New(L.NewConfig()) | ||
|
||
myL.Dict().Field("k", 77).Log() | ||
|
||
// add a bunch of stuff in a chain | ||
myL.Dict().Field("k", 3).Field("mini", "kube").Field("who", "me").Log() | ||
|
||
// add a tree structure '{"k": 3, "k2": [ "a", 4 ] }' | ||
myL.Dict().Field("k", 3).Set( | ||
"k2", | ||
myL.Array().Str("a").Int(4)). | ||
Log() | ||
|
||
// slower, but easier on the eyes | ||
myL.Fmt(`{"k": %d, %q: [ "%s", %d ] }`, 3, "key", "a", 4) | ||
|
||
``` | ||
|
||
## Custom Log project-native types | ||
|
||
L provides a means to avoid copying your native types at | ||
all their points they are used and logged, as this clearly is tedious. | ||
|
||
The idea is simple: define a json Unmarshaler for a type and then call | ||
``` | ||
v := &T{...} | ||
obj.JSON(v) | ||
``` | ||
|
||
Note that there are libraries to autogenerate marshalers, and also | ||
one can easily enough define an alternate marshaler for logging specific purposes. | ||
|
||
The distinction that L makes here is that if `v` does not implement json.Marshaler, | ||
then the program will fail to compile. The restriction helps to guarantee that logging | ||
is fast. | ||
|
||
|
||
|
||
### Overriding the configuration at the entry point | ||
|
||
The entry point can set the configuration for all | ||
imported packages, transitively by calling `L.Apply(...)`: | ||
``` | ||
import "github.com/scott-cotton/L" | ||
L.Apply(MyAppConfig()) | ||
``` | ||
|
||
In this context, the meaning of the labels is as follows: | ||
a label key is a regular expression which | ||
- a label value is an indication of whether or not labels matched | ||
with the corresponding key should be kept. It can contain package names | ||
- deletion takes precedence over insertion, so setting the value to | ||
false in the label map will always delete the corresponding matched | ||
keys. | ||
|
||
This allows for the application of labels accross all packages following | ||
a project policy. | ||
|
||
Applying a config allows setting middleware for pre- and post-processing. | ||
nil values for the middleware, .E, .F, and .W attributes will not overwrite | ||
package-specified ones. | ||
|
||
|
||
### Levelled Logging | ||
|
||
Traditionally, levelled logging uses an interface where the level is made explicit | ||
by a function call at each log site. For example, we often see | ||
|
||
``` | ||
log.Info().Msg("hello") | ||
``` | ||
|
||
or similar. Logging can happen at very high frequency in terms of lines of code, | ||
leading to a whole lot ".\<level\>()" repeats. | ||
|
||
Another problem with existing levelled logging options is that one must choose | ||
the levels for a whole project, making inter-project dependencies difficult when | ||
different projects use different levels. | ||
|
||
L proposes to treat this differently. L uses labels which can be turned on or | ||
off at the call site and at main entry points, together with middle ware to | ||
have efficient levelled logging. | ||
|
||
The idea is that for a project which wants to define its own levels, the project | ||
defines a project specific level key and associated integer values. | ||
|
||
[this test file](levels_test.go) contains a full working example. | ||
|
||
This mechanism is dynamic. At runtime, you can set the logging to a given | ||
level. For example, you can automatically increase the level if the frequency | ||
of errors goes above some threshold. | ||
|
||
L provides more general middleware for filtering logging. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package L_test | ||
|
||
import ( | ||
"io" | ||
"testing" | ||
|
||
"github.com/scott-cotton/L" | ||
) | ||
|
||
func BenchmarkBasic(b *testing.B) { | ||
L := L.New(&L.Config{ | ||
Labels: map[string]int{}, | ||
W: io.Discard, | ||
//F: &L.CLI{Fields: []string{"key3", "key0"}}, | ||
F: L.JSONFmter(), | ||
E: L.EPanic, | ||
}) | ||
b.StartTimer() | ||
for i := 0; i < b.N; i++ { | ||
L.Dict(). | ||
Field("key0", 22). | ||
Field("key2", false). | ||
Field("key3", "hello susan"). | ||
Log() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package L | ||
|
||
import ( | ||
"io" | ||
"os" | ||
"runtime" | ||
"strings" | ||
) | ||
|
||
// Config is the configuration of a logger. | ||
type Config struct { | ||
// Labels represents the set of labels associated with | ||
// a logger. | ||
Labels map[string]int | ||
|
||
// Pre is a sequence of Middlewares to pre-process | ||
// loggable objects. | ||
Pre []Middleware | ||
// Post is a sequence of Middlewares to post-process | ||
// loggable objects. Post middlewares are called | ||
// after .Log() and before .F.Fmt(). | ||
Post []Middleware | ||
// W is the writer for this logger. | ||
W io.Writer | ||
// F is a Fmter for the logger. | ||
F Fmter | ||
// E is a handler for any errors which occur during | ||
// formatting. | ||
E func(Logger, *Config, error) | ||
|
||
pkg string | ||
} | ||
|
||
// NewConfig returns a *Config with the associated labels. The labels are | ||
// subject to the following expansion rules | ||
// | ||
// - a label starting with a colon ':' is prefixed with package name | ||
func NewConfig(labels ...string) *Config { | ||
pc, _, _, _ := runtime.Caller(1) | ||
fn := runtime.FuncForPC(pc).Name() | ||
i := strings.LastIndexByte(fn, byte('.')) | ||
pkg := fn | ||
if i != -1 { | ||
pkg = fn[:i] | ||
} | ||
c := &Config{ | ||
Labels: map[string]int{}, | ||
W: os.Stderr, | ||
E: EPanic, | ||
pkg: pkg, | ||
} | ||
for _, lbl := range labels { | ||
key := lbl | ||
if lbl != "" && lbl[0] == '.' { | ||
key = pkg + lbl | ||
} | ||
c.Labels[key] = 0 | ||
} | ||
return c | ||
} | ||
|
||
// Clone clones the configuration c. | ||
func (c *Config) Clone() *Config { | ||
res := &Config{} | ||
*res = *c | ||
res.Pre = append([]Middleware{}, c.Pre...) | ||
res.Post = append([]Middleware{}, c.Post...) | ||
res.Labels = make(map[string]int, len(c.Labels)) | ||
res.pkg = c.pkg | ||
for k, v := range c.Labels { | ||
res.Labels[k] = v | ||
} | ||
return res | ||
} | ||
|
||
// Apply applies the configuration o to c. Fields | ||
// are copied over if they are not nil in o, otherwise | ||
// left untouched. Labels in o should not include | ||
// the package name, but if they start with '.', they | ||
// are expanded with the package name of 'c' when | ||
// copied to c's Labels. | ||
// | ||
// if a label in 'c', with any package name stripped, | ||
// is not in o, then it is removed from c. | ||
func (c *Config) Apply(o *Config) { | ||
if o.E != nil { | ||
c.E = o.E | ||
} | ||
if o.W != nil { | ||
c.W = o.W | ||
} | ||
if o.F != nil { | ||
c.F = o.F | ||
} | ||
if o.Pre != nil { | ||
c.Pre = append([]Middleware{}, o.Pre...) | ||
} | ||
if o.Post != nil { | ||
c.Post = append([]Middleware{}, o.Post...) | ||
} | ||
if o.Labels == nil { | ||
return | ||
} | ||
if c.Labels == nil { | ||
c.Labels = make(map[string]int, len(o.Labels)) | ||
} | ||
for k := range c.Labels { | ||
if _, ok := o.Labels[c.Unlocalize(k)]; !ok { | ||
delete(c.Labels, k) | ||
} | ||
} | ||
for k, v := range o.Labels { | ||
c.Labels[c.Localize(k)] = v | ||
} | ||
} | ||
|
||
func (c *Config) Unlocalize(label string) string { | ||
if label != "" && label[0] == '.' { | ||
return c.pkg + label | ||
} | ||
return label | ||
} | ||
|
||
func (c *Config) Localize(label string) string { | ||
if strings.HasPrefix(label, c.pkg+".") { | ||
return label[len(c.pkg)+1:] | ||
} | ||
return label | ||
} | ||
|
||
// CurrentRootConfig retrieves a clone of the configuration | ||
// from the last call to Apply, if any. | ||
func CurrentRootConfig() *Config { | ||
return root.ReadConfig() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package L_test | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
|
||
"github.com/scott-cotton/L" | ||
) | ||
|
||
var eW = bytes.NewBuffer(nil) | ||
var cfg = &L.Config{ | ||
W: eW, | ||
F: L.JSONFmter(), | ||
E: func(l L.Logger, _ *L.Config, e error) { | ||
fmt.Printf("logger couldn't log obj: %s", e.Error()) | ||
}, | ||
} | ||
|
||
var eL = L.New(cfg) | ||
|
||
func Example_configE() { | ||
// make a mistake and don't create real json. | ||
eL.Str("hello").Str("again").Log() | ||
fmt.Printf("%s\n", eW.String()) | ||
|
||
// Output: | ||
// logger couldn't log obj: invalid character ',' after top-level value | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Package L provides minimalist, efficient, project-aware structured logging. | ||
// | ||
// | ||
package L |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package L | ||
|
||
import "os" | ||
|
||
// EPanic is a Config.E that panics when there is an error. | ||
func EPanic(_ Logger, _ *Config, e error) { | ||
panic(e) | ||
} | ||
|
||
// EFatal is a Config.E that calls ELog and then exits. | ||
func EFatal(l Logger, c *Config, e error) { | ||
ELog(l, c, e) | ||
os.Exit(7) | ||
} | ||
|
||
// ELog is a Config.E that safely logs the error 'e' in a dict with key '"LE"'. | ||
func ELog(l Logger, _ *Config, e error) { | ||
ev := l.Dict() | ||
ev.Field("LE", e.Error()) | ||
l.Log(ev) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package L | ||
|
||
import "io" | ||
|
||
// a Fmter formats logs | ||
type Fmter interface { | ||
Fmt(w io.Writer, d []byte) error | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package L | ||
|
||
import "testing" | ||
|
||
func TestTableFmter(t *testing.T) { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/scott-cotton/L | ||
|
||
go 1.18 |
Oops, something went wrong.