├── script ├── fmt └── test ├── context_test.go ├── LICENSE ├── statter_test.go ├── loggers.go ├── timer.go ├── context.go ├── loggers_test.go ├── README.md ├── statter.go ├── timer_test.go ├── grohl.go ├── format.go ├── errors_test.go ├── format_test.go ├── errors.go └── doc.go /script/fmt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | gofmt -w -l *.go 4 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | script/fmt 2 | go test -race -v . 3 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestContextMerge(t *testing.T) { 8 | orig := NewContext(nil) 9 | 10 | orig.Add("a", 1) 11 | orig.Add("b", 1) 12 | 13 | merged := orig.Merge(Data{"b": 2, "c": 3}) 14 | 15 | AssertData(t, merged, "a=1", "b=2", "c=3") 16 | AssertLog(t, orig, "a=1", "b=1") 17 | } 18 | 19 | func TestContextStatterPrefix(t *testing.T) { 20 | ctx1 := NewContext(nil) 21 | ctx2 := NewContext(nil) 22 | ctx3 := ctx1.New(nil) 23 | AssertString(t, "", ctx1.StatterBucket) 24 | AssertString(t, "", ctx2.StatterBucket) 25 | AssertString(t, "", ctx3.StatterBucket) 26 | 27 | ctx1.SetStatter(nil, 1.0, "abc") 28 | AssertString(t, "abc", ctx1.StatterBucket) 29 | AssertString(t, "", ctx2.StatterBucket) 30 | AssertString(t, "", ctx3.StatterBucket) 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 rick olson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /statter_test.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestLogsCounter(t *testing.T) { 9 | s, buf := setupLogger(t) 10 | s.Counter(1.0, "a", 1, 2) 11 | buf.AssertLine("metric=a", "count=1") 12 | buf.AssertLine("metric=a", "count=2") 13 | buf.AssertEOF() 14 | } 15 | 16 | func TestLogsTiming(t *testing.T) { 17 | s, buf := setupLogger(t) 18 | dur1, _ := time.ParseDuration("15ms") 19 | dur2, _ := time.ParseDuration("3s") 20 | 21 | s.Timing(1.0, "a", dur1, dur2) 22 | buf.AssertLine("metric=a", "timing=15") 23 | buf.AssertLine("metric=a", "timing=3000") 24 | buf.AssertEOF() 25 | } 26 | 27 | func TestLogsGauge(t *testing.T) { 28 | s, buf := setupLogger(t) 29 | s.Gauge(1.0, "a", "1", "2") 30 | buf.AssertLine("metric=a", "gauge=1") 31 | buf.AssertLine("metric=a", "gauge=2") 32 | buf.AssertEOF() 33 | } 34 | 35 | var suffixTests = []string{"abc", "abc."} 36 | 37 | func TestSetsBucketSuffix(t *testing.T) { 38 | s, _ := setupLogger(t) 39 | for _, prefix := range suffixTests { 40 | s.StatterBucket = prefix 41 | s.StatterBucketSuffix("def") 42 | if s.StatterBucket != "abc.def" { 43 | t.Errorf("bucket is wrong after prefix %s: %s", prefix, s.StatterBucket) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /loggers.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | // IoLogger assembles the key/value pairs into a line and writes it to any 10 | // io.Writer. This expects the writers to be threadsafe. 11 | type IoLogger struct { 12 | stream io.Writer 13 | AddTime bool 14 | } 15 | 16 | func NewIoLogger(stream io.Writer) *IoLogger { 17 | if stream == nil { 18 | stream = os.Stdout 19 | } 20 | 21 | return &IoLogger{stream, true} 22 | } 23 | 24 | // Log writes the assembled log line. 25 | func (l *IoLogger) Log(data Data) error { 26 | line := fmt.Sprintf("%s\n", BuildLog(data, l.AddTime)) 27 | _, err := l.stream.Write([]byte(line)) 28 | return err 29 | } 30 | 31 | // ChannelLogger sends the key/value data to a channel. This is useful when 32 | // loggers are in separate goroutines. 33 | type ChannelLogger struct { 34 | channel chan Data 35 | } 36 | 37 | func NewChannelLogger(channel chan Data) (*ChannelLogger, chan Data) { 38 | if channel == nil { 39 | channel = make(chan Data) 40 | } 41 | return &ChannelLogger{channel}, channel 42 | } 43 | 44 | // Log writes the assembled log line. 45 | func (l *ChannelLogger) Log(data Data) error { 46 | l.channel <- data 47 | return nil 48 | } 49 | 50 | // Watch starts a for loop that sends any output from logch to logger.Log(). 51 | // This is intended to be used in a goroutine. 52 | func Watch(logger Logger, logch chan Data) { 53 | for { 54 | data := <-logch 55 | if data != nil { 56 | logger.Log(data) 57 | } else { 58 | return 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /timer.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // A Timer tracks the duration spent since its creation. 8 | type Timer struct { 9 | Started time.Time 10 | TimeUnit string 11 | context *Context 12 | *_statter 13 | } 14 | 15 | // Creates a Timer from the current Context, with the given key/value data. 16 | func (c *Context) Timer(data Data) *Timer { 17 | context := c.New(data) 18 | context.Log(Data{"at": "start"}) 19 | return &Timer{ 20 | Started: time.Now(), 21 | TimeUnit: context.TimeUnit, 22 | context: context, 23 | _statter: c._statter.dup(), 24 | } 25 | } 26 | 27 | // Finish writes a final log message with the elapsed time shown. 28 | func (t *Timer) Finish() { 29 | t.Log(Data{"at": "finish"}) 30 | } 31 | 32 | // Log writes a log message with extra data or the elapsed time shown. Pass nil 33 | // or use Finish() if there is no extra data. 34 | func (t *Timer) Log(data Data) error { 35 | if data == nil { 36 | data = make(Data) 37 | } 38 | 39 | dur := t.Elapsed() 40 | 41 | if _, ok := data["elapsed"]; !ok { 42 | data["elapsed"] = t.durationUnit(dur) 43 | } 44 | 45 | t._statter.Timing(dur) 46 | return t.context.Log(data) 47 | } 48 | 49 | // Add adds the key and value to the Timer's Context. 50 | func (t *Timer) Add(key string, value interface{}) { 51 | t.context.Add(key, value) 52 | } 53 | 54 | // Elapsed returns the duration since the Timer was created. 55 | func (t *Timer) Elapsed() time.Duration { 56 | return time.Since(t.Started) 57 | } 58 | 59 | func (t *Timer) durationUnit(dur time.Duration) float64 { 60 | sec := dur.Seconds() 61 | if t.TimeUnit == "ms" { 62 | return sec * 1000 63 | } 64 | return sec 65 | } 66 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | // A Context holds default key/value data that merges with the data every Log() 4 | // call receives. 5 | type Context struct { 6 | data Data 7 | Logger Logger 8 | TimeUnit string 9 | ErrorReporter ErrorReporter 10 | *_statter 11 | } 12 | 13 | // Log merges the given data with the Context's data, and passes it to the 14 | // Logger. 15 | func (c *Context) Log(data Data) error { 16 | return c.Logger.Log(c.Merge(data)) 17 | } 18 | 19 | func (c *Context) log(data Data) error { 20 | return c.Logger.Log(data) 21 | } 22 | 23 | // New creates a duplicate Context object, merging the given data with the 24 | // Context's data. 25 | func (c *Context) New(data Data) *Context { 26 | return newContext(c.Merge(data), c.Logger, c.TimeUnit, c.ErrorReporter, c._statter.dup()) 27 | } 28 | 29 | // Add adds the key and value to the Context's data. 30 | func (c *Context) Add(key string, value interface{}) { 31 | c.data[key] = value 32 | } 33 | 34 | // Merge combines the given key/value data with the Context's data. If no data 35 | // is given, a clean duplicate of the Context's data is returned. 36 | func (c *Context) Merge(data Data) Data { 37 | if data == nil { 38 | return dupeMaps(c.data) 39 | } else { 40 | return dupeMaps(c.data, data) 41 | } 42 | } 43 | 44 | // Data returns the Context's current Data. 45 | func (c *Context) Data() Data { 46 | return c.data 47 | } 48 | 49 | // Delete removes the key from the Context's data. 50 | func (c *Context) Delete(key string) { 51 | delete(c.data, key) 52 | } 53 | 54 | func dupeMaps(maps ...Data) Data { 55 | merged := make(Data) 56 | for _, orig := range maps { 57 | for key, value := range orig { 58 | merged[key] = value 59 | } 60 | } 61 | return merged 62 | } 63 | 64 | func newContext(data Data, logger Logger, timeunit string, reporter ErrorReporter, statter *_statter) *Context { 65 | return &Context{ 66 | data: data, 67 | Logger: logger, 68 | TimeUnit: timeunit, 69 | ErrorReporter: reporter, 70 | _statter: statter, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /loggers_test.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestIoLog(t *testing.T) { 9 | buf := bytes.NewBufferString("") 10 | logger := NewIoLogger(buf) 11 | logger.AddTime = false 12 | logger.Log(Data{"a": 1}) 13 | expected := "a=1\n" 14 | 15 | if actual := buf.String(); actual != expected { 16 | t.Errorf("e: %s\na: %s", expected, actual) 17 | } 18 | } 19 | 20 | func TestChannelLog(t *testing.T) { 21 | channel := make(chan Data, 1) 22 | logger, channel := NewChannelLogger(channel) 23 | data := Data{"a": 1} 24 | logger.Log(data) 25 | 26 | recv := <-channel 27 | 28 | if recvKeys := len(recv); recvKeys != len(data) { 29 | t.Errorf("Wrong number of keys: %d (%s)", recvKeys, recv) 30 | } 31 | 32 | if data["a"] != recv["a"] { 33 | t.Errorf("Received: %s", recv) 34 | } 35 | } 36 | 37 | type loggerBuffer struct { 38 | channel chan Data 39 | t *testing.T 40 | lines []builtLogLine 41 | index int 42 | } 43 | 44 | func (b *loggerBuffer) Lines() []builtLogLine { 45 | if b.lines == nil { 46 | close(b.channel) 47 | b.lines = make([]builtLogLine, len(b.channel)) 48 | i := 0 49 | 50 | for data := range b.channel { 51 | b.lines[i] = buildLogLine(data) 52 | i = i + 1 53 | } 54 | } 55 | 56 | return b.lines 57 | } 58 | 59 | func (b *loggerBuffer) AssertLine(parts ...string) { 60 | lines := b.Lines() 61 | if b.index < 0 || b.index >= len(lines) { 62 | b.t.Errorf("No line %d", b.index) 63 | return 64 | } 65 | 66 | AssertBuiltLine(b.t, lines[b.index], parts...) 67 | b.index += 1 68 | } 69 | 70 | func (b *loggerBuffer) AssertEOF() { 71 | lines := b.Lines() 72 | if b.index < 0 { 73 | b.t.Errorf("Invalid index %d", b.index) 74 | return 75 | } 76 | 77 | if b.index < len(lines) { 78 | b.t.Errorf("Not EOF, on line %d", b.index) 79 | return 80 | } 81 | } 82 | 83 | func setupLogger(t *testing.T) (*Context, *loggerBuffer) { 84 | ch := make(chan Data, 100) 85 | logger, _ := NewChannelLogger(ch) 86 | context := NewContext(nil) 87 | context.Logger = logger 88 | return context, &loggerBuffer{channel: ch, t: t} 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grohl 2 | 3 | Grohl is an opinionated library for gathering metrics and data about how your 4 | applications are running in production. It does this through writing logs 5 | in a key=value structure. It also provides interfaces for sending exceptions 6 | or metrics to external services. 7 | 8 | This is a Go version of [asenchi/scrolls](https://github.com/asenchi/scrolls). 9 | The name for this library came from mashing the words "go" and "scrolls" 10 | together. Also, Dave Grohl (lead singer of Foo Fighters) is passionate about 11 | event driven metrics. 12 | 13 | See this [blog post][blog] for the rationale behind this library. 14 | 15 | [blog]: http://techno-weenie.net/2013/11/2/key-value-logs-in-go/ 16 | 17 | ## Installation 18 | 19 | $ go get github.com/technoweenie/grohl 20 | 21 | Then import it: 22 | 23 | import "github.com/technoweenie/grohl" 24 | 25 | ## Usage 26 | 27 | Grohl takes almost no setup. Everything writes to STDOUT by default. Here's a 28 | quick http server example: 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "github.com/technoweenie/grohl" 35 | "log" 36 | "net/http" 37 | ) 38 | 39 | func main() { 40 | grohl.AddContext("app", "example") 41 | 42 | http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { 43 | grohl.Log(grohl.Data{"path": r.URL.Path}) 44 | fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) 45 | }) 46 | 47 | log.Fatal(http.ListenAndServe(":8080", nil)) 48 | } 49 | ``` 50 | 51 | This writes a log on every HTTP request like: 52 | 53 | now=2013-10-14T15:04:05-0700 app=example path=/foo 54 | 55 | See the [godocs](http://godoc.org/github.com/technoweenie/grohl) for details on 56 | metrics, statsd integration, and custom error reporters. 57 | 58 | ## Note on Patches/Pull Requests 59 | 60 | 1. Fork the project on GitHub. 61 | 2. Make your feature addition or bug fix. 62 | 3. Add tests for it. This is important so I don't break it in a future version 63 | unintentionally. 64 | 4. Commit, do not mess with rakefile, version, or history. (if you want to have 65 | your own version, that is fine but bump version in a commit by itself I can 66 | ignore when I pull) 67 | 5. Send me a pull request. Bonus points for topic branches. 68 | -------------------------------------------------------------------------------- /statter.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | // Statter describes the interface used by the g2s Statter object. 11 | // http://godoc.org/github.com/peterbourgon/g2s 12 | type Statter interface { 13 | Counter(sampleRate float32, bucket string, n ...int) 14 | Timing(sampleRate float32, bucket string, d ...time.Duration) 15 | Gauge(sampleRate float32, bucket string, value ...string) 16 | } 17 | 18 | // Counter writes a counter value to the Context. 19 | func (c *Context) Counter(sampleRate float32, bucket string, n ...int) { 20 | if rand.Float32() > sampleRate { 21 | return 22 | } 23 | 24 | for _, num := range n { 25 | c.Log(Data{"metric": bucket, "count": num}) 26 | } 27 | } 28 | 29 | // Timing writes a timer value to the Context. 30 | func (c *Context) Timing(sampleRate float32, bucket string, d ...time.Duration) { 31 | if rand.Float32() > sampleRate { 32 | return 33 | } 34 | 35 | for _, dur := range d { 36 | c.Log(Data{"metric": bucket, "timing": int64(dur / time.Millisecond)}) 37 | } 38 | } 39 | 40 | // Gauge writes a static value to the Context. 41 | func (c *Context) Gauge(sampleRate float32, bucket string, value ...string) { 42 | if rand.Float32() > sampleRate { 43 | return 44 | } 45 | 46 | for _, v := range value { 47 | c.Log(Data{"metric": bucket, "gauge": v}) 48 | } 49 | } 50 | 51 | // Embedded in Context and Timer. 52 | type _statter struct { 53 | statter Statter 54 | StatterSampleRate float32 55 | StatterBucket string 56 | } 57 | 58 | // SetStatter sets a Statter to be used in Timer Log() calls. 59 | func (s *_statter) SetStatter(statter Statter, sampleRate float32, bucket string) { 60 | s.statter = statter 61 | s.StatterSampleRate = sampleRate 62 | s.StatterBucket = bucket 63 | } 64 | 65 | // StatterBucketSuffix changes the suffix of the bucket. If SetStatter() is 66 | // called with bucket of "foo", then StatterBucketSuffix("bar") changes it to 67 | // "foo.bar". 68 | func (s *_statter) StatterBucketSuffix(suffix string) { 69 | if len(s.StatterBucket) == 0 { 70 | s.StatterBucket = suffix 71 | return 72 | } 73 | 74 | sep := "." 75 | if strings.HasSuffix(s.StatterBucket, ".") { 76 | sep = "" 77 | } 78 | s.StatterBucket = s.StatterBucket + fmt.Sprintf("%s%s", sep, suffix) 79 | } 80 | 81 | // Timing sends the timing to the configured Statter. 82 | func (s *_statter) Timing(dur time.Duration) { 83 | if s.statter == nil { 84 | s.statter = CurrentStatter 85 | } 86 | 87 | s.statter.Timing(s.StatterSampleRate, s.StatterBucket, dur) 88 | } 89 | 90 | func (s *_statter) dup() *_statter { 91 | return &_statter{s.statter, s.StatterSampleRate, s.StatterBucket} 92 | } 93 | -------------------------------------------------------------------------------- /timer_test.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTimerLog(t *testing.T) { 8 | context, buf := setupLogger(t) 9 | context.Add("a", "1") 10 | timer := context.Timer(Data{"b": "2"}) 11 | timer.Add("c", "3") 12 | timer.Log(Data{"d": "4"}) 13 | 14 | buf.AssertLine("a=1", "b=2", "at=start") 15 | buf.AssertLine("a=1", "b=2", "c=3", "d=4", "elapsed=0.000") 16 | buf.AssertEOF() 17 | } 18 | 19 | func TestTimerLogInMS(t *testing.T) { 20 | context, buf := setupLogger(t) 21 | context.Add("a", "1") 22 | timer := context.Timer(Data{"b": "2"}) 23 | timer.TimeUnit = "ms" 24 | timer.Log(Data{"c": "3"}) 25 | 26 | buf.AssertLine("a=1", "b=2", "at=start") 27 | buf.AssertLine("a=1", "b=2", "c=3", "~elapsed=0.00") 28 | buf.AssertEOF() 29 | } 30 | 31 | func TestTimerFinish(t *testing.T) { 32 | context, buf := setupLogger(t) 33 | context.Add("a", "1") 34 | timer := context.Timer(Data{"b": "2"}) 35 | timer.Add("c", "3") 36 | timer.Finish() 37 | 38 | buf.AssertLine("a=1", "b=2", "at=start") 39 | buf.AssertLine("a=1", "b=2", "c=3", "at=finish", "elapsed=0.000") 40 | buf.AssertEOF() 41 | } 42 | 43 | func TestTimerWithCurrentStatter(t *testing.T) { 44 | context, buf := setupLogger(t) 45 | context.Add("a", "1") 46 | timer := context.Timer(Data{"b": "2"}) 47 | timer.StatterBucketSuffix("bucket") 48 | 49 | oldStatter := CurrentStatter 50 | CurrentStatter = context 51 | timer.Finish() 52 | CurrentStatter = oldStatter 53 | 54 | buf.AssertLine("a=1", "b=2", "at=start") 55 | buf.AssertLine("a=1", "metric=bucket", "timing=0") 56 | buf.AssertLine("a=1", "b=2", "at=finish", "elapsed=0.000") 57 | } 58 | 59 | func TestTimerWithStatter(t *testing.T) { 60 | context, buf := setupLogger(t) 61 | context.Add("a", "1") 62 | timer := context.Timer(Data{"b": "2"}) 63 | statter := NewContext(nil) 64 | statter.Logger = context.Logger 65 | timer.SetStatter(statter, 1.0, "bucket") 66 | timer.Finish() 67 | 68 | buf.AssertLine("a=1", "b=2", "at=start") 69 | buf.AssertLine("metric=bucket", "timing=0") 70 | buf.AssertLine("a=1", "b=2", "at=finish", "elapsed=0.000") 71 | buf.AssertEOF() 72 | } 73 | 74 | func TestTimerWithContextStatter(t *testing.T) { 75 | context, buf := setupLogger(t) 76 | context.Add("a", "1") 77 | context.SetStatter(context, 1.0, "bucket") 78 | timer := context.Timer(Data{"b": "2"}) 79 | timer.StatterBucket = "bucket2" 80 | timer.Finish() 81 | 82 | buf.AssertLine("a=1", "b=2", "at=start") 83 | buf.AssertLine("a=1", "metric=bucket2", "timing=0") 84 | buf.AssertLine("a=1", "b=2", "at=finish", "elapsed=0.000") 85 | buf.AssertEOF() 86 | 87 | if context.StatterBucket == "bucket2" { 88 | t.Errorf("Context's stat bucket was changed") 89 | } 90 | } 91 | 92 | func TestTimerWithNilStatter(t *testing.T) { 93 | oldlogger := CurrentContext.Logger 94 | 95 | context, buf := setupLogger(t) 96 | context.Add("a", "1") 97 | CurrentContext.Logger = context.Logger 98 | timer := context.Timer(Data{"b": "2"}) 99 | timer.SetStatter(nil, 1.0, "bucket") 100 | timer.Finish() 101 | 102 | CurrentContext.Logger = oldlogger 103 | buf.AssertLine("a=1", "b=2", "at=start") 104 | buf.AssertLine("metric=bucket", "timing=0") 105 | buf.AssertLine("a=1", "b=2", "at=finish", "elapsed=0.000") 106 | buf.AssertEOF() 107 | } 108 | -------------------------------------------------------------------------------- /grohl.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import "time" 4 | 5 | // Data is the map used to specify the key/value pairs for a logged message. 6 | type Data map[string]interface{} 7 | 8 | // The Logger interface represents the ability to log key/value data. 9 | type Logger interface { 10 | Log(Data) error 11 | } 12 | 13 | // CurrentLogger is the default Logger used by Log, Report. 14 | var CurrentLogger Logger = NewIoLogger(nil) 15 | 16 | // CurrentContext is the default Context used by Log, Report, AddContext, 17 | // DeleteContext, NewTimer. 18 | var CurrentContext = newContext(make(Data), CurrentLogger, "s", nil, &_statter{StatterSampleRate: 1}) 19 | 20 | // The CurrentStatter is the default Statter used in Counter, Timing, Gauge. 21 | var CurrentStatter Statter = CurrentContext 22 | 23 | // Log writes the key/value data to the CurrentLogger. 24 | func Log(data Data) { 25 | CurrentContext.Log(data) 26 | } 27 | 28 | // Report sends the error and key/value data to the CurrentContext's 29 | // ErrorReporter. If no reporter is set, the CurrentContext simply logs the 30 | // error and stacktrace. 31 | func Report(err error, data Data) { 32 | CurrentContext.Report(err, data) 33 | } 34 | 35 | // Counter writes a counter value to the CurrentStatter. By default, values are 36 | // simply logged. 37 | func Counter(sampleRate float32, bucket string, n ...int) { 38 | CurrentStatter.Counter(sampleRate, bucket, n...) 39 | } 40 | 41 | // Timing writes a timer value to the CurrentStatter. By default, values are 42 | // simply logged. 43 | func Timing(sampleRate float32, bucket string, d ...time.Duration) { 44 | CurrentStatter.Timing(sampleRate, bucket, d...) 45 | } 46 | 47 | // Gauge writes a static value to the CurrentStatter. By default, values are 48 | // simply logged. 49 | func Gauge(sampleRate float32, bucket string, value ...string) { 50 | CurrentStatter.Gauge(sampleRate, bucket, value...) 51 | } 52 | 53 | // SetLogger updates the Logger object used by CurrentLogger and CurrentContext. 54 | func SetLogger(logger Logger) Logger { 55 | if logger == nil { 56 | logger = NewIoLogger(nil) 57 | } 58 | 59 | CurrentLogger = logger 60 | CurrentContext.Logger = logger 61 | 62 | return logger 63 | } 64 | 65 | // NewContext returns a new Context object with the given key/value data. 66 | func NewContext(data Data) *Context { 67 | return CurrentContext.New(data) 68 | } 69 | 70 | // AddContext adds the key and value to the CurrentContext's data. 71 | func AddContext(key string, value interface{}) { 72 | CurrentContext.Add(key, value) 73 | } 74 | 75 | // DeleteContext removes the key from the CurrentContext's data. 76 | func DeleteContext(key string) { 77 | CurrentContext.Delete(key) 78 | } 79 | 80 | // SetStatter sets up a basic Statter in the CurrentContext. This Statter will 81 | // be used by any Timer created from this Context. 82 | func SetStatter(statter Statter, sampleRate float32, bucket string) { 83 | CurrentContext.SetStatter(statter, sampleRate, bucket) 84 | } 85 | 86 | // NewTimer creates a new Timer with the given key/value data. 87 | func NewTimer(data Data) *Timer { 88 | return CurrentContext.Timer(data) 89 | } 90 | 91 | // SetTimeUnit sets the default time unit for the CurrentContext. This gets 92 | // passed down to Timer objects created from this Context. 93 | func SetTimeUnit(unit string) { 94 | CurrentContext.TimeUnit = unit 95 | } 96 | 97 | // TimeUnit returns the default time unit for the CurrentContext. 98 | func TimeUnit() string { 99 | return CurrentContext.TimeUnit 100 | } 101 | 102 | // SetErrorReporter sets the ErrorReporter used by the CurrentContext. This 103 | // will skip the default logging of the reported errors. 104 | func SetErrorReporter(reporter ErrorReporter) { 105 | CurrentContext.ErrorReporter = reporter 106 | } 107 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // BuildLog assembles a log message from the key/value data. If addTime is true, 12 | // the current timestamp is logged with the "now" key. 13 | func BuildLog(data Data, addTime bool) string { 14 | return strings.Join(BuildLogParts(data, addTime), space) 15 | } 16 | 17 | func BuildLogParts(data Data, addTime bool) []string { 18 | index := 0 19 | extraRows := 0 20 | if addTime { 21 | extraRows = extraRows + 1 22 | delete(data, "now") 23 | } 24 | 25 | pieces := make([]string, len(data)+extraRows) 26 | for key, value := range data { 27 | pieces[index+extraRows] = fmt.Sprintf("%s=%s", key, Format(value)) 28 | index = index + 1 29 | } 30 | 31 | if addTime { 32 | pieces[0] = fmt.Sprintf("now=%s", time.Now().UTC().Format(timeLayout)) 33 | } 34 | 35 | return pieces 36 | } 37 | 38 | // Format converts the value into a string for the Logger output. 39 | func Format(value interface{}) string { 40 | if value == nil { 41 | return "nil" 42 | } 43 | 44 | t := reflect.TypeOf(value) 45 | formatter := formatters[t.Kind().String()] 46 | if formatter == nil { 47 | formatter = formatters[t.String()] 48 | } 49 | 50 | if formatter == nil { 51 | if _, ok := t.MethodByName("Error"); ok == true { 52 | return formatString(value.(error).Error()) 53 | } else { 54 | return formatString(fmt.Sprintf("%+v", value)) 55 | } 56 | } 57 | 58 | return formatter(value) 59 | } 60 | 61 | func formatString(value interface{}) string { 62 | str := value.(string) 63 | 64 | if len(str) == 0 { 65 | return "nil" 66 | } 67 | 68 | if idx := strings.Index(str, " "); idx != -1 { 69 | hasSingle := strings.Index(str, sQuote) != -1 70 | hasDouble := strings.Index(str, dQuote) != -1 71 | str = strings.Replace(str, back, backReplace, -1) 72 | 73 | if hasSingle && hasDouble { 74 | str = dQuote + strings.Replace(str, dQuote, dReplace, -1) + dQuote 75 | } else if hasDouble { 76 | str = sQuote + str + sQuote 77 | } else { 78 | str = dQuote + str + dQuote 79 | } 80 | } else { 81 | if idx := strings.Index(str, "="); idx != -1 { 82 | str = dQuote + str + dQuote 83 | } 84 | } 85 | 86 | return str 87 | } 88 | 89 | const ( 90 | space = " " 91 | equals = "=" 92 | sQuote = "'" 93 | dQuote = `"` 94 | dReplace = `\"` 95 | back = `\` 96 | backReplace = `\\` 97 | timeLayout = "2006-01-02T15:04:05-0700" 98 | ) 99 | 100 | var durationFormat = []byte("f")[0] 101 | 102 | var formatters = map[string]func(value interface{}) string{ 103 | "string": formatString, 104 | 105 | "bool": func(value interface{}) string { 106 | return strconv.FormatBool(value.(bool)) 107 | }, 108 | 109 | "int": func(value interface{}) string { 110 | return strconv.FormatInt(int64(value.(int)), 10) 111 | }, 112 | 113 | "int8": func(value interface{}) string { 114 | return strconv.FormatInt(int64(value.(int8)), 10) 115 | }, 116 | 117 | "int16": func(value interface{}) string { 118 | return strconv.FormatInt(int64(value.(int16)), 10) 119 | }, 120 | 121 | "int32": func(value interface{}) string { 122 | return strconv.FormatInt(int64(value.(int32)), 10) 123 | }, 124 | 125 | "int64": func(value interface{}) string { 126 | return strconv.FormatInt(value.(int64), 10) 127 | }, 128 | 129 | "float32": func(value interface{}) string { 130 | return strconv.FormatFloat(float64(value.(float32)), durationFormat, 3, 32) 131 | }, 132 | 133 | "float64": func(value interface{}) string { 134 | return strconv.FormatFloat(value.(float64), durationFormat, 3, 64) 135 | }, 136 | 137 | "uint": func(value interface{}) string { 138 | return strconv.FormatUint(uint64(value.(uint)), 10) 139 | }, 140 | 141 | "uint8": func(value interface{}) string { 142 | return strconv.FormatUint(uint64(value.(uint8)), 10) 143 | }, 144 | 145 | "uint16": func(value interface{}) string { 146 | return strconv.FormatUint(uint64(value.(uint16)), 10) 147 | }, 148 | 149 | "uint32": func(value interface{}) string { 150 | return strconv.FormatUint(uint64(value.(uint32)), 10) 151 | }, 152 | 153 | "uint64": func(value interface{}) string { 154 | return strconv.FormatUint(value.(uint64), 10) 155 | }, 156 | 157 | "time.Time": func(value interface{}) string { 158 | return value.(time.Time).Format(timeLayout) 159 | }, 160 | } 161 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestWrapHttpError(t *testing.T) { 10 | err := errors.New("sup") 11 | e := NewHttpError(err, 0) 12 | if e.Message != "sup" { 13 | t.Errorf("Unexpected error message: %s", e.Message) 14 | } 15 | 16 | if e.StatusCode != 500 { 17 | t.Errorf("Unexpected status code: %d", e.StatusCode) 18 | } 19 | 20 | if e.InnerError != err { 21 | t.Errorf("Unexpected inner error: %v", e.InnerError) 22 | } 23 | } 24 | 25 | func TestWrapHttpErrorWithStatus(t *testing.T) { 26 | err := errors.New("sup") 27 | e := NewHttpError(err, 409) 28 | if e.Message != "sup" { 29 | t.Errorf("Unexpected error message: %s", e.Message) 30 | } 31 | 32 | if e.StatusCode != 409 { 33 | t.Errorf("Unexpected status code: %d", e.StatusCode) 34 | } 35 | 36 | if e.InnerError != err { 37 | t.Errorf("Unexpected inner error: %v", e.InnerError) 38 | } 39 | } 40 | 41 | func TestWrapError(t *testing.T) { 42 | err := errors.New("sup") 43 | e := NewError(err) 44 | if e.Message != "sup" { 45 | t.Errorf("Unexpected error message: %s", e.Message) 46 | } 47 | 48 | if e.InnerError != err { 49 | t.Errorf("Unexpected inner error: %v", e.InnerError) 50 | } 51 | } 52 | 53 | func TestWrapErrorWithMessage(t *testing.T) { 54 | err := errors.New("sup") 55 | e := NewErrorf(err, "nuff said, %s", "bub") 56 | if e.Message != "nuff said, bub" { 57 | t.Errorf("Unexpected error message: %s", e.Message) 58 | } 59 | 60 | if e.InnerError != err { 61 | t.Errorf("Unexpected inner error: %v", e.InnerError) 62 | } 63 | } 64 | 65 | func TestWrapNilError(t *testing.T) { 66 | e := NewError(nil) 67 | if e.Message != "" { 68 | t.Errorf("Expected empty error message: %s", e.Message) 69 | } 70 | 71 | if e.InnerError != nil { 72 | t.Errorf("Expected nil inner error: %v", e.InnerError) 73 | } 74 | } 75 | 76 | func TestWrapNilErrorWithMessage(t *testing.T) { 77 | e := NewErrorf(nil, "nuff said, %s", "bub") 78 | if e.Message != "nuff said, bub" { 79 | t.Errorf("Unexpected error message: %s", e.Message) 80 | } 81 | 82 | if e.InnerError != nil { 83 | t.Errorf("Expected nil inner error: %v", e.InnerError) 84 | } 85 | } 86 | 87 | func TestLogsWrappedError(t *testing.T) { 88 | err := errors.New("sup") 89 | e := NewErrorf(err, "wat") 90 | e.Add("b", 2) 91 | e.Add("c", 2) 92 | 93 | reporter, buf := setupLogger(t) 94 | reporter.Add("a", 1) 95 | reporter.Add("b", 1) 96 | 97 | reporter.Report(e, Data{"c": 3, "d": 4, "at": "overwrite"}) 98 | firstRow := []string{ 99 | "a=1", 100 | "b=2", 101 | "c=3", 102 | "d=4", 103 | "at=exception", 104 | "class=*grohl.Err", 105 | "message=sup", 106 | } 107 | 108 | otherRows := append(firstRow, "~site=") 109 | 110 | for i, line := range buf.Lines() { 111 | if i == 0 { 112 | AssertBuiltLine(t, line, firstRow...) 113 | } else { 114 | AssertBuiltLine(t, line, otherRows...) 115 | } 116 | } 117 | } 118 | 119 | func TestLogsError(t *testing.T) { 120 | reporter, buf := setupLogger(t) 121 | reporter.Add("a", 1) 122 | reporter.Add("b", 1) 123 | 124 | err := fmt.Errorf("Test") 125 | 126 | reporter.Report(err, Data{"b": 2, "c": 3, "at": "overwrite me"}) 127 | firstRow := []string{ 128 | "a=1", 129 | "b=2", 130 | "c=3", 131 | "at=exception", 132 | "class=*errors.errorString", 133 | "message=Test", 134 | } 135 | 136 | otherRows := append(firstRow, "~site=") 137 | 138 | for i, line := range buf.Lines() { 139 | if i == 0 { 140 | AssertBuiltLine(t, line, firstRow...) 141 | } else { 142 | AssertBuiltLine(t, line, otherRows...) 143 | } 144 | } 145 | } 146 | 147 | func TestCustomReporterMergesDataWithContext(t *testing.T) { 148 | context := NewContext(nil) 149 | 150 | errors := make(chan *reportedError, 1) 151 | context.ErrorReporter = &channelErrorReporter{errors} 152 | 153 | context.Add("a", 1) 154 | context.Add("b", 1) 155 | 156 | err := fmt.Errorf("Test") 157 | context.Report(err, Data{"b": 2}) 158 | reportedErr := <-errors 159 | 160 | expectedData := Data{"a": 1, "b": 2} 161 | if reportedErr.Data["a"] != expectedData["a"] || reportedErr.Data["b"] != expectedData["b"] { 162 | t.Errorf("Expected error data to be %v but was %v", expectedData, reportedErr.Data) 163 | } 164 | } 165 | 166 | type reportedError struct { 167 | Error error 168 | Data Data 169 | } 170 | 171 | type channelErrorReporter struct { 172 | Channel chan *reportedError 173 | } 174 | 175 | func (c *channelErrorReporter) Report(err error, data Data) error { 176 | c.Channel <- &reportedError{err, data} 177 | return nil 178 | } 179 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var exampleTime = time.Date(2000, 1, 2, 3, 4, 5, 6, time.UTC) 11 | var exampleError = fmt.Errorf("error message") 12 | 13 | type ExampleStruct struct { 14 | Value interface{} 15 | } 16 | 17 | var actuals = []Data{ 18 | Data{"fn": "string", "test": "hi"}, 19 | Data{"fn": "stringspace", "test": "a b"}, 20 | Data{"fn": "stringslasher", "test": `slasher \\`}, 21 | Data{"fn": "stringeqspace", "test": "x=4, y=10"}, 22 | Data{"fn": "stringeq", "test": "x=4,y=10"}, 23 | Data{"fn": "stringspace", "test": "hello world"}, 24 | Data{"fn": "stringbothquotes", "test": `echo 'hello' "world"`}, 25 | Data{"fn": "stringsinglequotes", "test": `a 'a'`}, 26 | Data{"fn": "stringdoublequotes", "test": `echo "hello"`}, 27 | Data{"fn": "stringbothquotesnospace", "test": `'a"`}, 28 | Data{"fn": "emptystring", "test": ""}, 29 | Data{"fn": "int", "test": int(1)}, 30 | Data{"fn": "int8", "test": int8(1)}, 31 | Data{"fn": "int16", "test": int16(1)}, 32 | Data{"fn": "int32", "test": int32(1)}, 33 | Data{"fn": "int64", "test": int64(1)}, 34 | Data{"fn": "uint", "test": uint(1)}, 35 | Data{"fn": "uint8", "test": uint8(1)}, 36 | Data{"fn": "uint16", "test": uint16(1)}, 37 | Data{"fn": "uint32", "test": uint32(1)}, 38 | Data{"fn": "uint64", "test": uint64(1)}, 39 | Data{"fn": "float", "test": float32(1.0)}, 40 | Data{"fn": "bool", "test": true}, 41 | Data{"fn": "nil", "test": nil}, 42 | Data{"fn": "time", "test": exampleTime}, 43 | Data{"fn": "error", "test": exampleError}, 44 | Data{"fn": "slice", "test": []byte{86, 87, 88}}, 45 | Data{"fn": "struct", "test": ExampleStruct{Value: "testing123"}}, 46 | } 47 | 48 | var expectations = [][]string{ 49 | []string{"fn=string", "test=hi"}, 50 | []string{"fn=stringspace", `test="a b"`}, 51 | []string{`fn=stringslasher`, `test="slasher \\\\"`}, 52 | []string{`fn=stringeqspace`, `test="x=4, y=10"`}, 53 | []string{`fn=stringeq`, `test="x=4,y=10"`}, 54 | []string{`fn=stringspace`, `test="hello world"`}, 55 | []string{`fn=stringbothquotes`, `test="echo 'hello' \"world\""`}, 56 | []string{`fn=stringsinglequotes`, `test="a 'a'"`}, 57 | []string{`fn=stringdoublequotes`, `test='echo "hello"'`}, 58 | []string{`fn=stringbothquotesnospace`, `test='a"`}, 59 | []string{"fn=emptystring", "test=nil"}, 60 | []string{"fn=int", "test=1"}, 61 | []string{"fn=int8", "test=1"}, 62 | []string{"fn=int16", "test=1"}, 63 | []string{"fn=int32", "test=1"}, 64 | []string{"fn=int64", "test=1"}, 65 | []string{"fn=uint", "test=1"}, 66 | []string{"fn=uint8", "test=1"}, 67 | []string{"fn=uint16", "test=1"}, 68 | []string{"fn=uint32", "test=1"}, 69 | []string{"fn=uint64", "test=1"}, 70 | []string{"fn=float", "test=1.000"}, 71 | []string{"fn=bool", "test=true"}, 72 | []string{"fn=nil", "test=nil"}, 73 | []string{"fn=time", "test=2000-01-02T03:04:05+0000"}, 74 | []string{`fn=error`, `test="error message"`}, 75 | []string{`fn=slice`, `test="[86 87 88]"`}, 76 | []string{`fn=struct`, `test={Value:testing123}`}, 77 | } 78 | 79 | func TestFormat(t *testing.T) { 80 | for i, actual := range actuals { 81 | AssertData(t, actual, expectations[i]...) 82 | } 83 | } 84 | 85 | func TestFormatWithTime(t *testing.T) { 86 | data := Data{"fn": "time", "test": 1} 87 | m := make(map[string]bool) 88 | parts := BuildLogParts(data, true) 89 | for _, pair := range parts { 90 | m[pair] = true 91 | } 92 | line := builtLogLine{m, strings.Join(parts, space)} 93 | 94 | if !strings.HasPrefix(line.full, "now=") { 95 | t.Errorf("Invalid prefix: %s", line.full) 96 | } 97 | 98 | AssertBuiltLine(t, line, "fn=time", "test=1", "~now=") 99 | } 100 | 101 | func AssertLog(t *testing.T, ctx *Context, expected ...string) { 102 | AssertData(t, ctx.Merge(nil), expected...) 103 | } 104 | 105 | func AssertData(t *testing.T, data Data, expected ...string) { 106 | AssertBuiltLine(t, buildLogLine(data), expected...) 107 | } 108 | 109 | func AssertBuiltLine(t *testing.T, line builtLogLine, expected ...string) { 110 | for _, pair := range expected { 111 | if strings.HasPrefix(pair, "~") { 112 | pair = pair[1:len(pair)] 113 | found := false 114 | for actual, _ := range line.pairs { 115 | if !found { 116 | found = strings.HasPrefix(actual, pair) 117 | } 118 | } 119 | 120 | if !found { 121 | t.Errorf("Expected partial pair ~ '%s' in %s", pair, line.full) 122 | } 123 | } else { 124 | if _, ok := line.pairs[pair]; !ok { 125 | t.Errorf("Expected pair '%s' in %s", pair, line.full) 126 | } 127 | } 128 | } 129 | 130 | if expectedLen := len(expected); expectedLen != len(line.pairs) { 131 | t.Errorf("Expected %d pairs in %s", expectedLen, line.full) 132 | } 133 | } 134 | 135 | func AssertString(t *testing.T, expected, actual string) { 136 | if expected != actual { 137 | t.Errorf("Expected %s\nGot: %s", expected, actual) 138 | } 139 | } 140 | 141 | type builtLogLine struct { 142 | pairs map[string]bool 143 | full string 144 | } 145 | 146 | func buildLogLine(d Data) builtLogLine { 147 | m := make(map[string]bool) 148 | parts := BuildLogParts(d, false) 149 | for _, pair := range parts { 150 | m[pair] = true 151 | } 152 | return builtLogLine{m, strings.Join(parts, space)} 153 | } 154 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package grohl 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "runtime" 8 | "sync" 9 | ) 10 | 11 | type Err struct { 12 | Message string 13 | reportable bool 14 | InnerError error 15 | data Data 16 | stack []byte 17 | } 18 | 19 | type HttpError struct { 20 | StatusCode int 21 | *Err 22 | } 23 | 24 | // NewError wraps an error with the error's message. 25 | func NewError(err error) *Err { 26 | return NewErrorf(err, "") 27 | } 28 | 29 | // NewErrorf wraps an error with a formatted message. 30 | func NewErrorf(err error, format string, a ...interface{}) *Err { 31 | var msg string 32 | if len(format) > 0 { 33 | msg = fmt.Sprintf(format, a...) 34 | } else if err != nil { 35 | msg = err.Error() 36 | } 37 | 38 | return &Err{ 39 | Message: msg, 40 | reportable: true, 41 | InnerError: err, 42 | data: nil, 43 | stack: Stack(), 44 | } 45 | } 46 | 47 | // NewHttpError wraps an error with an HTTP status code and the given error's 48 | // message. 49 | func NewHttpError(err error, status int) *HttpError { 50 | return NewHttpErrorf(err, status, "") 51 | } 52 | 53 | // NewHttpErrorf wraps an error with an HTTP status code and a formatted message. 54 | func NewHttpErrorf(err error, status int, format string, a ...interface{}) *HttpError { 55 | if status < 1 { 56 | status = 500 57 | } 58 | return &HttpError{status, NewErrorf(err, format, a...)} 59 | } 60 | 61 | // Error returns the error message. This will be the inner error's message, 62 | // unless a formatted message is provided from Errorf(). 63 | func (e *Err) Error() string { 64 | if e.InnerError != nil { 65 | return e.InnerError.Error() 66 | } 67 | return e.Message 68 | } 69 | 70 | // Stack returns the runtime stack stored with this Error. 71 | func (e *Err) Stack() []byte { 72 | return e.stack 73 | } 74 | 75 | // Data returns the error's current grohl.Data context. 76 | func (e *Err) Data() Data { 77 | return e.data 78 | } 79 | 80 | // Reportable returns whether this error should be sent to the grohl 81 | // ErrorReporter. 82 | func (e *Err) Reportable() bool { 83 | return e.reportable 84 | } 85 | 86 | // ErrorMessage returns a user-visible error message. 87 | func (e *Err) ErrorMessage() string { 88 | return e.Message 89 | } 90 | 91 | // Add adds the key and value to this error's context. 92 | func (e *Err) Add(key string, value interface{}) { 93 | if e.data == nil { 94 | e.data = Data{} 95 | } 96 | e.data[key] = value 97 | } 98 | 99 | // Delete removes the key from this error's context. 100 | func (e *Err) Delete(key string) { 101 | if e.data != nil { 102 | delete(e.data, key) 103 | } 104 | } 105 | 106 | // SetReportable sets whether the ErrorReporter should ignore this error. 107 | func (e *Err) SetReportable(v bool) { 108 | e.reportable = v 109 | } 110 | 111 | var stackPool = sync.Pool{ 112 | New: func() interface{} { 113 | return make([]byte, 1024*1024) 114 | }, 115 | } 116 | 117 | // Stack returns the current runtime stack (up to 1MB). 118 | func Stack() []byte { 119 | stackBuf := stackPool.Get().([]byte) 120 | written := runtime.Stack(stackBuf, false) 121 | stackPool.Put(stackBuf) 122 | return stackBuf[:written] 123 | } 124 | 125 | type ErrorReporter interface { 126 | Report(err error, data Data) error 127 | } 128 | 129 | // Report writes the error to the ErrorReporter, or logs it if there is none. 130 | func (c *Context) Report(err error, data Data) error { 131 | if rErr, ok := err.(reportableError); ok { 132 | if rErr.Reportable() == false { 133 | return nil 134 | } 135 | } 136 | 137 | dataMaps := make([]Data, 1, 3) 138 | dataMaps[0] = c.Data() 139 | if gErr, ok := err.(grohlError); ok { 140 | if errData := gErr.Data(); errData != nil { 141 | dataMaps = append(dataMaps, errData) 142 | } 143 | } 144 | 145 | if data != nil { 146 | dataMaps = append(dataMaps, data) 147 | } 148 | 149 | merged := dupeMaps(dataMaps...) 150 | errorToMap(err, merged) 151 | 152 | if c.ErrorReporter != nil { 153 | return c.ErrorReporter.Report(err, merged) 154 | } else { 155 | var logErr error 156 | logErr = c.log(merged) 157 | if logErr != nil { 158 | return logErr 159 | } 160 | 161 | for _, line := range ErrorBacktraceLines(err) { 162 | lineData := dupeMaps(merged) 163 | lineData["site"] = line 164 | logErr = c.log(lineData) 165 | if logErr != nil { 166 | return logErr 167 | } 168 | } 169 | return nil 170 | } 171 | } 172 | 173 | // ErrorBacktrace creates a backtrace of the call stack. 174 | func ErrorBacktrace(err error) string { 175 | return string(errorStack(err)) 176 | } 177 | 178 | // ErrorBacktraceLines creates a backtrace of the call stack, split into lines. 179 | func ErrorBacktraceLines(err error) []string { 180 | byteLines := bytes.Split(errorStack(err), byteLineBreak) 181 | lines := make([]string, 0, len(byteLines)) 182 | 183 | // skip top two frames which are this method and `errorBacktraceBytes` 184 | for i := 2; i < len(byteLines); i++ { 185 | lines = append(lines, string(byteLines[i])) 186 | } 187 | return lines 188 | } 189 | 190 | type stackedError interface { 191 | Stack() []byte 192 | } 193 | 194 | type reportableError interface { 195 | Reportable() bool 196 | } 197 | 198 | type grohlError interface { 199 | Data() Data 200 | } 201 | 202 | func errorStack(err error) []byte { 203 | if sErr, ok := err.(stackedError); ok { 204 | return sErr.Stack() 205 | } 206 | return Stack() 207 | } 208 | 209 | func errorToMap(err error, data Data) { 210 | data["at"] = "exception" 211 | data["class"] = reflect.TypeOf(err).String() 212 | data["message"] = err.Error() 213 | } 214 | 215 | var byteLineBreak = []byte{'\n'} 216 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Grohl is an opinionated library for gathering metrics and data about how your 3 | applications are running in production. It does this through writing logs 4 | in a key=value structure. It also provides interfaces for sending stacktraces 5 | or metrics to external services. 6 | 7 | This is a Go version of https://github.com/asenchi/scrolls. The name for this 8 | library came from mashing the words "go" and "scrolls" together. Also, Dave 9 | Grohl (lead singer of Foo Fighters) is passionate about event driven metrics. 10 | 11 | Grohl treats logs as the central authority for how an application is behaving. 12 | Logs are written in a key=value structure so that they are easily parsed. If 13 | you use a set of common log keys, you can relate events from various services 14 | together. 15 | 16 | Here's an example log that you might write: 17 | 18 | grohl.Log(grohl.Data{"fn": "trap", "signal": "TERM", "at": "exit", "status": 0}) 19 | 20 | The output would look something like: 21 | 22 | now=2013-10-14T15:04:05-0700 fn=trap signal=TERM at=exit status=0 23 | 24 | Note: Other examples leave out the "now" keyword for clarity. 25 | 26 | A *grohl.Context stores a map of keys and values that are written with every 27 | log message. You can set common keys for every request, or create a new context 28 | per new request or connection. 29 | 30 | You can add more context to the example above by setting up the app name and 31 | deployed environment. 32 | 33 | grohl.AddContext("app", "myapp") 34 | grohl.AddContext("deploy", os.Getenv("DEPLOY")) 35 | 36 | This changes the output from above to: 37 | 38 | app=myapp deploy=production fn=trap signal=TERM at=exit status=0 39 | 40 | You can also create scoped Context objects. For instance, a network server may 41 | want a scoped Context for each request or connection. 42 | 43 | context := grohl.NewContext(grohl.Data{"ns": "server"}) 44 | context.Log(grohl.Data{"fn": "trap", "signal": "TERM", "at": "exit", "status": 0}) 45 | 46 | This is the output (taking the global context above into consideration): 47 | 48 | app=myapp deploy=production ns=server fn=trap signal=TERM at=exit status=0 49 | 50 | As you can see we have some standard nomenclature around logging. Here's a cheat sheet for some of the methods we use: 51 | 52 | * now: The current timestamp, automatically set by an IoLogger. Can be disabled 53 | if IoLogger.AddTime is disabled. 54 | * app: Application 55 | * lib: Library 56 | * ns: Namespace (Class, Module or files) 57 | * fn: Function 58 | * at: Execution point 59 | * deploy: Our deployment (typically an environment variable i.e. DEPLOY=staging) 60 | * elapsed: Measurements (Time from a Timer) 61 | * metric: The name of a Statter measurement 62 | * count: Measurements (Counters through a Statter) 63 | * gauge: Measurements (Gauges through a Statter) 64 | * timing: Measurements (Timing through a Statter) 65 | 66 | By default, all *grohl.Context objects write to STDOUT. Grohl includes support 67 | for both io and channel loggers. 68 | 69 | writer, _ := syslog.Dial(network, raddr, syslog.LOG_INFO, tag) 70 | grohl.SetLogger(grohl.NewIoLogger(writer)) 71 | 72 | If you are writing to *grohl.Context objects in separate go routines, a 73 | channel logger can be used for concurrency. 74 | 75 | // you can pass in your own "chan grohl.data" too. 76 | chlogger, ch := grohl.NewChannelLogger(nil) 77 | grohl.SetLogger(chlogger) 78 | 79 | // pipe messages from the channel to a single io.writer: 80 | writer, _ := syslog.Dial(network, raddr, syslog.LOG_INFO, tag) 81 | logger := grohl.NewIoLogger(writer) 82 | 83 | // reads from the channel until the program dies 84 | go grohl.Watch(logger, ch) 85 | 86 | Grohl provides a grohl.Statter interface based on https://github.com/peterbourgon/g2s: 87 | 88 | // these functions are available on a *grohl.Context too 89 | grohl.Counter(1.0, "my.silly.counter", 1) 90 | grohl.Timing(1.0, "my.silly.slow-process", time.Since(somethingBegan)) 91 | grohl.Gauge(1.0, "my.silly.status", "green") 92 | 93 | Without any setup, this outputs: 94 | 95 | metric=my.silly.counter count=1 96 | metric=my.silly.slow-process timing=12345 97 | metric=my.silly.status gauge=green 98 | 99 | If you import "github.com/peterbourgon/g2s", you can dial into a statsd server 100 | over a udp socket: 101 | 102 | statter, err := g2s.Dial("udp", "statsd.server:1234") 103 | if err != nil { 104 | panic(err) 105 | } 106 | grohl.CurrentStatter = statter 107 | 108 | Once being set up, the statter functions above will not output any logs. 109 | 110 | Grohl makes it easy to measure the run time of a portion of code. 111 | 112 | // you can also create a timer from a *grohl.Context 113 | // timer := context.Timer(grohl.Data{"fn": "test"}) 114 | timer := grohl.NewTimer(grohl.Data{"fn": "test"}) 115 | grohl.Log(grohl.Data{"status": "exec"}) 116 | timer.Finish() 117 | 118 | This would output: 119 | 120 | fn=test at=start 121 | status=exec 122 | fn=test at=finish elapsed=0.300 123 | 124 | You can change the time unit that Grohl uses to "milliseconds" (the default is 125 | "seconds"): 126 | 127 | grohl.SetTimeUnit("ms") 128 | 129 | // or with a *grohl.Context 130 | context.TimeUnit = "ms" 131 | 132 | You can also write to a custom Statter: 133 | 134 | timer := grohl.NewTimer(grohl.data{"fn": "test"}) 135 | // uses grohl.CurrentStatter by default 136 | timer.SetStatter(nil, 1.0, "my.silly.slow-process") 137 | timer.Finish() 138 | 139 | You can also set all *grohl.Timer objects to use the same statter. 140 | 141 | // You can call SetStatter() on a *grohl.Context to affect any *grohl.Timer 142 | // objects created from it. 143 | // 144 | // This affects _all_ *grohl.Timer objects. 145 | grohl.SetStatter(nil, 1.0, "my.silly") 146 | 147 | timer := grohl.NewTimer(grohl.data{"fn": "test"}) 148 | 149 | // set just the suffix of the statter bucket set above 150 | timer.StatterBucketSuffix("slow-process") 151 | 152 | // overwrite the whole bucket 153 | timer.StatterBucket = "my.silly.slow-process" 154 | 155 | // Sample only 50% of the timings. 156 | timer.StatterSampleRate = 0.5 157 | 158 | timer.Finish() 159 | 160 | Grohl can report Go errors: 161 | 162 | written, err := writer.Write(someBytes) 163 | if err ! nil { 164 | // context has the following from above: 165 | // grohl.Data{"app": "myapp", "deploy": "production", "ns": "server"} 166 | context.Report(err, grohl.Data{"written": written}) 167 | } 168 | 169 | Without any ErrorReporter set, this logs the following: 170 | 171 | app=myapp deploy=production ns=server at=exception class=*errors.errorString message="some message" 172 | app=myapp deploy=production ns=server at=exception class=*errors.errorString message="some message" site="stack trace line 1" 173 | app=myapp deploy=production ns=server at=exception class=*errors.errorString message="some message" site="stack trace line 2" 174 | app=myapp deploy=production ns=server at=exception class=*errors.errorString message="some message" site="stack trace line 3" 175 | 176 | You can set the default ErrorReporter too: 177 | 178 | myReporter := myreporter.New() 179 | grohl.SetErrorReporter(myReporter) 180 | 181 | */ 182 | package grohl 183 | --------------------------------------------------------------------------------