WIP: Logging

This commit is contained in:
binwiederhier 2023-02-03 22:21:50 -05:00
parent af4175a5bc
commit a6641980c2
15 changed files with 631 additions and 168 deletions

150
log/event.go Normal file
View 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
}

View file

@ -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
View 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
View 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
}