mirror of
https://github.com/binwiederhier/ntfy.git
synced 2025-07-20 10:04:08 +00:00
WIP: Logging
This commit is contained in:
parent
af4175a5bc
commit
a6641980c2
15 changed files with 631 additions and 168 deletions
150
log/event.go
Normal file
150
log/event.go
Normal file
|
@ -0,0 +1,150 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
tagField = "tag"
|
||||
errorField = "error"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Time int64 `json:"time"`
|
||||
Level Level `json:"level"`
|
||||
Message string `json:"message"`
|
||||
fields map[string]any
|
||||
}
|
||||
|
||||
func newEvent() *Event {
|
||||
return &Event{
|
||||
Time: time.Now().UnixMilli(),
|
||||
fields: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Event) Fatal(message string, v ...any) {
|
||||
e.Log(FatalLevel, message, v...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (e *Event) Error(message string, v ...any) {
|
||||
e.Log(ErrorLevel, message, v...)
|
||||
}
|
||||
|
||||
func (e *Event) Warn(message string, v ...any) {
|
||||
e.Log(WarnLevel, message, v...)
|
||||
}
|
||||
|
||||
func (e *Event) Info(message string, v ...any) {
|
||||
e.Log(InfoLevel, message, v...)
|
||||
}
|
||||
|
||||
func (e *Event) Debug(message string, v ...any) {
|
||||
e.Log(DebugLevel, message, v...)
|
||||
}
|
||||
|
||||
func (e *Event) Trace(message string, v ...any) {
|
||||
e.Log(TraceLevel, message, v...)
|
||||
}
|
||||
|
||||
func (e *Event) Tag(tag string) *Event {
|
||||
e.fields[tagField] = tag
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) Err(err error) *Event {
|
||||
e.fields[errorField] = err
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) Field(key string, value any) *Event {
|
||||
e.fields[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) Fields(fields map[string]any) *Event {
|
||||
for k, v := range fields {
|
||||
e.fields[k] = v
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) Context(contexts ...Ctx) *Event {
|
||||
for _, c := range contexts {
|
||||
e.Fields(c.Context())
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) Log(l Level, message string, v ...any) {
|
||||
e.Message = fmt.Sprintf(message, v...)
|
||||
e.Level = l
|
||||
if e.shouldPrint() {
|
||||
if CurrentFormat() == JSONFormat {
|
||||
log.Println(e.JSON())
|
||||
} else {
|
||||
log.Println(e.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loggable returns true if the given log level is lower or equal to the current log level
|
||||
func (e *Event) Loggable(l Level) bool {
|
||||
return e.globalLevelWithOverride() <= l
|
||||
}
|
||||
|
||||
// IsTrace returns true if the current log level is TraceLevel
|
||||
func (e *Event) IsTrace() bool {
|
||||
return e.Loggable(TraceLevel)
|
||||
}
|
||||
|
||||
// IsDebug returns true if the current log level is DebugLevel or below
|
||||
func (e *Event) IsDebug() bool {
|
||||
return e.Loggable(DebugLevel)
|
||||
}
|
||||
|
||||
func (e *Event) JSON() string {
|
||||
b, _ := json.Marshal(e)
|
||||
s := string(b)
|
||||
if len(e.fields) > 0 {
|
||||
b, _ := json.Marshal(e.fields)
|
||||
s = fmt.Sprintf("{%s,%s}", s[1:len(s)-1], string(b[1:len(b)-1]))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (e *Event) String() string {
|
||||
if len(e.fields) == 0 {
|
||||
return fmt.Sprintf("%s %s", e.Level.String(), e.Message)
|
||||
}
|
||||
fields := make([]string, 0)
|
||||
for k, v := range e.fields {
|
||||
fields = append(fields, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
sort.Strings(fields)
|
||||
return fmt.Sprintf("%s %s (%s)", e.Level.String(), e.Message, strings.Join(fields, ", "))
|
||||
}
|
||||
|
||||
func (e *Event) shouldPrint() bool {
|
||||
return e.globalLevelWithOverride() <= e.Level
|
||||
}
|
||||
|
||||
func (e *Event) globalLevelWithOverride() Level {
|
||||
mu.Lock()
|
||||
l, ov := level, overrides
|
||||
mu.Unlock()
|
||||
for field, override := range ov {
|
||||
value, exists := e.fields[field]
|
||||
if exists && value == override.value {
|
||||
return override.level
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
151
log/log.go
151
log/log.go
|
@ -2,71 +2,60 @@ package log
|
|||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Level is a well-known log level, as defined below
|
||||
type Level int
|
||||
|
||||
// Well known log levels
|
||||
const (
|
||||
TraceLevel Level = iota
|
||||
DebugLevel
|
||||
InfoLevel
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
)
|
||||
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case TraceLevel:
|
||||
return "TRACE"
|
||||
case DebugLevel:
|
||||
return "DEBUG"
|
||||
case InfoLevel:
|
||||
return "INFO"
|
||||
case WarnLevel:
|
||||
return "WARN"
|
||||
case ErrorLevel:
|
||||
return "ERROR"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
var (
|
||||
level = InfoLevel
|
||||
mu = &sync.Mutex{}
|
||||
level = InfoLevel
|
||||
format = TextFormat
|
||||
overrides = make(map[string]*levelOverride)
|
||||
mu = &sync.Mutex{}
|
||||
)
|
||||
|
||||
// Trace prints the given message, if the current log level is TRACE
|
||||
func Trace(message string, v ...any) {
|
||||
logIf(TraceLevel, message, v...)
|
||||
}
|
||||
|
||||
// Debug prints the given message, if the current log level is DEBUG or lower
|
||||
func Debug(message string, v ...any) {
|
||||
logIf(DebugLevel, message, v...)
|
||||
}
|
||||
|
||||
// Info prints the given message, if the current log level is INFO or lower
|
||||
func Info(message string, v ...any) {
|
||||
logIf(InfoLevel, message, v...)
|
||||
}
|
||||
|
||||
// Warn prints the given message, if the current log level is WARN or lower
|
||||
func Warn(message string, v ...any) {
|
||||
logIf(WarnLevel, message, v...)
|
||||
// Fatal prints the given message, and exits the program
|
||||
func Fatal(message string, v ...any) {
|
||||
newEvent().Fatal(message, v...)
|
||||
}
|
||||
|
||||
// Error prints the given message, if the current log level is ERROR or lower
|
||||
func Error(message string, v ...any) {
|
||||
logIf(ErrorLevel, message, v...)
|
||||
newEvent().Error(message, v...)
|
||||
}
|
||||
|
||||
// Fatal prints the given message, and exits the program
|
||||
func Fatal(v ...any) {
|
||||
log.Fatalln(v...)
|
||||
// Warn prints the given message, if the current log level is WARN or lower
|
||||
func Warn(message string, v ...any) {
|
||||
newEvent().Warn(message, v...)
|
||||
}
|
||||
|
||||
// Info prints the given message, if the current log level is INFO or lower
|
||||
func Info(message string, v ...any) {
|
||||
newEvent().Info(message, v...)
|
||||
}
|
||||
|
||||
// Debug prints the given message, if the current log level is DEBUG or lower
|
||||
func Debug(message string, v ...any) {
|
||||
newEvent().Debug(message, v...)
|
||||
}
|
||||
|
||||
// Trace prints the given message, if the current log level is TRACE
|
||||
func Trace(message string, v ...any) {
|
||||
newEvent().Trace(message, v...)
|
||||
}
|
||||
|
||||
func Context(contexts ...Ctx) *Event {
|
||||
return newEvent().Context(contexts...)
|
||||
}
|
||||
|
||||
func Field(key string, value any) *Event {
|
||||
return newEvent().Field(key, value)
|
||||
}
|
||||
|
||||
func Fields(fields map[string]any) *Event {
|
||||
return newEvent().Fields(fields)
|
||||
}
|
||||
|
||||
func Tag(tag string) *Event {
|
||||
return newEvent().Tag(tag)
|
||||
}
|
||||
|
||||
// CurrentLevel returns the current log level
|
||||
|
@ -83,30 +72,42 @@ func SetLevel(newLevel Level) {
|
|||
level = newLevel
|
||||
}
|
||||
|
||||
// SetLevelOverride adds a log override for the given field
|
||||
func SetLevelOverride(field string, value any, level Level) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
overrides[field] = &levelOverride{value: value, level: level}
|
||||
}
|
||||
|
||||
// ResetLevelOverride removes all log level overrides
|
||||
func ResetLevelOverride() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
overrides = make(map[string]*levelOverride)
|
||||
}
|
||||
|
||||
// CurrentFormat returns the current log formt
|
||||
func CurrentFormat() Format {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return format
|
||||
}
|
||||
|
||||
// SetFormat sets a new log format
|
||||
func SetFormat(newFormat Format) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
format = newFormat
|
||||
if newFormat == JSONFormat {
|
||||
DisableDates()
|
||||
}
|
||||
}
|
||||
|
||||
// DisableDates disables the date/time prefix
|
||||
func DisableDates() {
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
// ToLevel converts a string to a Level. It returns InfoLevel if the string
|
||||
// does not match any known log levels.
|
||||
func ToLevel(s string) Level {
|
||||
switch strings.ToUpper(s) {
|
||||
case "TRACE":
|
||||
return TraceLevel
|
||||
case "DEBUG":
|
||||
return DebugLevel
|
||||
case "INFO":
|
||||
return InfoLevel
|
||||
case "WARN", "WARNING":
|
||||
return WarnLevel
|
||||
case "ERROR":
|
||||
return ErrorLevel
|
||||
default:
|
||||
return InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
// Loggable returns true if the given log level is lower or equal to the current log level
|
||||
func Loggable(l Level) bool {
|
||||
return CurrentLevel() <= l
|
||||
|
@ -121,9 +122,3 @@ func IsTrace() bool {
|
|||
func IsDebug() bool {
|
||||
return Loggable(DebugLevel)
|
||||
}
|
||||
|
||||
func logIf(l Level, message string, v ...any) {
|
||||
if CurrentLevel() <= l {
|
||||
log.Printf(l.String()+" "+message, v...)
|
||||
}
|
||||
}
|
||||
|
|
57
log/log_test.go
Normal file
57
log/log_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package log_test
|
||||
|
||||
import (
|
||||
"heckel.io/ntfy/log"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const tagPay = "PAY"
|
||||
|
||||
type visitor struct {
|
||||
UserID string
|
||||
IP string
|
||||
}
|
||||
|
||||
func (v *visitor) Context() map[string]any {
|
||||
return map[string]any{
|
||||
"user_id": v.UserID,
|
||||
"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")
|
||||
}
|
111
log/types.go
Normal file
111
log/types.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Level is a well-known log level, as defined below
|
||||
type Level int
|
||||
|
||||
// Well known log levels
|
||||
const (
|
||||
TraceLevel Level = iota
|
||||
DebugLevel
|
||||
InfoLevel
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
FatalLevel
|
||||
)
|
||||
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case TraceLevel:
|
||||
return "TRACE"
|
||||
case DebugLevel:
|
||||
return "DEBUG"
|
||||
case InfoLevel:
|
||||
return "INFO"
|
||||
case WarnLevel:
|
||||
return "WARN"
|
||||
case ErrorLevel:
|
||||
return "ERROR"
|
||||
case FatalLevel:
|
||||
return "FATAL"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (l Level) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(l.String())
|
||||
}
|
||||
|
||||
// ToLevel converts a string to a Level. It returns InfoLevel if the string
|
||||
// does not match any known log levels.
|
||||
func ToLevel(s string) Level {
|
||||
switch strings.ToUpper(s) {
|
||||
case "TRACE":
|
||||
return TraceLevel
|
||||
case "DEBUG":
|
||||
return DebugLevel
|
||||
case "INFO":
|
||||
return InfoLevel
|
||||
case "WARN", "WARNING":
|
||||
return WarnLevel
|
||||
case "ERROR":
|
||||
return ErrorLevel
|
||||
default:
|
||||
return InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
// Format is a well-known log format
|
||||
type Format int
|
||||
|
||||
// Log formats
|
||||
const (
|
||||
TextFormat Format = iota
|
||||
JSONFormat
|
||||
)
|
||||
|
||||
func (f Format) String() string {
|
||||
switch f {
|
||||
case TextFormat:
|
||||
return "text"
|
||||
case JSONFormat:
|
||||
return "json"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// ToFormat converts a string to a Format. It returns TextFormat if the string
|
||||
// does not match any known log formats.
|
||||
func ToFormat(s string) Format {
|
||||
switch strings.ToLower(s) {
|
||||
case "text":
|
||||
return TextFormat
|
||||
case "json":
|
||||
return JSONFormat
|
||||
default:
|
||||
return TextFormat
|
||||
}
|
||||
}
|
||||
|
||||
type Ctx interface {
|
||||
Context() map[string]any
|
||||
}
|
||||
|
||||
type fieldsCtx map[string]any
|
||||
|
||||
func (f fieldsCtx) Context() map[string]any {
|
||||
return f
|
||||
}
|
||||
|
||||
func NewCtx(fields map[string]any) Ctx {
|
||||
return fieldsCtx(fields)
|
||||
}
|
||||
|
||||
type levelOverride struct {
|
||||
value any
|
||||
level Level
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue