merge upstream, add tests, full method context

- added benchmarks, and assertions for timeliness
- replaced regex usage in package-name handling with a straight
  comparison to a cached value
- log the fullly-qualified method name, to remove ambiguity
    eg: github.com/fflintstone/yabba.dabba.doo
- clean up possibly-unsafe assumptions about using the standard logger,
  remove global lookup function to enforce safe usage
This commit is contained in:
Dave Clendenan 2016-12-01 10:23:00 -08:00
commit f08011a10f
12 changed files with 121 additions and 41 deletions

View File

@ -1,4 +1,4 @@
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/>&nbsp;[![Build Status](https://travis-ci.org/Sirupsen/logrus.svg?branch=master)](https://travis-ci.org/Sirupsen/logrus)&nbsp;[![GoDoc](https://godoc.org/github.com/Sirupsen/logrus?status.svg)](https://godoc.org/github.com/Sirupsen/logrus) # Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/>&nbsp;[![Build Status](https://travis-ci.org/sirupsen/logrus.svg?branch=master)](https://travis-ci.org/sirupsen/logrus)&nbsp;[![GoDoc](https://godoc.org/github.com/sirupsen/logrus?status.svg)](https://godoc.org/github.com/sirupsen/logrus)
Logrus is a structured logger for Go (golang), completely API compatible with Logrus is a structured logger for Go (golang), completely API compatible with
the standard library logger. [Godoc][godoc]. **Please note the Logrus API is not the standard library logger. [Godoc][godoc]. **Please note the Logrus API is not
@ -77,7 +77,7 @@ The simplest way to use Logrus is simply the package-level exported logger:
package main package main
import ( import (
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func main() { func main() {
@ -88,7 +88,7 @@ func main() {
``` ```
Note that it's completely api-compatible with the stdlib logger, so you can Note that it's completely api-compatible with the stdlib logger, so you can
replace your `log` imports everywhere with `log "github.com/Sirupsen/logrus"` replace your `log` imports everywhere with `log "github.com/sirupsen/logrus"`
and you'll now have the flexibility of Logrus. You can customize it all you and you'll now have the flexibility of Logrus. You can customize it all you
want: want:
@ -97,7 +97,7 @@ package main
import ( import (
"os" "os"
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func init() { func init() {
@ -146,7 +146,7 @@ application, you can also create an instance of the `logrus` Logger:
package main package main
import ( import (
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// Create a new instance of the logger. You can have any number of instances. // Create a new instance of the logger. You can have any number of instances.
@ -199,9 +199,9 @@ Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
```go ```go
import ( import (
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "aibrake" "gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "aibrake"
logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog" logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
"log/syslog" "log/syslog"
) )
@ -226,7 +226,7 @@ Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/v
| [Airbrake](https://github.com/gemnasium/logrus-airbrake-hook) | Send errors to the Airbrake API V3. Uses the official [`gobrake`](https://github.com/airbrake/gobrake) behind the scenes. | | [Airbrake](https://github.com/gemnasium/logrus-airbrake-hook) | Send errors to the Airbrake API V3. Uses the official [`gobrake`](https://github.com/airbrake/gobrake) behind the scenes. |
| [Airbrake "legacy"](https://github.com/gemnasium/logrus-airbrake-legacy-hook) | Send errors to an exception tracking service compatible with the Airbrake API V2. Uses [`airbrake-go`](https://github.com/tobi/airbrake-go) behind the scenes. | | [Airbrake "legacy"](https://github.com/gemnasium/logrus-airbrake-legacy-hook) | Send errors to an exception tracking service compatible with the Airbrake API V2. Uses [`airbrake-go`](https://github.com/tobi/airbrake-go) behind the scenes. |
| [Papertrail](https://github.com/polds/logrus-papertrail-hook) | Send errors to the [Papertrail](https://papertrailapp.com) hosted logging service via UDP. | | [Papertrail](https://github.com/polds/logrus-papertrail-hook) | Send errors to the [Papertrail](https://papertrailapp.com) hosted logging service via UDP. |
| [Syslog](https://github.com/Sirupsen/logrus/blob/master/hooks/syslog/syslog.go) | Send errors to remote syslog server. Uses standard library `log/syslog` behind the scenes. | | [Syslog](https://github.com/sirupsen/logrus/blob/master/hooks/syslog/syslog.go) | Send errors to remote syslog server. Uses standard library `log/syslog` behind the scenes. |
| [Bugsnag](https://github.com/Shopify/logrus-bugsnag/blob/master/bugsnag.go) | Send errors to the Bugsnag exception tracking service. | | [Bugsnag](https://github.com/Shopify/logrus-bugsnag/blob/master/bugsnag.go) | Send errors to the Bugsnag exception tracking service. |
| [Sentry](https://github.com/evalphobia/logrus_sentry) | Send errors to the Sentry error logging and aggregation service. | | [Sentry](https://github.com/evalphobia/logrus_sentry) | Send errors to the Sentry error logging and aggregation service. |
| [Hiprus](https://github.com/nubo/hiprus) | Send errors to a channel in hipchat. | | [Hiprus](https://github.com/nubo/hiprus) | Send errors to a channel in hipchat. |
@ -306,7 +306,7 @@ could do:
```go ```go
import ( import (
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
init() { init() {
@ -431,7 +431,7 @@ logrus.RegisterExitHandler(handler)
... ...
``` ```
#### Thread safty #### Thread safety
By default Logger is protected by mutex for concurrent writes, this mutex is invoked when calling hooks and writing logs. By default Logger is protected by mutex for concurrent writes, this mutex is invoked when calling hooks and writing logs.
If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking. If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking.

4
doc.go
View File

@ -7,7 +7,7 @@ The simplest way to use Logrus is simply the package-level exported logger:
package main package main
import ( import (
log "github.com/Sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func main() { func main() {
@ -21,6 +21,6 @@ The simplest way to use Logrus is simply the package-level exported logger:
Output: Output:
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10 time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
For a full guide visit https://github.com/Sirupsen/logrus For a full guide visit https://github.com/sirupsen/logrus
*/ */
package logrus package logrus

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"regexp"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
@ -13,8 +12,14 @@ import (
var bufferPool *sync.Pool var bufferPool *sync.Pool
// regex for validation is external, to reduce compilation overhead // qualified package name, cached at first use
var matchesLogrus *regexp.Regexp var logrusPackage string
// Positions in the call stack when tracing to report the calling method
var minimumCallerDepth int
const maximumCallerDepth int = 25
const knownLogrusFrames int = 4
func init() { func init() {
bufferPool = &sync.Pool{ bufferPool = &sync.Pool{
@ -23,7 +28,8 @@ func init() {
}, },
} }
matchesLogrus, _ = regexp.Compile("logrus.*") // start at the bottom of the stack before the package-name cache is primed
minimumCallerDepth = 1
} }
// Defines the key when adding errors using WithError. // Defines the key when adding errors using WithError.
@ -96,24 +102,42 @@ func (entry *Entry) WithFields(fields Fields) *Entry {
return &Entry{Logger: entry.Logger, Data: data} return &Entry{Logger: entry.Logger, Data: data}
} }
// getCaller retrieves the name of the first non-logrus calling function // getPackageName reduces a fully qualified function name to the package name
func getCaller() (method string) { // There really ought to be to be a better way...
// Restrict the lookback to 25 frames - if it's further than that, report UNKNOWN func getPackageName(f string) string {
pcs := make([]uintptr, 25) for {
lastPeriod := strings.LastIndex(f, ".")
// the first non-logrus caller is at least three frames away lastSlash := strings.LastIndex(f, "/")
depth := runtime.Callers(3, pcs) if lastPeriod > lastSlash {
for i := 0; i < depth; i++ { f = f[:lastPeriod]
fullFuncName := runtime.FuncForPC(pcs[i]).Name() } else {
idx := strings.LastIndex(fullFuncName, "/") + 1 break
if idx > 0 { }
fullFuncName = fullFuncName[idx:]
} }
matched := matchesLogrus.MatchString(fullFuncName) return f
}
// If the caller isn't part of logrus, we're done // getCaller retrieves the name of the first non-logrus calling function
if !matched { func getCaller() (method string) {
// Restrict the lookback frames to avoid runaway lookups
pcs := make([]uintptr, maximumCallerDepth)
depth := runtime.Callers(minimumCallerDepth, pcs)
// cache this package's fully-qualified name
if logrusPackage == "" {
logrusPackage = getPackageName(runtime.FuncForPC(pcs[0]).Name())
// now that we have the cache, we can skip a minimum count of known-logrus functions
minimumCallerDepth = knownLogrusFrames
}
for i := 0; i < depth; i++ {
fullFuncName := runtime.FuncForPC(pcs[i]).Name()
pkg := getPackageName(fullFuncName)
// If the caller isn't part of this package, we're done
if pkg != logrusPackage {
if fullFuncName == "main.main" { if fullFuncName == "main.main" {
return "main" return "main"
} else { } else {

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var log = logrus.New() var log = logrus.New()

View File

@ -1,8 +1,14 @@
// +build ignore
// Do NOT include the above line in your code. This is a build constraint used
// to prevent import loops in the code whilst go get'ting it.
// Read more about build constraints in golang here:
// https://golang.org/pkg/go/build/#hdr-Build_Constraints
package main package main
import ( import (
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2" airbrake "gopkg.in/gemnasium/logrus-airbrake-hook.v2"
) )
var log = logrus.New() var log = logrus.New()

View File

@ -5,8 +5,8 @@
```go ```go
import ( import (
"log/syslog" "log/syslog"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog" logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
) )
func main() { func main() {
@ -24,8 +24,8 @@ If you want to connect to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "
```go ```go
import ( import (
"log/syslog" "log/syslog"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog" logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
) )
func main() { func main() {

View File

@ -4,9 +4,10 @@ package logrus_syslog
import ( import (
"fmt" "fmt"
"github.com/Sirupsen/logrus"
"log/syslog" "log/syslog"
"os" "os"
"github.com/sirupsen/logrus"
) )
// SyslogHook to send logs via syslog. // SyslogHook to send logs via syslog.

View File

@ -1,9 +1,10 @@
package logrus_syslog package logrus_syslog
import ( import (
"github.com/Sirupsen/logrus"
"log/syslog" "log/syslog"
"testing" "testing"
"github.com/sirupsen/logrus"
) )
func TestLocalhostAddAndPrint(t *testing.T) { func TestLocalhostAddAndPrint(t *testing.T) {

View File

@ -3,7 +3,7 @@ package test
import ( import (
"io/ioutil" "io/ioutil"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// test.Hook is a hook designed for dealing with logs in test scenarios. // test.Hook is a hook designed for dealing with logs in test scenarios.

View File

@ -3,7 +3,7 @@ package test
import ( import (
"testing" "testing"
"github.com/Sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View File

@ -46,7 +46,7 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
switch v := v.(type) { switch v := v.(type) {
case error: case error:
// Otherwise errors are ignored by `encoding/json` // Otherwise errors are ignored by `encoding/json`
// https://github.com/Sirupsen/logrus/issues/137 // https://github.com/sirupsen/logrus/issues/137
data[k] = v.Error() data[k] = v.Error()
default: default:
data[k] = v data[k] = v

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -312,6 +313,53 @@ func TestNestedLoggingReportsCorrectCaller(t *testing.T) {
logger.ReportCaller = false // return to default value logger.ReportCaller = false // return to default value
} }
func logLoop(iterations int, reportCaller bool) {
var buffer bytes.Buffer
logger := New()
logger.Out = &buffer
logger.Formatter = new(JSONFormatter)
logger.ReportCaller = reportCaller
for i := 0; i < iterations; i++ {
logger.Infof("round %d of %d", i, iterations)
}
}
// Assertions for upper bounds to reporting overhead
func TestCallerReportingOverhead(t *testing.T) {
iterations := 10000
before := time.Now()
logLoop(iterations, false)
during := time.Now()
logLoop(iterations, true)
after := time.Now()
elapsedNotReporting := during.Sub(before).Nanoseconds()
elapsedReporting := after.Sub(during).Nanoseconds()
maxDelta := 1 * time.Second
assert.WithinDuration(t, during, before, maxDelta,
"%d log calls without caller name lookup takes less than %d second(s) (was %d nanoseconds)",
iterations, maxDelta.Seconds(), elapsedNotReporting)
assert.WithinDuration(t, after, during, maxDelta,
"%d log calls without caller name lookup takes less than %d second(s) (was %d nanoseconds)",
iterations, maxDelta.Seconds(), elapsedReporting)
}
// benchmarks for both with and without caller-function reporting
func BenchmarkWithoutCallerTracing(b *testing.B) {
for i := 0; i < b.N; i++ {
logLoop(1000, false)
}
}
func BenchmarkWithCallerTracing(b *testing.B) {
for i := 0; i < b.N; i++ {
logLoop(1000, true)
}
}
func TestConvertLevelToString(t *testing.T) { func TestConvertLevelToString(t *testing.T) {
assert.Equal(t, "debug", DebugLevel.String()) assert.Equal(t, "debug", DebugLevel.String())
assert.Equal(t, "info", InfoLevel.String()) assert.Equal(t, "info", InfoLevel.String())