Continued logging work
This commit is contained in:
parent
27bd79febf
commit
7cc8c81bd8
28 changed files with 287 additions and 171 deletions
45
log/event.go
45
log/event.go
|
@ -15,74 +15,95 @@ const (
|
|||
errorField = "error"
|
||||
)
|
||||
|
||||
// Event represents a single log event
|
||||
type Event struct {
|
||||
Time int64 `json:"time"`
|
||||
Level Level `json:"level"`
|
||||
Message string `json:"message"`
|
||||
fields map[string]any
|
||||
Timestamp int64 `json:"time"`
|
||||
Level Level `json:"level"`
|
||||
Message string `json:"message"`
|
||||
fields Context
|
||||
}
|
||||
|
||||
// newEvent creates a new log event
|
||||
func newEvent() *Event {
|
||||
return &Event{
|
||||
Time: time.Now().UnixMilli(),
|
||||
fields: make(map[string]any),
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
fields: make(Context),
|
||||
}
|
||||
}
|
||||
|
||||
// Fatal logs the event as FATAL, and exits the program with exit code 1
|
||||
func (e *Event) Fatal(message string, v ...any) {
|
||||
e.Log(FatalLevel, message, v...)
|
||||
e.Field("exit_code", 1).Log(FatalLevel, message, v...)
|
||||
fmt.Fprintf(os.Stderr, fmt.Sprintf(message+"\n", v...)) // Always output error to stderr
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Error logs the event with log level error
|
||||
func (e *Event) Error(message string, v ...any) {
|
||||
e.Log(ErrorLevel, message, v...)
|
||||
}
|
||||
|
||||
// Warn logs the event with log level warn
|
||||
func (e *Event) Warn(message string, v ...any) {
|
||||
e.Log(WarnLevel, message, v...)
|
||||
}
|
||||
|
||||
// Info logs the event with log level info
|
||||
func (e *Event) Info(message string, v ...any) {
|
||||
e.Log(InfoLevel, message, v...)
|
||||
}
|
||||
|
||||
// Debug logs the event with log level debug
|
||||
func (e *Event) Debug(message string, v ...any) {
|
||||
e.Log(DebugLevel, message, v...)
|
||||
}
|
||||
|
||||
// Trace logs the event with log level trace
|
||||
func (e *Event) Trace(message string, v ...any) {
|
||||
e.Log(TraceLevel, message, v...)
|
||||
}
|
||||
|
||||
// Tag adds a "tag" field to the log event
|
||||
func (e *Event) Tag(tag string) *Event {
|
||||
e.fields[tagField] = tag
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) Err(err error) *Event {
|
||||
e.fields[errorField] = err
|
||||
// Time sets the time field
|
||||
func (e *Event) Time(time time.Time) *Event {
|
||||
e.Timestamp = time.UnixMilli()
|
||||
return e
|
||||
}
|
||||
|
||||
// Err adds an "error" field to the log event
|
||||
func (e *Event) Err(err error) *Event {
|
||||
e.fields[errorField] = err.Error()
|
||||
return e
|
||||
}
|
||||
|
||||
// Field adds a custom field and value to the log event
|
||||
func (e *Event) Field(key string, value any) *Event {
|
||||
e.fields[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) Fields(fields map[string]any) *Event {
|
||||
// Fields adds a map of fields to the log event
|
||||
func (e *Event) Fields(fields Context) *Event {
|
||||
for k, v := range fields {
|
||||
e.fields[k] = v
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) Context(contexts ...Contexter) *Event {
|
||||
// With adds the fields of the given Contexter structs to the log event by calling their With method
|
||||
func (e *Event) With(contexts ...Contexter) *Event {
|
||||
for _, c := range contexts {
|
||||
e.Fields(c.Context())
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Log logs a message with the given log level
|
||||
func (e *Event) Log(l Level, message string, v ...any) {
|
||||
e.Message = fmt.Sprintf(message, v...)
|
||||
e.Level = l
|
||||
|
@ -110,6 +131,7 @@ func (e *Event) IsDebug() bool {
|
|||
return e.Loggable(DebugLevel)
|
||||
}
|
||||
|
||||
// JSON returns the event as a JSON representation
|
||||
func (e *Event) JSON() string {
|
||||
b, _ := json.Marshal(e)
|
||||
s := string(b)
|
||||
|
@ -120,6 +142,7 @@ func (e *Event) JSON() string {
|
|||
return s
|
||||
}
|
||||
|
||||
// String returns the event as a string
|
||||
func (e *Event) String() string {
|
||||
if len(e.fields) == 0 {
|
||||
return fmt.Sprintf("%s %s", e.Level.String(), e.Message)
|
||||
|
|
64
log/log.go
64
log/log.go
|
@ -1,17 +1,30 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultLevel = InfoLevel
|
||||
DefaultFormat = TextFormat
|
||||
)
|
||||
|
||||
var (
|
||||
level = InfoLevel
|
||||
format = TextFormat
|
||||
level = DefaultLevel
|
||||
format = DefaultFormat
|
||||
overrides = make(map[string]*levelOverride)
|
||||
mu = &sync.Mutex{}
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultOutput = os.Stderr
|
||||
output io.Writer = DefaultOutput
|
||||
)
|
||||
|
||||
// Fatal prints the given message, and exits the program
|
||||
func Fatal(message string, v ...any) {
|
||||
newEvent().Fatal(message, v...)
|
||||
|
@ -42,22 +55,31 @@ func Trace(message string, v ...any) {
|
|||
newEvent().Trace(message, v...)
|
||||
}
|
||||
|
||||
func Context(contexts ...Contexter) *Event {
|
||||
return newEvent().Context(contexts...)
|
||||
// With creates a new log event and adds the fields of the given Contexter structs
|
||||
func With(contexts ...Contexter) *Event {
|
||||
return newEvent().With(contexts...)
|
||||
}
|
||||
|
||||
// Field creates a new log event and adds a custom field and value to it
|
||||
func Field(key string, value any) *Event {
|
||||
return newEvent().Field(key, value)
|
||||
}
|
||||
|
||||
func Fields(fields map[string]any) *Event {
|
||||
// Fields creates a new log event and adds a map of fields to it
|
||||
func Fields(fields Context) *Event {
|
||||
return newEvent().Fields(fields)
|
||||
}
|
||||
|
||||
// Tag creates a new log event and adds a "tag" field to it
|
||||
func Tag(tag string) *Event {
|
||||
return newEvent().Tag(tag)
|
||||
}
|
||||
|
||||
// Time creates a new log event and sets the time field
|
||||
func Time(time time.Time) *Event {
|
||||
return newEvent().Time(time)
|
||||
}
|
||||
|
||||
// CurrentLevel returns the current log level
|
||||
func CurrentLevel() Level {
|
||||
mu.Lock()
|
||||
|
@ -79,8 +101,8 @@ func SetLevelOverride(field string, value any, level Level) {
|
|||
overrides[field] = &levelOverride{value: value, level: level}
|
||||
}
|
||||
|
||||
// ResetLevelOverride removes all log level overrides
|
||||
func ResetLevelOverride() {
|
||||
// ResetLevelOverrides removes all log level overrides
|
||||
func ResetLevelOverrides() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
overrides = make(map[string]*levelOverride)
|
||||
|
@ -103,6 +125,34 @@ func SetFormat(newFormat Format) {
|
|||
}
|
||||
}
|
||||
|
||||
// SetOutput sets the log output writer
|
||||
func SetOutput(w io.Writer) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
log.SetOutput(w)
|
||||
output = w
|
||||
}
|
||||
|
||||
// File returns the log file, if any, or an empty string otherwise
|
||||
func File() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if f, ok := output.(*os.File); ok {
|
||||
return f.Name()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsFile returns true if the output is a non-default file
|
||||
func IsFile() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if _, ok := output.(*os.File); ok && output != DefaultOutput {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DisableDates disables the date/time prefix
|
||||
func DisableDates() {
|
||||
log.SetFlags(0)
|
||||
|
|
|
@ -1,57 +1,74 @@
|
|||
package log_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/log"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const tagPay = "PAY"
|
||||
func TestMain(m *testing.M) {
|
||||
exitCode := m.Run()
|
||||
resetState()
|
||||
log.SetLevel(log.ErrorLevel) // For other modules!
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
type visitor struct {
|
||||
func TestLog_TagContextFieldFields(t *testing.T) {
|
||||
t.Cleanup(resetState)
|
||||
v := &fakeVisitor{
|
||||
UserID: "u_abc",
|
||||
IP: "1.2.3.4",
|
||||
}
|
||||
var out bytes.Buffer
|
||||
log.SetOutput(&out)
|
||||
log.SetFormat(log.JSONFormat)
|
||||
log.SetLevelOverride("tag", "stripe", log.DebugLevel)
|
||||
|
||||
log.
|
||||
Tag("mytag").
|
||||
Field("field2", 123).
|
||||
Field("field1", "value1").
|
||||
Time(time.Unix(123, 0)).
|
||||
Info("hi there %s", "phil")
|
||||
log.
|
||||
Tag("not-stripe").
|
||||
Debug("this message will not appear")
|
||||
log.
|
||||
With(v).
|
||||
Fields(log.Context{
|
||||
"stripe_customer_id": "acct_123",
|
||||
"stripe_subscription_id": "sub_123",
|
||||
}).
|
||||
Tag("stripe").
|
||||
Err(http.ErrHandlerTimeout).
|
||||
Time(time.Unix(456, 0)).
|
||||
Debug("Subscription status %s", "active")
|
||||
|
||||
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"}
|
||||
`
|
||||
require.Equal(t, expected, out.String())
|
||||
}
|
||||
|
||||
type fakeVisitor struct {
|
||||
UserID string
|
||||
IP string
|
||||
}
|
||||
|
||||
func (v *visitor) Context() map[string]any {
|
||||
func (v *fakeVisitor) Context() log.Context {
|
||||
return map[string]any{
|
||||
"user_id": v.UserID,
|
||||
"ip": v.IP,
|
||||
"user_id": v.UserID,
|
||||
"visitor_ip": v.IP,
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvent_Info(t *testing.T) {
|
||||
/*
|
||||
log-level: INFO, user_id:u_abc=DEBUG
|
||||
log-level-overrides:
|
||||
- user_id=u_abc: DEBUG
|
||||
log-filter =
|
||||
|
||||
*/
|
||||
v := &visitor{
|
||||
UserID: "u_abc",
|
||||
IP: "1.2.3.4",
|
||||
}
|
||||
stripeCtx := log.NewCtx(map[string]any{
|
||||
"tag": "pay",
|
||||
})
|
||||
log.SetLevel(log.InfoLevel)
|
||||
//log.SetFormat(log.JSONFormat)
|
||||
//log.SetLevelOverride("user_id", "u_abc", log.DebugLevel)
|
||||
log.SetLevelOverride("tag", "pay", log.DebugLevel)
|
||||
mlog := log.Field("tag", "manager")
|
||||
mlog.Field("one", 1).Info("this is one")
|
||||
mlog.Err(http.ErrHandlerTimeout).Field("two", 2).Info("this is two")
|
||||
log.Info("somebody did something")
|
||||
log.
|
||||
Context(stripeCtx, v).
|
||||
Fields(map[string]any{
|
||||
"tier": "ti_abc",
|
||||
"user_id": "u_abc",
|
||||
}).
|
||||
Debug("Somebody paid something for $%d", 10)
|
||||
log.
|
||||
Field("tag", "account").
|
||||
Field("user_id", "u_abc").
|
||||
Debug("User logged in")
|
||||
func resetState() {
|
||||
log.SetLevel(log.DefaultLevel)
|
||||
log.SetFormat(log.DefaultFormat)
|
||||
log.SetOutput(log.DefaultOutput)
|
||||
log.ResetLevelOverrides()
|
||||
}
|
||||
|
|
15
log/types.go
15
log/types.go
|
@ -91,19 +91,14 @@ func ToFormat(s string) Format {
|
|||
}
|
||||
}
|
||||
|
||||
// Contexter allows structs to export a key-value pairs in the form of a Context
|
||||
type Contexter interface {
|
||||
Context() map[string]any
|
||||
// Context returns the object context as key-value pairs
|
||||
Context() Context
|
||||
}
|
||||
|
||||
type fieldsCtx map[string]any
|
||||
|
||||
func (f fieldsCtx) Context() map[string]any {
|
||||
return f
|
||||
}
|
||||
|
||||
func NewCtx(fields map[string]any) Contexter {
|
||||
return fieldsCtx(fields)
|
||||
}
|
||||
// Context represents an object's state in the form of key-value pairs
|
||||
type Context map[string]any
|
||||
|
||||
type levelOverride struct {
|
||||
value any
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue