Error logging

This commit is contained in:
binwiederhier 2023-02-06 16:01:32 -05:00
parent b5158adb51
commit 9b54f63eb1
5 changed files with 60 additions and 22 deletions

View file

@ -77,7 +77,11 @@ func (e *Event) Time(time time.Time) *Event {
// Err adds an "error" field to the log event // Err adds an "error" field to the log event
func (e *Event) Err(err error) *Event { func (e *Event) Err(err error) *Event {
e.fields[errorField] = err.Error() if c, ok := err.(Contexter); ok {
e.Fields(c.Context())
} else {
e.fields[errorField] = err.Error()
}
return e return e
} }

View file

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"heckel.io/ntfy/log" "heckel.io/ntfy/log"
"net/http"
"os" "os"
"testing" "testing"
"time" "time"
@ -23,6 +22,10 @@ func TestLog_TagContextFieldFields(t *testing.T) {
UserID: "u_abc", UserID: "u_abc",
IP: "1.2.3.4", IP: "1.2.3.4",
} }
err := &fakeError{
Code: 123,
Message: "some error",
}
var out bytes.Buffer var out bytes.Buffer
log.SetOutput(&out) log.SetOutput(&out)
log.SetFormat(log.JSONFormat) log.SetFormat(log.JSONFormat)
@ -44,23 +47,39 @@ func TestLog_TagContextFieldFields(t *testing.T) {
"stripe_subscription_id": "sub_123", "stripe_subscription_id": "sub_123",
}). }).
Tag("stripe"). Tag("stripe").
Err(http.ErrHandlerTimeout). Err(err).
Time(time.Unix(456, 0)). Time(time.Unix(456, 0)).
Debug("Subscription status %s", "active") Debug("Subscription status %s", "active")
expected := `{"time":123000,"level":"INFO","message":"hi there phil","field1":"value1","field2":123,"tag":"mytag"} expected := `{"time":123000,"level":"INFO","message":"hi there phil","field1":"value1","field2":123,"tag":"mytag"}
{"time":456000,"level":"DEBUG","message":"Subscription status active","error":"http: Handler timeout","stripe_customer_id":"acct_123","stripe_subscription_id":"sub_123","tag":"stripe","user_id":"u_abc","visitor_ip":"1.2.3.4"} {"time":456000,"level":"DEBUG","message":"Subscription status active","error":"some error","error_code":123,"stripe_customer_id":"acct_123","stripe_subscription_id":"sub_123","tag":"stripe","user_id":"u_abc","visitor_ip":"1.2.3.4"}
` `
require.Equal(t, expected, out.String()) require.Equal(t, expected, out.String())
} }
type fakeError struct {
Code int
Message string
}
func (e fakeError) Error() string {
return e.Message
}
func (e fakeError) Context() log.Context {
return log.Context{
"error": e.Message,
"error_code": e.Code,
}
}
type fakeVisitor struct { type fakeVisitor struct {
UserID string UserID string
IP string IP string
} }
func (v *fakeVisitor) Context() log.Context { func (v *fakeVisitor) Context() log.Context {
return map[string]any{ return log.Context{
"user_id": v.UserID, "user_id": v.UserID,
"visitor_ip": v.IP, "visitor_ip": v.IP,
} }

View file

@ -3,6 +3,7 @@ package server
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"heckel.io/ntfy/log"
"net/http" "net/http"
) )
@ -23,6 +24,14 @@ func (e errHTTP) JSON() string {
return string(b) return string(b)
} }
func (e errHTTP) Context() log.Context {
return log.Context{
"error": e.Message,
"error_code": e.Code,
"http_status": e.HTTPCode,
}
}
func wrapErrHTTP(err *errHTTP, message string, args ...any) *errHTTP { func wrapErrHTTP(err *errHTTP, message string, args ...any) *errHTTP {
return &errHTTP{ return &errHTTP{
Code: err.Code, Code: err.Code,

View file

@ -3,6 +3,7 @@ package server
import ( import (
"fmt" "fmt"
"github.com/emersion/go-smtp" "github.com/emersion/go-smtp"
"github.com/gorilla/websocket"
"golang.org/x/time/rate" "golang.org/x/time/rate"
"heckel.io/ntfy/log" "heckel.io/ntfy/log"
"heckel.io/ntfy/util" "heckel.io/ntfy/util"
@ -13,7 +14,7 @@ import (
// logr creates a new log event with HTTP request fields // logr creates a new log event with HTTP request fields
func logr(r *http.Request) *log.Event { func logr(r *http.Request) *log.Event {
return log.Fields(httpFields(r)) return log.Fields(httpContext(r))
} }
// logr creates a new log event with visitor fields // logr creates a new log event with visitor fields
@ -23,7 +24,7 @@ func logv(v *visitor) *log.Event {
// logr creates a new log event with HTTP request and visitor fields // logr creates a new log event with HTTP request and visitor fields
func logvr(v *visitor, r *http.Request) *log.Event { func logvr(v *visitor, r *http.Request) *log.Event {
return logv(v).Fields(httpFields(r)) return logv(v).Fields(httpContext(r))
} }
// logvrm creates a new log event with HTTP request, visitor fields and message fields // logvrm creates a new log event with HTTP request, visitor fields and message fields
@ -46,17 +47,30 @@ func logem(state *smtp.ConnectionState) *log.Event {
}) })
} }
func httpFields(r *http.Request) map[string]any { func httpContext(r *http.Request) log.Context {
requestURI := r.RequestURI requestURI := r.RequestURI
if requestURI == "" { if requestURI == "" {
requestURI = r.URL.Path requestURI = r.URL.Path
} }
return map[string]any{ return log.Context{
"http_method": r.Method, "http_method": r.Method,
"http_path": requestURI, "http_path": requestURI,
} }
} }
func websocketErrorContext(err error) log.Context {
if c, ok := err.(*websocket.CloseError); ok {
return log.Context{
"error": c.Error(),
"error_code": c.Code,
"error_type": "websocket.CloseError",
}
}
return log.Context{
"error": err.Error(),
}
}
func requestLimiterFields(limiter *rate.Limiter) map[string]any { func requestLimiterFields(limiter *rate.Limiter) map[string]any {
return map[string]any{ return map[string]any{
"visitor_request_limiter_limit": limiter.Limit(), "visitor_request_limiter_limit": limiter.Limit(),

View file

@ -334,9 +334,9 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
if websocket.IsWebSocketUpgrade(r) { if websocket.IsWebSocketUpgrade(r) {
isNormalError := strings.Contains(err.Error(), "i/o timeout") isNormalError := strings.Contains(err.Error(), "i/o timeout")
if isNormalError { if isNormalError {
logvr(v, r).Tag(tagWebsocket).Err(err).Debug("WebSocket error (this error is okay, it happens a lot): %s", err.Error()) logvr(v, r).Tag(tagWebsocket).Err(err).Fields(websocketErrorContext(err)).Debug("WebSocket error (this error is okay, it happens a lot): %s", err.Error())
} else { } else {
logvr(v, r).Tag(tagWebsocket).Err(err).Info("WebSocket error: %s", err.Error()) logvr(v, r).Tag(tagWebsocket).Err(err).Fields(websocketErrorContext(err)).Info("WebSocket error: %s", err.Error())
} }
return // Do not attempt to write to upgraded connection return // Do not attempt to write to upgraded connection
} }
@ -351,19 +351,11 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
isNormalError := httpErr.HTTPCode == http.StatusNotFound || httpErr.HTTPCode == http.StatusBadRequest isNormalError := httpErr.HTTPCode == http.StatusNotFound || httpErr.HTTPCode == http.StatusBadRequest
if isNormalError { if isNormalError {
logvr(v, r). logvr(v, r).
Fields(log.Context{ Err(httpErr).
"error_code": httpErr.Code,
"http_status": httpErr.HTTPCode,
}).
Err(err).
Debug("Connection closed with HTTP %d (ntfy error %d): %s", httpErr.HTTPCode, httpErr.Code, err.Error()) Debug("Connection closed with HTTP %d (ntfy error %d): %s", httpErr.HTTPCode, httpErr.Code, err.Error())
} else { } else {
logvr(v, r). logvr(v, r).
Fields(log.Context{ Err(httpErr).
"error_code": httpErr.Code,
"http_status": httpErr.HTTPCode,
}).
Err(err).
Info("Connection closed with HTTP %d (ntfy error %d): %s", httpErr.HTTPCode, httpErr.Code, err.Error()) Info("Connection closed with HTTP %d (ntfy error %d): %s", httpErr.HTTPCode, httpErr.Code, err.Error())
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -1161,7 +1153,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
} }
err = g.Wait() err = g.Wait()
if err != nil && websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { if err != nil && websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
logvr(v, r).Tag(tagWebsocket).Err(err).Trace("WebSocket connection closed") logvr(v, r).Tag(tagWebsocket).Err(err).Fields(websocketErrorContext(err)).Trace("WebSocket connection closed")
return nil // Normal closures are not errors; note: "1006 (abnormal closure)" is treated as normal, because people disconnect a lot return nil // Normal closures are not errors; note: "1006 (abnormal closure)" is treated as normal, because people disconnect a lot
} }
return err return err