Merge pull request #293 from stevvooe/log-formatter-configuration

main, configuration, doc: allow logging formatter to be configured
This commit is contained in:
Stephen Day 2015-03-25 13:34:42 -07:00
commit e9c69ffe14
26 changed files with 845 additions and 142 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 { func NewHook(endpoint, apiKey, env string) *airbrakeHook {
if entry.Data["error"] == nil { return &airbrakeHook{
entry.Logger.WithFields(logrus.Fields{ APIKey: apiKey,
"source": "airbrake", Endpoint: endpoint,
"endpoint": airbrake.Endpoint, Environment: env,
}).Warn("Exceptions sent to Airbrake must have an 'error' key with the error")
return nil
} }
}
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,11 +65,15 @@ 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{}) {
@ -77,31 +81,45 @@ func (logger *Logger) Printf(format string, args ...interface{}) {
} }
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{}) {
@ -109,31 +127,45 @@ func (logger *Logger) Print(args ...interface{}) {
} }
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{}) {
@ -141,21 +173,31 @@ func (logger *Logger) Println(args ...interface{}) {
} }
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)

View file

@ -1,5 +1,9 @@
version: 0.1 version: 0.1
loglevel: debug log:
level: debug
fields:
service: registry
environment: development
storage: storage:
filesystem: filesystem:
rootdirectory: /tmp/registry-dev rootdirectory: /tmp/registry-dev

View file

@ -9,6 +9,7 @@ import (
"os" "os"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/Sirupsen/logrus/formatters/logstash"
"github.com/bugsnag/bugsnag-go" "github.com/bugsnag/bugsnag-go"
"github.com/docker/distribution/configuration" "github.com/docker/distribution/configuration"
ctxu "github.com/docker/distribution/context" ctxu "github.com/docker/distribution/context"
@ -49,9 +50,10 @@ func main() {
fatalf("configuration error: %v", err) fatalf("configuration error: %v", err)
} }
log.SetLevel(logLevel(config.Loglevel)) ctx, err = configureLogging(ctx, config)
ctx = context.WithValue(ctx, "version", version.Version) if err != nil {
ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, "version")) fatalf("error configuring logger: %v", err)
}
app := handlers.NewApp(ctx, *config) app := handlers.NewApp(ctx, *config)
handler := configureReporting(app) handler := configureReporting(app)
@ -111,16 +113,6 @@ func resolveConfiguration() (*configuration.Configuration, error) {
return config, nil return config, nil
} }
func logLevel(level configuration.Loglevel) log.Level {
l, err := log.ParseLevel(string(level))
if err != nil {
log.Warnf("error parsing level %q: %v", level, err)
l = log.InfoLevel
}
return l
}
func configureReporting(app *handlers.App) http.Handler { func configureReporting(app *handlers.App) http.Handler {
var handler http.Handler = app var handler http.Handler = app
@ -157,6 +149,93 @@ func configureReporting(app *handlers.App) http.Handler {
return handler return handler
} }
// configureLogging prepares the context with a logger using the
// configuration.
func configureLogging(ctx context.Context, config *configuration.Configuration) (context.Context, error) {
if config.Log.Level == "" && config.Log.Formatter == "" {
// If no config for logging is set, fallback to deprecated "Loglevel".
log.SetLevel(logLevel(config.Loglevel))
ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, "version"))
return ctx, nil
}
log.SetLevel(logLevel(config.Log.Level))
switch config.Log.Formatter {
case "json":
log.SetFormatter(&log.JSONFormatter{})
case "text":
log.SetFormatter(&log.TextFormatter{})
case "logstash":
log.SetFormatter(&logstash.LogstashFormatter{})
default:
// just let the library use default on empty string.
if config.Log.Formatter != "" {
return ctx, fmt.Errorf("unsupported logging formatter: %q", config.Log.Formatter)
}
}
if config.Log.Formatter != "" {
log.Debugf("using %q logging formatter", config.Log.Formatter)
}
// log the application version with messages
ctx = context.WithValue(ctx, "version", version.Version)
if len(config.Log.Fields) > 0 {
// build up the static fields, if present.
var fields []interface{}
for k := range config.Log.Fields {
fields = append(fields, k)
}
ctx = withMapContext(ctx, config.Log.Fields)
ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, fields...))
}
return ctx, nil
}
func logLevel(level configuration.Loglevel) log.Level {
l, err := log.ParseLevel(string(level))
if err != nil {
l = log.InfoLevel
log.Warnf("error parsing level %q: %v, using %q ", level, err, l)
}
return l
}
// stringMapContext is a simple context implementation that checks a map for a
// key, falling back to a parent if not present.
type stringMapContext struct {
context.Context
m map[string]string
}
// withMapContext returns a context that proxies lookups through a map.
func withMapContext(ctx context.Context, m map[string]string) context.Context {
mo := make(map[string]string, len(m)) // make our own copy.
for k, v := range m {
mo[k] = v
}
return stringMapContext{
Context: ctx,
m: mo,
}
}
func (smc stringMapContext) Value(key interface{}) interface{} {
if ks, ok := key.(string); ok {
if v, ok := smc.m[ks]; ok {
return v
}
}
return smc.Context.Value(key)
}
// debugServer starts the debug server with pprof, expvar among other // debugServer starts the debug server with pprof, expvar among other
// endpoints. The addr should not be exposed externally. For most of these to // endpoints. The addr should not be exposed externally. For most of these to
// work, tls cannot be enabled on the endpoint, so it is generally separate. // work, tls cannot be enabled on the endpoint, so it is generally separate.

