Changes relevant for Docker since 0.6.6 are (most other changes are hooks and
options for formatters):
* Debugging color output changed to gray.
* Don't quote the number 9 when it's by it self (i.e. `omg=9` instead of
`omg="8"`, this was the case for all other numbers)
* Performance is better when running a high logging level with lots of low-level
logging.
* Minor internal refactoring and more tests.
Signed-off-by: Simon Eskildsen <sirup@sirupsen.com>
| ... | ... |
@@ -53,7 +53,7 @@ clone hg code.google.com/p/gosqlite 74691fb6f837 |
| 53 | 53 |
|
| 54 | 54 |
clone git github.com/docker/libtrust 230dfd18c232 |
| 55 | 55 |
|
| 56 |
-clone git github.com/Sirupsen/logrus v0.6.6 |
|
| 56 |
+clone git github.com/Sirupsen/logrus v0.7.1 |
|
| 57 | 57 |
|
| 58 | 58 |
clone git github.com/go-fsnotify/fsnotify v1.0.4 |
| 59 | 59 |
|
| ... | ... |
@@ -82,7 +82,7 @@ func init() {
|
| 82 | 82 |
|
| 83 | 83 |
// Use the Airbrake hook to report errors that have Error severity or above to |
| 84 | 84 |
// an exception tracker. You can create custom hooks, see the Hooks section. |
| 85 |
- log.AddHook(&logrus_airbrake.AirbrakeHook{})
|
|
| 85 |
+ log.AddHook(airbrake.NewHook("https://example.com", "xyz", "development"))
|
|
| 86 | 86 |
|
| 87 | 87 |
// Output to stderr instead of stdout, could also be a file. |
| 88 | 88 |
log.SetOutput(os.Stderr) |
| ... | ... |
@@ -164,43 +164,8 @@ You can add hooks for logging levels. For example to send errors to an exception |
| 164 | 164 |
tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to |
| 165 | 165 |
multiple places simultaneously, e.g. syslog. |
| 166 | 166 |
|
| 167 |
-```go |
|
| 168 |
-// Not the real implementation of the Airbrake hook. Just a simple sample. |
|
| 169 |
-import ( |
|
| 170 |
- log "github.com/Sirupsen/logrus" |
|
| 171 |
-) |
|
| 172 |
- |
|
| 173 |
-func init() {
|
|
| 174 |
- log.AddHook(new(AirbrakeHook)) |
|
| 175 |
-} |
|
| 176 |
- |
|
| 177 |
-type AirbrakeHook struct{}
|
|
| 178 |
- |
|
| 179 |
-// `Fire()` takes the entry that the hook is fired for. `entry.Data[]` contains |
|
| 180 |
-// the fields for the entry. See the Fields section of the README. |
|
| 181 |
-func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error {
|
|
| 182 |
- err := airbrake.Notify(entry.Data["error"].(error)) |
|
| 183 |
- if err != nil {
|
|
| 184 |
- log.WithFields(log.Fields{
|
|
| 185 |
- "source": "airbrake", |
|
| 186 |
- "endpoint": airbrake.Endpoint, |
|
| 187 |
- }).Info("Failed to send error to Airbrake")
|
|
| 188 |
- } |
|
| 189 |
- |
|
| 190 |
- return nil |
|
| 191 |
-} |
|
| 192 |
- |
|
| 193 |
-// `Levels()` returns a slice of `Levels` the hook is fired for. |
|
| 194 |
-func (hook *AirbrakeHook) Levels() []log.Level {
|
|
| 195 |
- return []log.Level{
|
|
| 196 |
- log.ErrorLevel, |
|
| 197 |
- log.FatalLevel, |
|
| 198 |
- log.PanicLevel, |
|
| 199 |
- } |
|
| 200 |
-} |
|
| 201 |
-``` |
|
| 202 |
- |
|
| 203 |
-Logrus comes with built-in hooks. Add those, or your custom hook, in `init`: |
|
| 167 |
+Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in |
|
| 168 |
+`init`: |
|
| 204 | 169 |
|
| 205 | 170 |
```go |
| 206 | 171 |
import ( |
| ... | ... |
@@ -211,7 +176,7 @@ import ( |
| 211 | 211 |
) |
| 212 | 212 |
|
| 213 | 213 |
func init() {
|
| 214 |
- log.AddHook(new(logrus_airbrake.AirbrakeHook)) |
|
| 214 |
+ log.AddHook(airbrake.NewHook("https://example.com", "xyz", "development"))
|
|
| 215 | 215 |
|
| 216 | 216 |
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
| 217 | 217 |
if err != nil {
|
| ... | ... |
@@ -233,6 +198,9 @@ func init() {
|
| 233 | 233 |
Send errors to remote syslog server. |
| 234 | 234 |
Uses standard library `log/syslog` behind the scenes. |
| 235 | 235 |
|
| 236 |
+* [`github.com/Sirupsen/logrus/hooks/bugsnag`](https://github.com/Sirupsen/logrus/blob/master/hooks/bugsnag/bugsnag.go) |
|
| 237 |
+ Send errors to the Bugsnag exception tracking service. |
|
| 238 |
+ |
|
| 236 | 239 |
* [`github.com/nubo/hiprus`](https://github.com/nubo/hiprus) |
| 237 | 240 |
Send errors to a channel in hipchat. |
| 238 | 241 |
|
| ... | ... |
@@ -321,6 +289,11 @@ The built-in logging formatters are: |
| 321 | 321 |
field to `true`. To force no colored output even if there is a TTY set the |
| 322 | 322 |
`DisableColors` field to `true` |
| 323 | 323 |
* `logrus.JSONFormatter`. Logs fields as JSON. |
| 324 |
+* `logrus_logstash.LogstashFormatter`. Logs fields as Logstash Events (http://logstash.net). |
|
| 325 |
+ |
|
| 326 |
+ ```go |
|
| 327 |
+ logrus.SetFormatter(&logrus_logstash.LogstashFormatter{Type: “application_name"})
|
|
| 328 |
+ ``` |
|
| 324 | 329 |
|
| 325 | 330 |
Third party logging formatters: |
| 326 | 331 |
|
| ... | ... |
@@ -3,21 +3,16 @@ package main |
| 3 | 3 |
import ( |
| 4 | 4 |
"github.com/Sirupsen/logrus" |
| 5 | 5 |
"github.com/Sirupsen/logrus/hooks/airbrake" |
| 6 |
- "github.com/tobi/airbrake-go" |
|
| 7 | 6 |
) |
| 8 | 7 |
|
| 9 | 8 |
var log = logrus.New() |
| 10 | 9 |
|
| 11 | 10 |
func init() {
|
| 12 | 11 |
log.Formatter = new(logrus.TextFormatter) // default |
| 13 |
- log.Hooks.Add(new(logrus_airbrake.AirbrakeHook)) |
|
| 12 |
+ log.Hooks.Add(airbrake.NewHook("https://example.com", "xyz", "development"))
|
|
| 14 | 13 |
} |
| 15 | 14 |
|
| 16 | 15 |
func main() {
|
| 17 |
- airbrake.Endpoint = "https://exceptions.whatever.com/notifier_api/v2/notices.xml" |
|
| 18 |
- airbrake.ApiKey = "whatever" |
|
| 19 |
- airbrake.Environment = "production" |
|
| 20 |
- |
|
| 21 | 16 |
log.WithFields(logrus.Fields{
|
| 22 | 17 |
"animal": "walrus", |
| 23 | 18 |
"size": 10, |
| 24 | 19 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,48 @@ |
| 0 |
+package logstash |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "fmt" |
|
| 5 |
+ "github.com/Sirupsen/logrus" |
|
| 6 |
+ "time" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// Formatter generates json in logstash format. |
|
| 10 |
+// Logstash site: http://logstash.net/ |
|
| 11 |
+type LogstashFormatter struct {
|
|
| 12 |
+ Type string // if not empty use for logstash type field. |
|
| 13 |
+} |
|
| 14 |
+ |
|
| 15 |
+func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
|
| 16 |
+ entry.Data["@version"] = 1 |
|
| 17 |
+ entry.Data["@timestamp"] = entry.Time.Format(time.RFC3339) |
|
| 18 |
+ |
|
| 19 |
+ // set message field |
|
| 20 |
+ v, ok := entry.Data["message"] |
|
| 21 |
+ if ok {
|
|
| 22 |
+ entry.Data["fields.message"] = v |
|
| 23 |
+ } |
|
| 24 |
+ entry.Data["message"] = entry.Message |
|
| 25 |
+ |
|
| 26 |
+ // set level field |
|
| 27 |
+ v, ok = entry.Data["level"] |
|
| 28 |
+ if ok {
|
|
| 29 |
+ entry.Data["fields.level"] = v |
|
| 30 |
+ } |
|
| 31 |
+ entry.Data["level"] = entry.Level.String() |
|
| 32 |
+ |
|
| 33 |
+ // set type field |
|
| 34 |
+ if f.Type != "" {
|
|
| 35 |
+ v, ok = entry.Data["type"] |
|
| 36 |
+ if ok {
|
|
| 37 |
+ entry.Data["fields.type"] = v |
|
| 38 |
+ } |
|
| 39 |
+ entry.Data["type"] = f.Type |
|
| 40 |
+ } |
|
| 41 |
+ |
|
| 42 |
+ serialized, err := json.Marshal(entry.Data) |
|
| 43 |
+ if err != nil {
|
|
| 44 |
+ return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
|
|
| 45 |
+ } |
|
| 46 |
+ return append(serialized, '\n'), nil |
|
| 47 |
+} |
| 0 | 48 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,52 @@ |
| 0 |
+package logstash |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "github.com/Sirupsen/logrus" |
|
| 6 |
+ "github.com/stretchr/testify/assert" |
|
| 7 |
+ "testing" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+func TestLogstashFormatter(t *testing.T) {
|
|
| 11 |
+ assert := assert.New(t) |
|
| 12 |
+ |
|
| 13 |
+ lf := LogstashFormatter{Type: "abc"}
|
|
| 14 |
+ |
|
| 15 |
+ fields := logrus.Fields{
|
|
| 16 |
+ "message": "def", |
|
| 17 |
+ "level": "ijk", |
|
| 18 |
+ "type": "lmn", |
|
| 19 |
+ "one": 1, |
|
| 20 |
+ "pi": 3.14, |
|
| 21 |
+ "bool": true, |
|
| 22 |
+ } |
|
| 23 |
+ |
|
| 24 |
+ entry := logrus.WithFields(fields) |
|
| 25 |
+ entry.Message = "msg" |
|
| 26 |
+ entry.Level = logrus.InfoLevel |
|
| 27 |
+ |
|
| 28 |
+ b, _ := lf.Format(entry) |
|
| 29 |
+ |
|
| 30 |
+ var data map[string]interface{}
|
|
| 31 |
+ dec := json.NewDecoder(bytes.NewReader(b)) |
|
| 32 |
+ dec.UseNumber() |
|
| 33 |
+ dec.Decode(&data) |
|
| 34 |
+ |
|
| 35 |
+ // base fields |
|
| 36 |
+ assert.Equal(json.Number("1"), data["@version"])
|
|
| 37 |
+ assert.NotEmpty(data["@timestamp"]) |
|
| 38 |
+ assert.Equal("abc", data["type"])
|
|
| 39 |
+ assert.Equal("msg", data["message"])
|
|
| 40 |
+ assert.Equal("info", data["level"])
|
|
| 41 |
+ |
|
| 42 |
+ // substituted fields |
|
| 43 |
+ assert.Equal("def", data["fields.message"])
|
|
| 44 |
+ assert.Equal("ijk", data["fields.level"])
|
|
| 45 |
+ assert.Equal("lmn", data["fields.type"])
|
|
| 46 |
+ |
|
| 47 |
+ // formats |
|
| 48 |
+ assert.Equal(json.Number("1"), data["one"])
|
|
| 49 |
+ assert.Equal(json.Number("3.14"), data["pi"])
|
|
| 50 |
+ assert.Equal(true, data["bool"]) |
|
| 51 |
+} |
| ... | ... |
@@ -1,51 +1,51 @@ |
| 1 |
-package logrus_airbrake |
|
| 1 |
+package airbrake |
|
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "errors" |
|
| 5 |
+ "fmt" |
|
| 6 |
+ |
|
| 4 | 7 |
"github.com/Sirupsen/logrus" |
| 5 | 8 |
"github.com/tobi/airbrake-go" |
| 6 | 9 |
) |
| 7 | 10 |
|
| 8 | 11 |
// AirbrakeHook to send exceptions to an exception-tracking service compatible |
| 9 |
-// with the Airbrake API. You must set: |
|
| 10 |
-// * airbrake.Endpoint |
|
| 11 |
-// * airbrake.ApiKey |
|
| 12 |
-// * airbrake.Environment |
|
| 13 |
-// |
|
| 14 |
-// Before using this hook, to send an error. Entries that trigger an Error, |
|
| 15 |
-// Fatal or Panic should now include an "error" field to send to Airbrake. |
|
| 16 |
-type AirbrakeHook struct{}
|
|
| 17 |
- |
|
| 18 |
-func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error {
|
|
| 19 |
- if entry.Data["error"] == nil {
|
|
| 20 |
- entry.Logger.WithFields(logrus.Fields{
|
|
| 21 |
- "source": "airbrake", |
|
| 22 |
- "endpoint": airbrake.Endpoint, |
|
| 23 |
- }).Warn("Exceptions sent to Airbrake must have an 'error' key with the error")
|
|
| 24 |
- return nil |
|
| 12 |
+// with the Airbrake API. |
|
| 13 |
+type airbrakeHook struct {
|
|
| 14 |
+ APIKey string |
|
| 15 |
+ Endpoint string |
|
| 16 |
+ Environment string |
|
| 17 |
+} |
|
| 18 |
+ |
|
| 19 |
+func NewHook(endpoint, apiKey, env string) *airbrakeHook {
|
|
| 20 |
+ return &airbrakeHook{
|
|
| 21 |
+ APIKey: apiKey, |
|
| 22 |
+ Endpoint: endpoint, |
|
| 23 |
+ Environment: env, |
|
| 25 | 24 |
} |
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+func (hook *airbrakeHook) Fire(entry *logrus.Entry) error {
|
|
| 28 |
+ airbrake.ApiKey = hook.APIKey |
|
| 29 |
+ airbrake.Endpoint = hook.Endpoint |
|
| 30 |
+ airbrake.Environment = hook.Environment |
|
| 26 | 31 |
|
| 32 |
+ var notifyErr error |
|
| 27 | 33 |
err, ok := entry.Data["error"].(error) |
| 28 |
- if !ok {
|
|
| 29 |
- entry.Logger.WithFields(logrus.Fields{
|
|
| 30 |
- "source": "airbrake", |
|
| 31 |
- "endpoint": airbrake.Endpoint, |
|
| 32 |
- }).Warn("Exceptions sent to Airbrake must have an `error` key of type `error`")
|
|
| 33 |
- return nil |
|
| 34 |
+ if ok {
|
|
| 35 |
+ notifyErr = err |
|
| 36 |
+ } else {
|
|
| 37 |
+ notifyErr = errors.New(entry.Message) |
|
| 34 | 38 |
} |
| 35 | 39 |
|
| 36 |
- airErr := airbrake.Notify(err) |
|
| 40 |
+ airErr := airbrake.Notify(notifyErr) |
|
| 37 | 41 |
if airErr != nil {
|
| 38 |
- entry.Logger.WithFields(logrus.Fields{
|
|
| 39 |
- "source": "airbrake", |
|
| 40 |
- "endpoint": airbrake.Endpoint, |
|
| 41 |
- "error": airErr, |
|
| 42 |
- }).Warn("Failed to send error to Airbrake")
|
|
| 42 |
+ return fmt.Errorf("Failed to send error to Airbrake: %s", airErr)
|
|
| 43 | 43 |
} |
| 44 | 44 |
|
| 45 | 45 |
return nil |
| 46 | 46 |
} |
| 47 | 47 |
|
| 48 |
-func (hook *AirbrakeHook) Levels() []logrus.Level {
|
|
| 48 |
+func (hook *airbrakeHook) Levels() []logrus.Level {
|
|
| 49 | 49 |
return []logrus.Level{
|
| 50 | 50 |
logrus.ErrorLevel, |
| 51 | 51 |
logrus.FatalLevel, |
| 52 | 52 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,133 @@ |
| 0 |
+package airbrake |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/xml" |
|
| 4 |
+ "net/http" |
|
| 5 |
+ "net/http/httptest" |
|
| 6 |
+ "testing" |
|
| 7 |
+ "time" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/Sirupsen/logrus" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+type notice struct {
|
|
| 13 |
+ Error NoticeError `xml:"error"` |
|
| 14 |
+} |
|
| 15 |
+type NoticeError struct {
|
|
| 16 |
+ Class string `xml:"class"` |
|
| 17 |
+ Message string `xml:"message"` |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+type customErr struct {
|
|
| 21 |
+ msg string |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (e *customErr) Error() string {
|
|
| 25 |
+ return e.msg |
|
| 26 |
+} |
|
| 27 |
+ |
|
| 28 |
+const ( |
|
| 29 |
+ testAPIKey = "abcxyz" |
|
| 30 |
+ testEnv = "development" |
|
| 31 |
+ expectedClass = "*airbrake.customErr" |
|
| 32 |
+ expectedMsg = "foo" |
|
| 33 |
+ unintendedMsg = "Airbrake will not see this string" |
|
| 34 |
+) |
|
| 35 |
+ |
|
| 36 |
+var ( |
|
| 37 |
+ noticeError = make(chan NoticeError, 1) |
|
| 38 |
+) |
|
| 39 |
+ |
|
| 40 |
+// TestLogEntryMessageReceived checks if invoking Logrus' log.Error |
|
| 41 |
+// method causes an XML payload containing the log entry message is received |
|
| 42 |
+// by a HTTP server emulating an Airbrake-compatible endpoint. |
|
| 43 |
+func TestLogEntryMessageReceived(t *testing.T) {
|
|
| 44 |
+ log := logrus.New() |
|
| 45 |
+ ts := startAirbrakeServer(t) |
|
| 46 |
+ defer ts.Close() |
|
| 47 |
+ |
|
| 48 |
+ hook := NewHook(ts.URL, testAPIKey, "production") |
|
| 49 |
+ log.Hooks.Add(hook) |
|
| 50 |
+ |
|
| 51 |
+ log.Error(expectedMsg) |
|
| 52 |
+ |
|
| 53 |
+ select {
|
|
| 54 |
+ case received := <-noticeError: |
|
| 55 |
+ if received.Message != expectedMsg {
|
|
| 56 |
+ t.Errorf("Unexpected message received: %s", received.Message)
|
|
| 57 |
+ } |
|
| 58 |
+ case <-time.After(time.Second): |
|
| 59 |
+ t.Error("Timed out; no notice received by Airbrake API")
|
|
| 60 |
+ } |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+// TestLogEntryMessageReceived confirms that, when passing an error type using |
|
| 64 |
+// logrus.Fields, a HTTP server emulating an Airbrake endpoint receives the |
|
| 65 |
+// error message returned by the Error() method on the error interface |
|
| 66 |
+// rather than the logrus.Entry.Message string. |
|
| 67 |
+func TestLogEntryWithErrorReceived(t *testing.T) {
|
|
| 68 |
+ log := logrus.New() |
|
| 69 |
+ ts := startAirbrakeServer(t) |
|
| 70 |
+ defer ts.Close() |
|
| 71 |
+ |
|
| 72 |
+ hook := NewHook(ts.URL, testAPIKey, "production") |
|
| 73 |
+ log.Hooks.Add(hook) |
|
| 74 |
+ |
|
| 75 |
+ log.WithFields(logrus.Fields{
|
|
| 76 |
+ "error": &customErr{expectedMsg},
|
|
| 77 |
+ }).Error(unintendedMsg) |
|
| 78 |
+ |
|
| 79 |
+ select {
|
|
| 80 |
+ case received := <-noticeError: |
|
| 81 |
+ if received.Message != expectedMsg {
|
|
| 82 |
+ t.Errorf("Unexpected message received: %s", received.Message)
|
|
| 83 |
+ } |
|
| 84 |
+ if received.Class != expectedClass {
|
|
| 85 |
+ t.Errorf("Unexpected error class: %s", received.Class)
|
|
| 86 |
+ } |
|
| 87 |
+ case <-time.After(time.Second): |
|
| 88 |
+ t.Error("Timed out; no notice received by Airbrake API")
|
|
| 89 |
+ } |
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+// TestLogEntryWithNonErrorTypeNotReceived confirms that, when passing a |
|
| 93 |
+// non-error type using logrus.Fields, a HTTP server emulating an Airbrake |
|
| 94 |
+// endpoint receives the logrus.Entry.Message string. |
|
| 95 |
+// |
|
| 96 |
+// Only error types are supported when setting the 'error' field using |
|
| 97 |
+// logrus.WithFields(). |
|
| 98 |
+func TestLogEntryWithNonErrorTypeNotReceived(t *testing.T) {
|
|
| 99 |
+ log := logrus.New() |
|
| 100 |
+ ts := startAirbrakeServer(t) |
|
| 101 |
+ defer ts.Close() |
|
| 102 |
+ |
|
| 103 |
+ hook := NewHook(ts.URL, testAPIKey, "production") |
|
| 104 |
+ log.Hooks.Add(hook) |
|
| 105 |
+ |
|
| 106 |
+ log.WithFields(logrus.Fields{
|
|
| 107 |
+ "error": expectedMsg, |
|
| 108 |
+ }).Error(unintendedMsg) |
|
| 109 |
+ |
|
| 110 |
+ select {
|
|
| 111 |
+ case received := <-noticeError: |
|
| 112 |
+ if received.Message != unintendedMsg {
|
|
| 113 |
+ t.Errorf("Unexpected message received: %s", received.Message)
|
|
| 114 |
+ } |
|
| 115 |
+ case <-time.After(time.Second): |
|
| 116 |
+ t.Error("Timed out; no notice received by Airbrake API")
|
|
| 117 |
+ } |
|
| 118 |
+} |
|
| 119 |
+ |
|
| 120 |
+func startAirbrakeServer(t *testing.T) *httptest.Server {
|
|
| 121 |
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 122 |
+ var notice notice |
|
| 123 |
+ if err := xml.NewDecoder(r.Body).Decode(¬ice); err != nil {
|
|
| 124 |
+ t.Error(err) |
|
| 125 |
+ } |
|
| 126 |
+ r.Body.Close() |
|
| 127 |
+ |
|
| 128 |
+ noticeError <- notice.Error |
|
| 129 |
+ })) |
|
| 130 |
+ |
|
| 131 |
+ return ts |
|
| 132 |
+} |
| 0 | 133 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,68 @@ |
| 0 |
+package logrus_bugsnag |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "errors" |
|
| 4 |
+ |
|
| 5 |
+ "github.com/Sirupsen/logrus" |
|
| 6 |
+ "github.com/bugsnag/bugsnag-go" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+type bugsnagHook struct{}
|
|
| 10 |
+ |
|
| 11 |
+// ErrBugsnagUnconfigured is returned if NewBugsnagHook is called before |
|
| 12 |
+// bugsnag.Configure. Bugsnag must be configured before the hook. |
|
| 13 |
+var ErrBugsnagUnconfigured = errors.New("bugsnag must be configured before installing this logrus hook")
|
|
| 14 |
+ |
|
| 15 |
+// ErrBugsnagSendFailed indicates that the hook failed to submit an error to |
|
| 16 |
+// bugsnag. The error was successfully generated, but `bugsnag.Notify()` |
|
| 17 |
+// failed. |
|
| 18 |
+type ErrBugsnagSendFailed struct {
|
|
| 19 |
+ err error |
|
| 20 |
+} |
|
| 21 |
+ |
|
| 22 |
+func (e ErrBugsnagSendFailed) Error() string {
|
|
| 23 |
+ return "failed to send error to Bugsnag: " + e.err.Error() |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+// NewBugsnagHook initializes a logrus hook which sends exceptions to an |
|
| 27 |
+// exception-tracking service compatible with the Bugsnag API. Before using |
|
| 28 |
+// this hook, you must call bugsnag.Configure(). The returned object should be |
|
| 29 |
+// registered with a log via `AddHook()` |
|
| 30 |
+// |
|
| 31 |
+// Entries that trigger an Error, Fatal or Panic should now include an "error" |
|
| 32 |
+// field to send to Bugsnag. |
|
| 33 |
+func NewBugsnagHook() (*bugsnagHook, error) {
|
|
| 34 |
+ if bugsnag.Config.APIKey == "" {
|
|
| 35 |
+ return nil, ErrBugsnagUnconfigured |
|
| 36 |
+ } |
|
| 37 |
+ return &bugsnagHook{}, nil
|
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+// Fire forwards an error to Bugsnag. Given a logrus.Entry, it extracts the |
|
| 41 |
+// "error" field (or the Message if the error isn't present) and sends it off. |
|
| 42 |
+func (hook *bugsnagHook) Fire(entry *logrus.Entry) error {
|
|
| 43 |
+ var notifyErr error |
|
| 44 |
+ err, ok := entry.Data["error"].(error) |
|
| 45 |
+ if ok {
|
|
| 46 |
+ notifyErr = err |
|
| 47 |
+ } else {
|
|
| 48 |
+ notifyErr = errors.New(entry.Message) |
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ bugsnagErr := bugsnag.Notify(notifyErr) |
|
| 52 |
+ if bugsnagErr != nil {
|
|
| 53 |
+ return ErrBugsnagSendFailed{bugsnagErr}
|
|
| 54 |
+ } |
|
| 55 |
+ |
|
| 56 |
+ return nil |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+// Levels enumerates the log levels on which the error should be forwarded to |
|
| 60 |
+// bugsnag: everything at or above the "Error" level. |
|
| 61 |
+func (hook *bugsnagHook) Levels() []logrus.Level {
|
|
| 62 |
+ return []logrus.Level{
|
|
| 63 |
+ logrus.ErrorLevel, |
|
| 64 |
+ logrus.FatalLevel, |
|
| 65 |
+ logrus.PanicLevel, |
|
| 66 |
+ } |
|
| 67 |
+} |
| 0 | 68 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,64 @@ |
| 0 |
+package logrus_bugsnag |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "errors" |
|
| 5 |
+ "io/ioutil" |
|
| 6 |
+ "net/http" |
|
| 7 |
+ "net/http/httptest" |
|
| 8 |
+ "testing" |
|
| 9 |
+ "time" |
|
| 10 |
+ |
|
| 11 |
+ "github.com/Sirupsen/logrus" |
|
| 12 |
+ "github.com/bugsnag/bugsnag-go" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+type notice struct {
|
|
| 16 |
+ Events []struct {
|
|
| 17 |
+ Exceptions []struct {
|
|
| 18 |
+ Message string `json:"message"` |
|
| 19 |
+ } `json:"exceptions"` |
|
| 20 |
+ } `json:"events"` |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+func TestNoticeReceived(t *testing.T) {
|
|
| 24 |
+ msg := make(chan string, 1) |
|
| 25 |
+ expectedMsg := "foo" |
|
| 26 |
+ |
|
| 27 |
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
| 28 |
+ var notice notice |
|
| 29 |
+ data, _ := ioutil.ReadAll(r.Body) |
|
| 30 |
+ if err := json.Unmarshal(data, ¬ice); err != nil {
|
|
| 31 |
+ t.Error(err) |
|
| 32 |
+ } |
|
| 33 |
+ _ = r.Body.Close() |
|
| 34 |
+ |
|
| 35 |
+ msg <- notice.Events[0].Exceptions[0].Message |
|
| 36 |
+ })) |
|
| 37 |
+ defer ts.Close() |
|
| 38 |
+ |
|
| 39 |
+ hook := &bugsnagHook{}
|
|
| 40 |
+ |
|
| 41 |
+ bugsnag.Configure(bugsnag.Configuration{
|
|
| 42 |
+ Endpoint: ts.URL, |
|
| 43 |
+ ReleaseStage: "production", |
|
| 44 |
+ APIKey: "12345678901234567890123456789012", |
|
| 45 |
+ Synchronous: true, |
|
| 46 |
+ }) |
|
| 47 |
+ |
|
| 48 |
+ log := logrus.New() |
|
| 49 |
+ log.Hooks.Add(hook) |
|
| 50 |
+ |
|
| 51 |
+ log.WithFields(logrus.Fields{
|
|
| 52 |
+ "error": errors.New(expectedMsg), |
|
| 53 |
+ }).Error("Bugsnag will not see this string")
|
|
| 54 |
+ |
|
| 55 |
+ select {
|
|
| 56 |
+ case received := <-msg: |
|
| 57 |
+ if received != expectedMsg {
|
|
| 58 |
+ t.Errorf("Unexpected message received: %s", received)
|
|
| 59 |
+ } |
|
| 60 |
+ case <-time.After(time.Second): |
|
| 61 |
+ t.Error("Timed out; no notice received by Bugsnag API")
|
|
| 62 |
+ } |
|
| 63 |
+} |
| ... | ... |
@@ -11,11 +11,12 @@ type JSONFormatter struct{}
|
| 11 | 11 |
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
|
| 12 | 12 |
data := make(Fields, len(entry.Data)+3) |
| 13 | 13 |
for k, v := range entry.Data {
|
| 14 |
- // Otherwise errors are ignored by `encoding/json` |
|
| 15 |
- // https://github.com/Sirupsen/logrus/issues/137 |
|
| 16 |
- if err, ok := v.(error); ok {
|
|
| 17 |
- data[k] = err.Error() |
|
| 18 |
- } else {
|
|
| 14 |
+ switch v := v.(type) {
|
|
| 15 |
+ case error: |
|
| 16 |
+ // Otherwise errors are ignored by `encoding/json` |
|
| 17 |
+ // https://github.com/Sirupsen/logrus/issues/137 |
|
| 18 |
+ data[k] = v.Error() |
|
| 19 |
+ default: |
|
| 19 | 20 |
data[k] = v |
| 20 | 21 |
} |
| 21 | 22 |
} |
| ... | ... |
@@ -65,11 +65,15 @@ func (logger *Logger) WithFields(fields Fields) *Entry {
|
| 65 | 65 |
} |
| 66 | 66 |
|
| 67 | 67 |
func (logger *Logger) Debugf(format string, args ...interface{}) {
|
| 68 |
- NewEntry(logger).Debugf(format, args...) |
|
| 68 |
+ if logger.Level >= DebugLevel {
|
|
| 69 |
+ NewEntry(logger).Debugf(format, args...) |
|
| 70 |
+ } |
|
| 69 | 71 |
} |
| 70 | 72 |
|
| 71 | 73 |
func (logger *Logger) Infof(format string, args ...interface{}) {
|
| 72 |
- NewEntry(logger).Infof(format, args...) |
|
| 74 |
+ if logger.Level >= InfoLevel {
|
|
| 75 |
+ NewEntry(logger).Infof(format, args...) |
|
| 76 |
+ } |
|
| 73 | 77 |
} |
| 74 | 78 |
|
| 75 | 79 |
func (logger *Logger) Printf(format string, args ...interface{}) {
|
| ... | ... |
@@ -77,31 +81,45 @@ func (logger *Logger) Printf(format string, args ...interface{}) {
|
| 77 | 77 |
} |
| 78 | 78 |
|
| 79 | 79 |
func (logger *Logger) Warnf(format string, args ...interface{}) {
|
| 80 |
- NewEntry(logger).Warnf(format, args...) |
|
| 80 |
+ if logger.Level >= WarnLevel {
|
|
| 81 |
+ NewEntry(logger).Warnf(format, args...) |
|
| 82 |
+ } |
|
| 81 | 83 |
} |
| 82 | 84 |
|
| 83 | 85 |
func (logger *Logger) Warningf(format string, args ...interface{}) {
|
| 84 |
- NewEntry(logger).Warnf(format, args...) |
|
| 86 |
+ if logger.Level >= WarnLevel {
|
|
| 87 |
+ NewEntry(logger).Warnf(format, args...) |
|
| 88 |
+ } |
|
| 85 | 89 |
} |
| 86 | 90 |
|
| 87 | 91 |
func (logger *Logger) Errorf(format string, args ...interface{}) {
|
| 88 |
- NewEntry(logger).Errorf(format, args...) |
|
| 92 |
+ if logger.Level >= ErrorLevel {
|
|
| 93 |
+ NewEntry(logger).Errorf(format, args...) |
|
| 94 |
+ } |
|
| 89 | 95 |
} |
| 90 | 96 |
|
| 91 | 97 |
func (logger *Logger) Fatalf(format string, args ...interface{}) {
|
| 92 |
- NewEntry(logger).Fatalf(format, args...) |
|
| 98 |
+ if logger.Level >= FatalLevel {
|
|
| 99 |
+ NewEntry(logger).Fatalf(format, args...) |
|
| 100 |
+ } |
|
| 93 | 101 |
} |
| 94 | 102 |
|
| 95 | 103 |
func (logger *Logger) Panicf(format string, args ...interface{}) {
|
| 96 |
- NewEntry(logger).Panicf(format, args...) |
|
| 104 |
+ if logger.Level >= PanicLevel {
|
|
| 105 |
+ NewEntry(logger).Panicf(format, args...) |
|
| 106 |
+ } |
|
| 97 | 107 |
} |
| 98 | 108 |
|
| 99 | 109 |
func (logger *Logger) Debug(args ...interface{}) {
|
| 100 |
- NewEntry(logger).Debug(args...) |
|
| 110 |
+ if logger.Level >= DebugLevel {
|
|
| 111 |
+ NewEntry(logger).Debug(args...) |
|
| 112 |
+ } |
|
| 101 | 113 |
} |
| 102 | 114 |
|
| 103 | 115 |
func (logger *Logger) Info(args ...interface{}) {
|
| 104 |
- NewEntry(logger).Info(args...) |
|
| 116 |
+ if logger.Level >= InfoLevel {
|
|
| 117 |
+ NewEntry(logger).Info(args...) |
|
| 118 |
+ } |
|
| 105 | 119 |
} |
| 106 | 120 |
|
| 107 | 121 |
func (logger *Logger) Print(args ...interface{}) {
|
| ... | ... |
@@ -109,31 +127,45 @@ func (logger *Logger) Print(args ...interface{}) {
|
| 109 | 109 |
} |
| 110 | 110 |
|
| 111 | 111 |
func (logger *Logger) Warn(args ...interface{}) {
|
| 112 |
- NewEntry(logger).Warn(args...) |
|
| 112 |
+ if logger.Level >= WarnLevel {
|
|
| 113 |
+ NewEntry(logger).Warn(args...) |
|
| 114 |
+ } |
|
| 113 | 115 |
} |
| 114 | 116 |
|
| 115 | 117 |
func (logger *Logger) Warning(args ...interface{}) {
|
| 116 |
- NewEntry(logger).Warn(args...) |
|
| 118 |
+ if logger.Level >= WarnLevel {
|
|
| 119 |
+ NewEntry(logger).Warn(args...) |
|
| 120 |
+ } |
|
| 117 | 121 |
} |
| 118 | 122 |
|
| 119 | 123 |
func (logger *Logger) Error(args ...interface{}) {
|
| 120 |
- NewEntry(logger).Error(args...) |
|
| 124 |
+ if logger.Level >= ErrorLevel {
|
|
| 125 |
+ NewEntry(logger).Error(args...) |
|
| 126 |
+ } |
|
| 121 | 127 |
} |
| 122 | 128 |
|
| 123 | 129 |
func (logger *Logger) Fatal(args ...interface{}) {
|
| 124 |
- NewEntry(logger).Fatal(args...) |
|
| 130 |
+ if logger.Level >= FatalLevel {
|
|
| 131 |
+ NewEntry(logger).Fatal(args...) |
|
| 132 |
+ } |
|
| 125 | 133 |
} |
| 126 | 134 |
|
| 127 | 135 |
func (logger *Logger) Panic(args ...interface{}) {
|
| 128 |
- NewEntry(logger).Panic(args...) |
|
| 136 |
+ if logger.Level >= PanicLevel {
|
|
| 137 |
+ NewEntry(logger).Panic(args...) |
|
| 138 |
+ } |
|
| 129 | 139 |
} |
| 130 | 140 |
|
| 131 | 141 |
func (logger *Logger) Debugln(args ...interface{}) {
|
| 132 |
- NewEntry(logger).Debugln(args...) |
|
| 142 |
+ if logger.Level >= DebugLevel {
|
|
| 143 |
+ NewEntry(logger).Debugln(args...) |
|
| 144 |
+ } |
|
| 133 | 145 |
} |
| 134 | 146 |
|
| 135 | 147 |
func (logger *Logger) Infoln(args ...interface{}) {
|
| 136 |
- NewEntry(logger).Infoln(args...) |
|
| 148 |
+ if logger.Level >= InfoLevel {
|
|
| 149 |
+ NewEntry(logger).Infoln(args...) |
|
| 150 |
+ } |
|
| 137 | 151 |
} |
| 138 | 152 |
|
| 139 | 153 |
func (logger *Logger) Println(args ...interface{}) {
|
| ... | ... |
@@ -141,21 +173,31 @@ func (logger *Logger) Println(args ...interface{}) {
|
| 141 | 141 |
} |
| 142 | 142 |
|
| 143 | 143 |
func (logger *Logger) Warnln(args ...interface{}) {
|
| 144 |
- NewEntry(logger).Warnln(args...) |
|
| 144 |
+ if logger.Level >= WarnLevel {
|
|
| 145 |
+ NewEntry(logger).Warnln(args...) |
|
| 146 |
+ } |
|
| 145 | 147 |
} |
| 146 | 148 |
|
| 147 | 149 |
func (logger *Logger) Warningln(args ...interface{}) {
|
| 148 |
- NewEntry(logger).Warnln(args...) |
|
| 150 |
+ if logger.Level >= WarnLevel {
|
|
| 151 |
+ NewEntry(logger).Warnln(args...) |
|
| 152 |
+ } |
|
| 149 | 153 |
} |
| 150 | 154 |
|
| 151 | 155 |
func (logger *Logger) Errorln(args ...interface{}) {
|
| 152 |
- NewEntry(logger).Errorln(args...) |
|
| 156 |
+ if logger.Level >= ErrorLevel {
|
|
| 157 |
+ NewEntry(logger).Errorln(args...) |
|
| 158 |
+ } |
|
| 153 | 159 |
} |
| 154 | 160 |
|
| 155 | 161 |
func (logger *Logger) Fatalln(args ...interface{}) {
|
| 156 |
- NewEntry(logger).Fatalln(args...) |
|
| 162 |
+ if logger.Level >= FatalLevel {
|
|
| 163 |
+ NewEntry(logger).Fatalln(args...) |
|
| 164 |
+ } |
|
| 157 | 165 |
} |
| 158 | 166 |
|
| 159 | 167 |
func (logger *Logger) Panicln(args ...interface{}) {
|
| 160 |
- NewEntry(logger).Panicln(args...) |
|
| 168 |
+ if logger.Level >= PanicLevel {
|
|
| 169 |
+ NewEntry(logger).Panicln(args...) |
|
| 170 |
+ } |
|
| 161 | 171 |
} |