Update logrus dependency in Godep

Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
Stephen J Day 2015-03-23 20:09:19 -07:00
parent 73be4d5e3e
commit 00e6b0d2b8
21 changed files with 677 additions and 119 deletions

4
Godeps/Godeps.json generated
View file

@ -29,8 +29,8 @@
}, },
{ {
"ImportPath": "github.com/Sirupsen/logrus", "ImportPath": "github.com/Sirupsen/logrus",
"Comment": "v0.6.4-12-g467d9d5", "Comment": "v0.7.1",
"Rev": "467d9d55c2d2c17248441a8fc661561161f40d5e" "Rev": "3fc34d061b9c78a70db853c7cb6b0576b6d4f32d"
}, },
{ {
"ImportPath": "github.com/bugsnag/bugsnag-go", "ImportPath": "github.com/bugsnag/bugsnag-go",

View file

@ -2,9 +2,10 @@
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
yet stable (pre 1.0), the core API is unlikely to change much but please version yet stable (pre 1.0). Logrus itself is completely stable and has been used in
control your Logrus to make sure you aren't fetching latest `master` on every many large deployments. The core API is unlikely to change much but please
build.** version control your Logrus to make sure you aren't fetching latest `master` on
every build.**
Nicely color-coded in development (when a TTY is attached, otherwise just Nicely color-coded in development (when a TTY is attached, otherwise just
plain text): plain text):
@ -81,7 +82,7 @@ func init() {
// Use the Airbrake hook to report errors that have Error severity or above to // Use the Airbrake hook to report errors that have Error severity or above to
// an exception tracker. You can create custom hooks, see the Hooks section. // an exception tracker. You can create custom hooks, see the Hooks section.
log.AddHook(&logrus_airbrake.AirbrakeHook{}) log.AddHook(airbrake.NewHook("https://example.com", "xyz", "development"))
// Output to stderr instead of stdout, could also be a file. // Output to stderr instead of stdout, could also be a file.
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
@ -163,43 +164,8 @@ You can add hooks for logging levels. For example to send errors to an exception
tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to
multiple places simultaneously, e.g. syslog. multiple places simultaneously, e.g. syslog.
```go Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
// Not the real implementation of the Airbrake hook. Just a simple sample. `init`:
import (
log "github.com/Sirupsen/logrus"
)
func init() {
log.AddHook(new(AirbrakeHook))
}
type AirbrakeHook struct{}
// `Fire()` takes the entry that the hook is fired for. `entry.Data[]` contains
// the fields for the entry. See the Fields section of the README.
func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error {
err := airbrake.Notify(entry.Data["error"].(error))
if err != nil {
log.WithFields(log.Fields{
"source": "airbrake",
"endpoint": airbrake.Endpoint,
}).Info("Failed to send error to Airbrake")
}
return nil
}
// `Levels()` returns a slice of `Levels` the hook is fired for.
func (hook *AirbrakeHook) Levels() []log.Level {
return []log.Level{
log.ErrorLevel,
log.FatalLevel,
log.PanicLevel,
}
}
```
Logrus comes with built-in hooks. Add those, or your custom hook, in `init`:
```go ```go
import ( import (
@ -210,7 +176,7 @@ import (
) )
func init() { func init() {
log.AddHook(new(logrus_airbrake.AirbrakeHook)) log.AddHook(airbrake.NewHook("https://example.com", "xyz", "development"))
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "") hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
if err != nil { if err != nil {
@ -232,6 +198,9 @@ func init() {
Send errors to remote syslog server. Send errors to remote syslog server.
Uses standard library `log/syslog` behind the scenes. Uses standard library `log/syslog` behind the scenes.
* [`github.com/Sirupsen/logrus/hooks/bugsnag`](https://github.com/Sirupsen/logrus/blob/master/hooks/bugsnag/bugsnag.go)
Send errors to the Bugsnag exception tracking service.
* [`github.com/nubo/hiprus`](https://github.com/nubo/hiprus) * [`github.com/nubo/hiprus`](https://github.com/nubo/hiprus)
Send errors to a channel in hipchat. Send errors to a channel in hipchat.
@ -241,6 +210,9 @@ func init() {
* [`github.com/johntdyer/slackrus`](https://github.com/johntdyer/slackrus) * [`github.com/johntdyer/slackrus`](https://github.com/johntdyer/slackrus)
Hook for Slack chat. Hook for Slack chat.
* [`github.com/wercker/journalhook`](https://github.com/wercker/journalhook).
Hook for logging to `systemd-journald`.
#### Level logging #### Level logging
Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic. Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic.
@ -317,6 +289,11 @@ The built-in logging formatters are:
field to `true`. To force no colored output even if there is a TTY set the field to `true`. To force no colored output even if there is a TTY set the
`DisableColors` field to `true` `DisableColors` field to `true`
* `logrus.JSONFormatter`. Logs fields as JSON. * `logrus.JSONFormatter`. Logs fields as JSON.
* `logrus_logstash.LogstashFormatter`. Logs fields as Logstash Events (http://logstash.net).
```go
logrus.SetFormatter(&logrus_logstash.LogstashFormatter{Type: “application_name"})
```
Third party logging formatters: Third party logging formatters:
@ -347,7 +324,7 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
#### Logger as an `io.Writer` #### Logger as an `io.Writer`
Logrus can be transormed into an `io.Writer`. That writer is the end of an `io.Pipe` and it is your responsability to close it. Logrus can be transormed into an `io.Writer`. That writer is the end of an `io.Pipe` and it is your responsibility to close it.
```go ```go
w := logger.Writer() w := logger.Writer()
@ -366,7 +343,7 @@ and hooks. The level for those entries is `info`.
#### Rotation #### Rotation
Log rotation is not provided with Logrus. Log rotation should be done by an Log rotation is not provided with Logrus. Log rotation should be done by an
external program (like `logrotated(8)`) that can compress and delete old log external program (like `logrotate(8)`) that can compress and delete old log
entries. It should not be a feature of the application-level logger. entries. It should not be a feature of the application-level logger.

View file

@ -9,6 +9,7 @@ var log = logrus.New()
func init() { func init() {
log.Formatter = new(logrus.JSONFormatter) log.Formatter = new(logrus.JSONFormatter)
log.Formatter = new(logrus.TextFormatter) // default log.Formatter = new(logrus.TextFormatter) // default
log.Level = logrus.DebugLevel
} }
func main() { func main() {
@ -23,6 +24,11 @@ func main() {
} }
}() }()
log.WithFields(logrus.Fields{
"animal": "walrus",
"number": 8,
}).Debug("Started observing beach")
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"animal": "walrus", "animal": "walrus",
"size": 10, "size": 10,
@ -33,6 +39,10 @@ func main() {
"number": 122, "number": 122,
}).Warn("The group's number increased tremendously!") }).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"temperature": -4,
}).Debug("Temperature changes")
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"animal": "orca", "animal": "orca",
"size": 9009, "size": 9009,

View file

@ -3,21 +3,16 @@ package main
import ( import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/Sirupsen/logrus/hooks/airbrake" "github.com/Sirupsen/logrus/hooks/airbrake"
"github.com/tobi/airbrake-go"
) )
var log = logrus.New() var log = logrus.New()
func init() { func init() {
log.Formatter = new(logrus.TextFormatter) // default log.Formatter = new(logrus.TextFormatter) // default
log.Hooks.Add(new(logrus_airbrake.AirbrakeHook)) log.Hooks.Add(airbrake.NewHook("https://example.com", "xyz", "development"))
} }
func main() { func main() {
airbrake.Endpoint = "https://exceptions.whatever.com/notifier_api/v2/notices.xml"
airbrake.ApiKey = "whatever"
airbrake.Environment = "production"
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{
"animal": "walrus", "animal": "walrus",
"size": 10, "size": 10,

View file

@ -36,6 +36,8 @@ func SetLevel(level Level) {
// GetLevel returns the standard logger level. // GetLevel returns the standard logger level.
func GetLevel() Level { func GetLevel() Level {
std.mu.Lock()
defer std.mu.Unlock()
return std.Level return std.Level
} }

View file

@ -0,0 +1,48 @@
package logstash
import (
"encoding/json"
"fmt"
"github.com/Sirupsen/logrus"
"time"
)
// Formatter generates json in logstash format.
// Logstash site: http://logstash.net/
type LogstashFormatter struct {
Type string // if not empty use for logstash type field.
}
func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) {
entry.Data["@version"] = 1
entry.Data["@timestamp"] = entry.Time.Format(time.RFC3339)
// set message field
v, ok := entry.Data["message"]
if ok {
entry.Data["fields.message"] = v
}
entry.Data["message"] = entry.Message
// set level field
v, ok = entry.Data["level"]
if ok {
entry.Data["fields.level"] = v
}
entry.Data["level"] = entry.Level.String()
// set type field
if f.Type != "" {
v, ok = entry.Data["type"]
if ok {
entry.Data["fields.type"] = v
}
entry.Data["type"] = f.Type
}
serialized, err := json.Marshal(entry.Data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}

View file

@ -0,0 +1,52 @@
package logstash
import (
"bytes"
"encoding/json"
"github.com/Sirupsen/logrus"
"github.com/stretchr/testify/assert"
"testing"
)
func TestLogstashFormatter(t *testing.T) {
assert := assert.New(t)
lf := LogstashFormatter{Type: "abc"}
fields := logrus.Fields{
"message": "def",
"level": "ijk",
"type": "lmn",
"one": 1,
"pi": 3.14,
"bool": true,
}
entry := logrus.WithFields(fields)
entry.Message = "msg"
entry.Level = logrus.InfoLevel
b, _ := lf.Format(entry)
var data map[string]interface{}
dec := json.NewDecoder(bytes.NewReader(b))
dec.UseNumber()
dec.Decode(&data)
// base fields
assert.Equal(json.Number("1"), data["@version"])
assert.NotEmpty(data["@timestamp"])
assert.Equal("abc", data["type"])
assert.Equal("msg", data["message"])
assert.Equal("info", data["level"])
// substituted fields
assert.Equal("def", data["fields.message"])
assert.Equal("ijk", data["fields.level"])
assert.Equal("lmn", data["fields.type"])
// formats
assert.Equal(json.Number("1"), data["one"])
assert.Equal(json.Number("3.14"), data["pi"])
assert.Equal(true, data["bool"])
}

View file

@ -1,51 +1,51 @@
package logrus_airbrake package airbrake
import ( import (
"errors"
"fmt"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/tobi/airbrake-go" "github.com/tobi/airbrake-go"
) )
// AirbrakeHook to send exceptions to an exception-tracking service compatible // AirbrakeHook to send exceptions to an exception-tracking service compatible
// with the Airbrake API. You must set: // with the Airbrake API.
// * airbrake.Endpoint type airbrakeHook struct {
// * airbrake.ApiKey APIKey string
// * airbrake.Environment (only sends exceptions when set to "production") Endpoint string
// Environment string
// Before using this hook, to send an error. Entries that trigger an Error,
// Fatal or Panic should now include an "error" field to send to Airbrake.
type AirbrakeHook struct{}
func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error {
if entry.Data["error"] == nil {
entry.Logger.WithFields(logrus.Fields{
"source": "airbrake",
"endpoint": airbrake.Endpoint,
}).Warn("Exceptions sent to Airbrake must have an 'error' key with the error")
return nil
} }
func NewHook(endpoint, apiKey, env string) *airbrakeHook {
return &airbrakeHook{
APIKey: apiKey,
Endpoint: endpoint,
Environment: env,
}
}
func (hook *airbrakeHook) Fire(entry *logrus.Entry) error {
airbrake.ApiKey = hook.APIKey
airbrake.Endpoint = hook.Endpoint
airbrake.Environment = hook.Environment
var notifyErr error
err, ok := entry.Data["error"].(error) err, ok := entry.Data["error"].(error)
if !ok { if ok {
entry.Logger.WithFields(logrus.Fields{ notifyErr = err
"source": "airbrake", } else {
"endpoint": airbrake.Endpoint, notifyErr = errors.New(entry.Message)
}).Warn("Exceptions sent to Airbrake must have an `error` key of type `error`")
return nil
} }
airErr := airbrake.Notify(err) airErr := airbrake.Notify(notifyErr)
if airErr != nil { if airErr != nil {
entry.Logger.WithFields(logrus.Fields{ return fmt.Errorf("Failed to send error to Airbrake: %s", airErr)
"source": "airbrake",
"endpoint": airbrake.Endpoint,
"error": airErr,
}).Warn("Failed to send error to Airbrake")
} }
return nil return nil
} }
func (hook *AirbrakeHook) Levels() []logrus.Level { func (hook *airbrakeHook) Levels() []logrus.Level {
return []logrus.Level{ return []logrus.Level{
logrus.ErrorLevel, logrus.ErrorLevel,
logrus.FatalLevel, logrus.FatalLevel,

View file

@ -0,0 +1,133 @@
package airbrake
import (
"encoding/xml"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Sirupsen/logrus"
)
type notice struct {
Error NoticeError `xml:"error"`
}
type NoticeError struct {
Class string `xml:"class"`
Message string `xml:"message"`
}
type customErr struct {
msg string
}
func (e *customErr) Error() string {
return e.msg
}
const (
testAPIKey = "abcxyz"
testEnv = "development"
expectedClass = "*airbrake.customErr"
expectedMsg = "foo"
unintendedMsg = "Airbrake will not see this string"
)
var (
noticeError = make(chan NoticeError, 1)
)
// TestLogEntryMessageReceived checks if invoking Logrus' log.Error
// method causes an XML payload containing the log entry message is received
// by a HTTP server emulating an Airbrake-compatible endpoint.
func TestLogEntryMessageReceived(t *testing.T) {
log := logrus.New()
ts := startAirbrakeServer(t)
defer ts.Close()
hook := NewHook(ts.URL, testAPIKey, "production")
log.Hooks.Add(hook)
log.Error(expectedMsg)
select {
case received := <-noticeError:
if received.Message != expectedMsg {
t.Errorf("Unexpected message received: %s", received.Message)
}
case <-time.After(time.Second):
t.Error("Timed out; no notice received by Airbrake API")
}
}
// TestLogEntryMessageReceived confirms that, when passing an error type using
// logrus.Fields, a HTTP server emulating an Airbrake endpoint receives the
// error message returned by the Error() method on the error interface
// rather than the logrus.Entry.Message string.
func TestLogEntryWithErrorReceived(t *testing.T) {
log := logrus.New()
ts := startAirbrakeServer(t)
defer ts.Close()
hook := NewHook(ts.URL, testAPIKey, "production")
log.Hooks.Add(hook)
log.WithFields(logrus.Fields{
"error": &customErr{expectedMsg},
}).Error(unintendedMsg)
select {
case received := <-noticeError:
if received.Message != expectedMsg {
t.Errorf("Unexpected message received: %s", received.Message)
}
if received.Class != expectedClass {
t.Errorf("Unexpected error class: %s", received.Class)
}
case <-time.After(time.Second):
t.Error("Timed out; no notice received by Airbrake API")
}
}
// TestLogEntryWithNonErrorTypeNotReceived confirms that, when passing a
// non-error type using logrus.Fields, a HTTP server emulating an Airbrake
// endpoint receives the logrus.Entry.Message string.
//
// Only error types are supported when setting the 'error' field using
// logrus.WithFields().
func TestLogEntryWithNonErrorTypeNotReceived(t *testing.T) {
log := logrus.New()
ts := startAirbrakeServer(t)
defer ts.Close()
hook := NewHook(ts.URL, testAPIKey, "production")
log.Hooks.Add(hook)
log.WithFields(logrus.Fields{
"error": expectedMsg,
}).Error(unintendedMsg)
select {
case received := <-noticeError:
if received.Message != unintendedMsg {
t.Errorf("Unexpected message received: %s", received.Message)
}
case <-time.After(time.Second):
t.Error("Timed out; no notice received by Airbrake API")
}
}
func startAirbrakeServer(t *testing.T) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var notice notice
if err := xml.NewDecoder(r.Body).Decode(&notice); err != nil {
t.Error(err)
}
r.Body.Close()
noticeError <- notice.Error
}))
return ts
}

View file

@ -0,0 +1,68 @@
package logrus_bugsnag
import (
"errors"
"github.com/Sirupsen/logrus"
"github.com/bugsnag/bugsnag-go"
)
type bugsnagHook struct{}
// ErrBugsnagUnconfigured is returned if NewBugsnagHook is called before
// bugsnag.Configure. Bugsnag must be configured before the hook.
var ErrBugsnagUnconfigured = errors.New("bugsnag must be configured before installing this logrus hook")
// ErrBugsnagSendFailed indicates that the hook failed to submit an error to
// bugsnag. The error was successfully generated, but `bugsnag.Notify()`
// failed.
type ErrBugsnagSendFailed struct {
err error
}
func (e ErrBugsnagSendFailed) Error() string {
return "failed to send error to Bugsnag: " + e.err.Error()
}
// NewBugsnagHook initializes a logrus hook which sends exceptions to an
// exception-tracking service compatible with the Bugsnag API. Before using
// this hook, you must call bugsnag.Configure(). The returned object should be
// registered with a log via `AddHook()`
//
// Entries that trigger an Error, Fatal or Panic should now include an "error"
// field to send to Bugsnag.
func NewBugsnagHook() (*bugsnagHook, error) {
if bugsnag.Config.APIKey == "" {
return nil, ErrBugsnagUnconfigured
}
return &bugsnagHook{}, nil
}
// Fire forwards an error to Bugsnag. Given a logrus.Entry, it extracts the
// "error" field (or the Message if the error isn't present) and sends it off.
func (hook *bugsnagHook) Fire(entry *logrus.Entry) error {
var notifyErr error
err, ok := entry.Data["error"].(error)
if ok {
notifyErr = err
} else {
notifyErr = errors.New(entry.Message)
}
bugsnagErr := bugsnag.Notify(notifyErr)
if bugsnagErr != nil {
return ErrBugsnagSendFailed{bugsnagErr}
}
return nil
}
// Levels enumerates the log levels on which the error should be forwarded to
// bugsnag: everything at or above the "Error" level.
func (hook *bugsnagHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}
}

View file

@ -0,0 +1,64 @@
package logrus_bugsnag
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Sirupsen/logrus"
"github.com/bugsnag/bugsnag-go"
)
type notice struct {
Events []struct {
Exceptions []struct {
Message string `json:"message"`
} `json:"exceptions"`
} `json:"events"`
}
func TestNoticeReceived(t *testing.T) {
msg := make(chan string, 1)
expectedMsg := "foo"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var notice notice
data, _ := ioutil.ReadAll(r.Body)
if err := json.Unmarshal(data, &notice); err != nil {
t.Error(err)
}
_ = r.Body.Close()
msg <- notice.Events[0].Exceptions[0].Message
}))
defer ts.Close()
hook := &bugsnagHook{}
bugsnag.Configure(bugsnag.Configuration{
Endpoint: ts.URL,
ReleaseStage: "production",
APIKey: "12345678901234567890123456789012",
Synchronous: true,
})
log := logrus.New()
log.Hooks.Add(hook)
log.WithFields(logrus.Fields{
"error": errors.New(expectedMsg),
}).Error("Bugsnag will not see this string")
select {
case received := <-msg:
if received != expectedMsg {
t.Errorf("Unexpected message received: %s", received)
}
case <-time.After(time.Second):
t.Error("Timed out; no notice received by Bugsnag API")
}
}

View file

@ -57,5 +57,5 @@ with a call to `NewSentryHook`. This can be changed by assigning a value to the
```go ```go
hook, _ := logrus_sentry.NewSentryHook(...) hook, _ := logrus_sentry.NewSentryHook(...)
hook.Timeout = 20*time.Seconds hook.Timeout = 20*time.Second
``` ```

View file

@ -11,8 +11,15 @@ type JSONFormatter struct{}
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+3) data := make(Fields, len(entry.Data)+3)
for k, v := range entry.Data { for k, v := range entry.Data {
switch v := v.(type) {
case error:
// Otherwise errors are ignored by `encoding/json`
// https://github.com/Sirupsen/logrus/issues/137
data[k] = v.Error()
default:
data[k] = v data[k] = v
} }
}
prefixFieldClashes(data) prefixFieldClashes(data)
data["time"] = entry.Time.Format(time.RFC3339) data["time"] = entry.Time.Format(time.RFC3339)
data["msg"] = entry.Message data["msg"] = entry.Message

View file

@ -0,0 +1,120 @@
package logrus
import (
"encoding/json"
"errors"
"testing"
)
func TestErrorNotLost(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("error", errors.New("wild walrus")))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["error"] != "wild walrus" {
t.Fatal("Error field not set")
}
}
func TestErrorNotLostOnFieldNotNamedError(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("omg", errors.New("wild walrus")))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["omg"] != "wild walrus" {
t.Fatal("Error field not set")
}
}
func TestFieldClashWithTime(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("time", "right now!"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["fields.time"] != "right now!" {
t.Fatal("fields.time not set to original time field")
}
if entry["time"] != "0001-01-01T00:00:00Z" {
t.Fatal("time field not set to current time, was: ", entry["time"])
}
}
func TestFieldClashWithMsg(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("msg", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["fields.msg"] != "something" {
t.Fatal("fields.msg not set to original msg field")
}
}
func TestFieldClashWithLevel(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("level", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["fields.level"] != "something" {
t.Fatal("fields.level not set to original level field")
}
}
func TestJSONEntryEndsWithNewline(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("level", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
if b[len(b)-1] != '\n' {
t.Fatal("Expected JSON log entry to end with a newline")
}
}

View file

@ -65,97 +65,139 @@ func (logger *Logger) WithFields(fields Fields) *Entry {
} }
func (logger *Logger) Debugf(format string, args ...interface{}) { func (logger *Logger) Debugf(format string, args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debugf(format, args...) NewEntry(logger).Debugf(format, args...)
} }
}
func (logger *Logger) Infof(format string, args ...interface{}) { func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Infof(format, args...) NewEntry(logger).Infof(format, args...)
} }
}
func (logger *Logger) Printf(format string, args ...interface{}) { func (logger *Logger) Printf(format string, args ...interface{}) {
NewEntry(logger).Printf(format, args...) NewEntry(logger).Printf(format, args...)
} }
func (logger *Logger) Warnf(format string, args ...interface{}) { func (logger *Logger) Warnf(format string, args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...) NewEntry(logger).Warnf(format, args...)
} }
}
func (logger *Logger) Warningf(format string, args ...interface{}) { func (logger *Logger) Warningf(format string, args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...) NewEntry(logger).Warnf(format, args...)
} }
}
func (logger *Logger) Errorf(format string, args ...interface{}) { func (logger *Logger) Errorf(format string, args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Errorf(format, args...) NewEntry(logger).Errorf(format, args...)
} }
}
func (logger *Logger) Fatalf(format string, args ...interface{}) { func (logger *Logger) Fatalf(format string, args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatalf(format, args...) NewEntry(logger).Fatalf(format, args...)
} }
}
func (logger *Logger) Panicf(format string, args ...interface{}) { func (logger *Logger) Panicf(format string, args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panicf(format, args...) NewEntry(logger).Panicf(format, args...)
} }
}
func (logger *Logger) Debug(args ...interface{}) { func (logger *Logger) Debug(args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debug(args...) NewEntry(logger).Debug(args...)
} }
}
func (logger *Logger) Info(args ...interface{}) { func (logger *Logger) Info(args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Info(args...) NewEntry(logger).Info(args...)
} }
}
func (logger *Logger) Print(args ...interface{}) { func (logger *Logger) Print(args ...interface{}) {
NewEntry(logger).Info(args...) NewEntry(logger).Info(args...)
} }
func (logger *Logger) Warn(args ...interface{}) { func (logger *Logger) Warn(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...) NewEntry(logger).Warn(args...)
} }
}
func (logger *Logger) Warning(args ...interface{}) { func (logger *Logger) Warning(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...) NewEntry(logger).Warn(args...)
} }
}
func (logger *Logger) Error(args ...interface{}) { func (logger *Logger) Error(args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Error(args...) NewEntry(logger).Error(args...)
} }
}
func (logger *Logger) Fatal(args ...interface{}) { func (logger *Logger) Fatal(args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatal(args...) NewEntry(logger).Fatal(args...)
} }
}
func (logger *Logger) Panic(args ...interface{}) { func (logger *Logger) Panic(args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panic(args...) NewEntry(logger).Panic(args...)
} }
}
func (logger *Logger) Debugln(args ...interface{}) { func (logger *Logger) Debugln(args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debugln(args...) NewEntry(logger).Debugln(args...)
} }
}
func (logger *Logger) Infoln(args ...interface{}) { func (logger *Logger) Infoln(args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Infoln(args...) NewEntry(logger).Infoln(args...)
} }
}
func (logger *Logger) Println(args ...interface{}) { func (logger *Logger) Println(args ...interface{}) {
NewEntry(logger).Println(args...) NewEntry(logger).Println(args...)
} }
func (logger *Logger) Warnln(args ...interface{}) { func (logger *Logger) Warnln(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...) NewEntry(logger).Warnln(args...)
} }
}
func (logger *Logger) Warningln(args ...interface{}) { func (logger *Logger) Warningln(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...) NewEntry(logger).Warnln(args...)
} }
}
func (logger *Logger) Errorln(args ...interface{}) { func (logger *Logger) Errorln(args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Errorln(args...) NewEntry(logger).Errorln(args...)
} }
}
func (logger *Logger) Fatalln(args ...interface{}) { func (logger *Logger) Fatalln(args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatalln(args...) NewEntry(logger).Fatalln(args...)
} }
}
func (logger *Logger) Panicln(args ...interface{}) { func (logger *Logger) Panicln(args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panicln(args...) NewEntry(logger).Panicln(args...)
} }
}

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"strconv" "strconv"
"strings" "strings"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -281,3 +282,20 @@ func TestParseLevel(t *testing.T) {
l, err = ParseLevel("invalid") l, err = ParseLevel("invalid")
assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error()) assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
} }
func TestGetSetLevelRace(t *testing.T) {
wg := sync.WaitGroup{}
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if i%2 == 0 {
SetLevel(InfoLevel)
} else {
GetLevel()
}
}(i)
}
wg.Wait()
}