View file

@ -16,7 +16,23 @@ type Configuration struct {
// Version is the version which defines the format of the rest of the configuration // Version is the version which defines the format of the rest of the configuration
Version Version `yaml:"version"` Version Version `yaml:"version"`
// Loglevel is the level at which registry operations are logged // Log supports setting various parameters related to the logging
// subsystem.
Log struct {
// Level is the granularity at which registry operations are logged.
Level Loglevel `yaml:"level"`
// Formatter overrides the default formatter with another. Options
// include "text", "json" and "logstash".
Formatter string `yaml:"formatter"`
// Fields allows users to specify static string fields to include in
// the logger context.
Fields map[string]string `yaml:"fields"`
}
// Loglevel is the level at which registry operations are logged. This is
// deprecated. Please use Log.Level in the future.
Loglevel Loglevel `yaml:"loglevel"` Loglevel Loglevel `yaml:"loglevel"`
// Storage is the configuration for the registry's storage driver // Storage is the configuration for the registry's storage driver

View file

@ -16,6 +16,13 @@ func Test(t *testing.T) { TestingT(t) }
// configStruct is a canonical example configuration, which should map to configYamlV0_1 // configStruct is a canonical example configuration, which should map to configYamlV0_1
var configStruct = Configuration{ var configStruct = Configuration{
Version: "0.1", Version: "0.1",
Log: struct {
Level Loglevel `yaml:"level"`
Formatter string `yaml:"formatter"`
Fields map[string]string `yaml:"fields"`
}{
Fields: map[string]string{"environment": "test"},
},
Loglevel: "info", Loglevel: "info",
Storage: Storage{ Storage: Storage{
"s3": Parameters{ "s3": Parameters{
@ -57,6 +64,9 @@ var configStruct = Configuration{
// configYamlV0_1 is a Version 0.1 yaml document representing configStruct // configYamlV0_1 is a Version 0.1 yaml document representing configStruct
var configYamlV0_1 = ` var configYamlV0_1 = `
version: 0.1 version: 0.1
log:
fields:
environment: test
loglevel: info loglevel: info
storage: storage:
s3: s3:
@ -136,6 +146,7 @@ func (suite *ConfigSuite) TestParseSimple(c *C) {
func (suite *ConfigSuite) TestParseInmemory(c *C) { func (suite *ConfigSuite) TestParseInmemory(c *C) {
suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}} suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
suite.expectedConfig.Reporting = Reporting{} suite.expectedConfig.Reporting = Reporting{}
suite.expectedConfig.Log.Fields = nil
config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1))) config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1)))
c.Assert(err, IsNil) c.Assert(err, IsNil)
@ -150,6 +161,7 @@ func (suite *ConfigSuite) TestParseIncomplete(c *C) {
_, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml))) _, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml)))
c.Assert(err, NotNil) c.Assert(err, NotNil)
suite.expectedConfig.Log.Fields = nil
suite.expectedConfig.Storage = Storage{"filesystem": Parameters{"rootdirectory": "/tmp/testroot"}} suite.expectedConfig.Storage = Storage{"filesystem": Parameters{"rootdirectory": "/tmp/testroot"}}
suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}} suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
suite.expectedConfig.Reporting = Reporting{} suite.expectedConfig.Reporting = Reporting{}
@ -303,6 +315,12 @@ func copyConfig(config Configuration) *Configuration {
configCopy.Version = MajorMinorVersion(config.Version.Major(), config.Version.Minor()) configCopy.Version = MajorMinorVersion(config.Version.Major(), config.Version.Minor())
configCopy.Loglevel = config.Loglevel configCopy.Loglevel = config.Loglevel
configCopy.Log = config.Log
configCopy.Log.Fields = make(map[string]string, len(config.Log.Fields))
for k, v := range config.Log.Fields {
configCopy.Log.Fields[k] = v
}
configCopy.Storage = Storage{config.Storage.Type(): Parameters{}} configCopy.Storage = Storage{config.Storage.Type(): Parameters{}}
for k, v := range config.Storage.Parameters() { for k, v := range config.Storage.Parameters() {
configCopy.Storage.setParameter(k, v) configCopy.Storage.setParameter(k, v)

View file

@ -4,7 +4,13 @@ Below is a comprehensive example of all possible configuration options for the r
```yaml ```yaml
version: 0.1 version: 0.1
loglevel: debug log:
level: debug
formatter: text
fields:
service: registry
environment: staging
loglevel: debug # deprecated: use "log"
storage: storage:
filesystem: filesystem:
rootdirectory: /tmp/registry rootdirectory: /tmp/registry
@ -87,18 +93,40 @@ The version option is **required** and indicates the version of the configuratio
N.B. The version of the registry software may be found at [/version/version.go](https://github.com/docker/distribution/blob/master/version/version.go) N.B. The version of the registry software may be found at [/version/version.go](https://github.com/docker/distribution/blob/master/version/version.go)
## log
The log subsection configures the behavior of the logging system. The logging
system outputs everything to stdout. The granularity and format of the log
messages can be adjusted with this configuration section.
```yaml
log:
level: debug
formatter: text
fields:
service: registry
environment: staging
```
- level: **Optional** - Sets the sensitivity of logging output. Permitted
values are `error`, `warn`, `info` and `debug`. The default is `info`.
- formatter: **Optional** - This selects the format of logging output, which
mostly affects how keyed attributes for a log line are encoded. Options are
"text", "json" or "logstash". The default is "text".
- fields: **Optional** - A map of field names to values that will be added to
every log line for the context. This is useful for identifying log messages
source after being mixed in other systems.
## loglevel ## loglevel
> **DEPRECATED:** Please use [log](#log) instead.
```yaml ```yaml
loglevel: debug loglevel: debug
``` ```
The loglevel option is **required** and sets the sensitivity of logging output. Permitted values are: Permitted values are `error`, `warn`, `info` and `debug`. The default is
`info`.
- ```error```
- ```warn```
- ```info```
- ```debug```
## storage ## storage