View file

@ -3,7 +3,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build linux,!appengine darwin freebsd openbsd // +build linux darwin freebsd openbsd
package logrus package logrus

View file

@ -1,4 +1,3 @@
package logrus package logrus
import "syscall" import "syscall"

View file

@ -3,7 +3,6 @@ package logrus
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"regexp"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -15,12 +14,12 @@ const (
green = 32 green = 32
yellow = 33 yellow = 33
blue = 34 blue = 34
gray = 37
) )
var ( var (
baseTimestamp time.Time baseTimestamp time.Time
isTerminal bool isTerminal bool
noQuoteNeeded *regexp.Regexp
) )
func init() { func init() {
@ -35,19 +34,33 @@ func miniTS() int {
type TextFormatter struct { type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors. // Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool ForceColors bool
// Force disabling colors.
DisableColors bool DisableColors bool
// Set to true to disable timestamp logging (useful when the output
// is redirected to a logging system already adding a timestamp) // Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps.
DisableTimestamp bool DisableTimestamp bool
// Enable logging the full timestamp when a TTY is attached instead of just
// the time passed since beginning of execution.
FullTimestamp bool
// The fields are sorted by default for a consistent output. For applications
// that log extremely frequently and don't use the JSON formatter this may not
// be desired.
DisableSorting bool
} }
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
var keys []string = make([]string, 0, len(entry.Data))
var keys []string
for k := range entry.Data { for k := range entry.Data {
keys = append(keys, k) keys = append(keys, k)
} }
if !f.DisableSorting {
sort.Strings(keys) sort.Strings(keys)
}
b := &bytes.Buffer{} b := &bytes.Buffer{}
@ -56,7 +69,7 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
isColored := (f.ForceColors || isTerminal) && !f.DisableColors isColored := (f.ForceColors || isTerminal) && !f.DisableColors
if isColored { if isColored {
printColored(b, entry, keys) f.printColored(b, entry, keys)
} else { } else {
if !f.DisableTimestamp { if !f.DisableTimestamp {
f.appendKeyValue(b, "time", entry.Time.Format(time.RFC3339)) f.appendKeyValue(b, "time", entry.Time.Format(time.RFC3339))
@ -72,9 +85,11 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
return b.Bytes(), nil return b.Bytes(), nil
} }
func printColored(b *bytes.Buffer, entry *Entry, keys []string) { func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string) {
var levelColor int var levelColor int
switch entry.Level { switch entry.Level {
case DebugLevel:
levelColor = gray
case WarnLevel: case WarnLevel:
levelColor = yellow levelColor = yellow
case ErrorLevel, FatalLevel, PanicLevel: case ErrorLevel, FatalLevel, PanicLevel:
@ -85,7 +100,11 @@ func printColored(b *bytes.Buffer, entry *Entry, keys []string) {
levelText := strings.ToUpper(entry.Level.String())[0:4] levelText := strings.ToUpper(entry.Level.String())[0:4]
if !f.FullTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message) fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
} else {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(time.RFC3339), entry.Message)
}
for _, k := range keys { for _, k := range keys {
v := entry.Data[k] v := entry.Data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v) fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v)
@ -96,7 +115,7 @@ func needsQuoting(text string) bool {
for _, ch := range text { for _, ch := range text {
if !((ch >= 'a' && ch <= 'z') || if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch < '9') || (ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.') { ch == '-' || ch == '.') {
return false return false
} }

View file

@ -25,9 +25,13 @@ func TestQuoting(t *testing.T) {
checkQuoting(false, "abcd") checkQuoting(false, "abcd")
checkQuoting(false, "v1.0") checkQuoting(false, "v1.0")
checkQuoting(false, "1234567890")
checkQuoting(true, "/foobar") checkQuoting(true, "/foobar")
checkQuoting(true, "x y") checkQuoting(true, "x y")
checkQuoting(true, "x,y") checkQuoting(true, "x,y")
checkQuoting(false, errors.New("invalid")) checkQuoting(false, errors.New("invalid"))
checkQuoting(true, errors.New("invalid argument")) checkQuoting(true, errors.New("invalid argument"))
} }
// TODO add tests for sorting etc., this requires a parser for the text
// formatter output.

View file

@ -6,7 +6,7 @@ import (
"runtime" "runtime"
) )
func (logger *Logger) Writer() (*io.PipeWriter) { func (logger *Logger) Writer() *io.PipeWriter {
reader, writer := io.Pipe() reader, writer := io.Pipe()
go logger.writerScanner(reader) go logger.writerScanner(reader)