WIP: Logging
This commit is contained in:
parent
af4175a5bc
commit
a6641980c2
15 changed files with 631 additions and 168 deletions
27
cmd/app.go
27
cmd/app.go
|
@ -2,10 +2,12 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
"heckel.io/ntfy/log"
|
"heckel.io/ntfy/log"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -20,8 +22,14 @@ var flagsDefault = []cli.Flag{
|
||||||
&cli.BoolFlag{Name: "trace", EnvVars: []string{"NTFY_TRACE"}, Usage: "enable tracing (very verbose, be careful)"},
|
&cli.BoolFlag{Name: "trace", EnvVars: []string{"NTFY_TRACE"}, Usage: "enable tracing (very verbose, be careful)"},
|
||||||
&cli.BoolFlag{Name: "no-log-dates", Aliases: []string{"no_log_dates"}, EnvVars: []string{"NTFY_NO_LOG_DATES"}, Usage: "disable the date/time prefix"},
|
&cli.BoolFlag{Name: "no-log-dates", Aliases: []string{"no_log_dates"}, EnvVars: []string{"NTFY_NO_LOG_DATES"}, Usage: "disable the date/time prefix"},
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "log-level", Aliases: []string{"log_level"}, Value: log.InfoLevel.String(), EnvVars: []string{"NTFY_LOG_LEVEL"}, Usage: "set log level"}),
|
||||||
|
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "log-level-overrides", Aliases: []string{"log_level_overrides"}, EnvVars: []string{"NTFY_LOG_LEVEL_OVERRIDES"}, Usage: "set log level overrides"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "log-format", Aliases: []string{"log_format"}, Value: log.TextFormat.String(), EnvVars: []string{"NTFY_LOG_FORMAT"}, Usage: "set log level"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
logLevelOverrideRegex = regexp.MustCompile(`(?i)^([^=]+)\s*=\s*(\S+)\s*->\s*(TRACE|DEBUG|INFO|WARN|ERROR)$`)
|
||||||
|
)
|
||||||
|
|
||||||
// New creates a new CLI application
|
// New creates a new CLI application
|
||||||
func New() *cli.App {
|
func New() *cli.App {
|
||||||
return &cli.App{
|
return &cli.App{
|
||||||
|
@ -40,15 +48,30 @@ func New() *cli.App {
|
||||||
}
|
}
|
||||||
|
|
||||||
func initLogFunc(c *cli.Context) error {
|
func initLogFunc(c *cli.Context) error {
|
||||||
|
log.SetLevel(log.ToLevel(c.String("log-level")))
|
||||||
|
log.SetFormat(log.ToFormat(c.String("log-format")))
|
||||||
if c.Bool("trace") {
|
if c.Bool("trace") {
|
||||||
log.SetLevel(log.TraceLevel)
|
log.SetLevel(log.TraceLevel)
|
||||||
} else if c.Bool("debug") {
|
} else if c.Bool("debug") {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
} else {
|
|
||||||
log.SetLevel(log.ToLevel(c.String("log-level")))
|
|
||||||
}
|
}
|
||||||
if c.Bool("no-log-dates") {
|
if c.Bool("no-log-dates") {
|
||||||
log.DisableDates()
|
log.DisableDates()
|
||||||
}
|
}
|
||||||
|
if err := applyLogLevelOverrides(c.StringSlice("log-level-overrides")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyLogLevelOverrides(rawOverrides []string) error {
|
||||||
|
for _, override := range rawOverrides {
|
||||||
|
m := logLevelOverrideRegex.FindStringSubmatch(override)
|
||||||
|
if len(m) != 4 {
|
||||||
|
return fmt.Errorf(`invalid log level override "%s", must be "field=value -> loglevel", e.g. "user_id=u_123 -> DEBUG"`, override)
|
||||||
|
}
|
||||||
|
field, value, level := m[1], m[2], m[3]
|
||||||
|
log.SetLevelOverride(field, value, log.ToLevel(level))
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
31
cmd/serve.go
31
cmd/serve.go
|
@ -309,9 +309,9 @@ func execServe(c *cli.Context) error {
|
||||||
// Run server
|
// Run server
|
||||||
s, err := server.New(conf)
|
s, err := server.New(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err.Error())
|
||||||
} else if err := s.Run(); err != nil {
|
} else if err := s.Run(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
log.Info("Exiting.")
|
log.Info("Exiting.")
|
||||||
return nil
|
return nil
|
||||||
|
@ -338,7 +338,9 @@ func sigHandlerConfigReload(config string) {
|
||||||
log.Warn("Hot reload failed: %s", err.Error())
|
log.Warn("Hot reload failed: %s", err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
reloadLogLevel(inputSource)
|
if err := reloadLogLevel(inputSource); err != nil {
|
||||||
|
log.Warn("Reloading log level failed: %s", err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,13 +369,24 @@ func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func reloadLogLevel(inputSource altsrc.InputSourceContext) {
|
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
|
||||||
newLevelStr, err := inputSource.String("log-level")
|
newLevelStr, err := inputSource.String("log-level")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Cannot load log level: %s", err.Error())
|
return fmt.Errorf("cannot load log level: %s", err.Error())
|
||||||
return
|
|
||||||
}
|
}
|
||||||
newLevel := log.ToLevel(newLevelStr)
|
overrides, err := inputSource.StringSlice("log-level-overrides")
|
||||||
log.SetLevel(newLevel)
|
if err != nil {
|
||||||
log.Info("Log level is %s", newLevel.String())
|
return fmt.Errorf("cannot load log level overrides (1): %s", err.Error())
|
||||||
|
}
|
||||||
|
log.ResetLevelOverride()
|
||||||
|
if err := applyLogLevelOverrides(overrides); err != nil {
|
||||||
|
return fmt.Errorf("cannot load log level overrides (2): %s", err.Error())
|
||||||
|
}
|
||||||
|
log.SetLevel(log.ToLevel(newLevelStr))
|
||||||
|
if len(overrides) > 0 {
|
||||||
|
log.Info("Log level is %v, %d override(s) in place", strings.ToUpper(newLevelStr), len(overrides))
|
||||||
|
} else {
|
||||||
|
log.Info("Log level is %v", strings.ToUpper(newLevelStr))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
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
|
||||||
|
}
|
147
log/log.go
147
log/log.go
|
@ -2,71 +2,60 @@ package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
|
||||||
"sync"
|
"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 (
|
var (
|
||||||
level = InfoLevel
|
level = InfoLevel
|
||||||
|
format = TextFormat
|
||||||
|
overrides = make(map[string]*levelOverride)
|
||||||
mu = &sync.Mutex{}
|
mu = &sync.Mutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Trace prints the given message, if the current log level is TRACE
|
// Fatal prints the given message, and exits the program
|
||||||
func Trace(message string, v ...any) {
|
func Fatal(message string, v ...any) {
|
||||||
logIf(TraceLevel, message, v...)
|
newEvent().Fatal(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...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error prints the given message, if the current log level is ERROR or lower
|
// Error prints the given message, if the current log level is ERROR or lower
|
||||||
func Error(message string, v ...any) {
|
func Error(message string, v ...any) {
|
||||||
logIf(ErrorLevel, message, v...)
|
newEvent().Error(message, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal prints the given message, and exits the program
|
// Warn prints the given message, if the current log level is WARN or lower
|
||||||
func Fatal(v ...any) {
|
func Warn(message string, v ...any) {
|
||||||
log.Fatalln(v...)
|
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
|
// CurrentLevel returns the current log level
|
||||||
|
@ -83,30 +72,42 @@ func SetLevel(newLevel Level) {
|
||||||
level = newLevel
|
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
|
// DisableDates disables the date/time prefix
|
||||||
func DisableDates() {
|
func DisableDates() {
|
||||||
log.SetFlags(0)
|
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
|
// Loggable returns true if the given log level is lower or equal to the current log level
|
||||||
func Loggable(l Level) bool {
|
func Loggable(l Level) bool {
|
||||||
return CurrentLevel() <= l
|
return CurrentLevel() <= l
|
||||||
|
@ -121,9 +122,3 @@ func IsTrace() bool {
|
||||||
func IsDebug() bool {
|
func IsDebug() bool {
|
||||||
return Loggable(DebugLevel)
|
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
|
||||||
|
}
|
119
server/server.go
119
server/server.go
|
@ -138,6 +138,19 @@ const (
|
||||||
wsPongWait = 15 * time.Second
|
wsPongWait = 15 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Log tags
|
||||||
|
const (
|
||||||
|
tagPublish = "publish"
|
||||||
|
tagFirebase = "firebase"
|
||||||
|
tagEmail = "email" // Send email
|
||||||
|
tagSMTP = "smtp" // Receive email
|
||||||
|
tagPay = "pay"
|
||||||
|
tagAccount = "account"
|
||||||
|
tagManager = "manager"
|
||||||
|
tagResetter = "resetter"
|
||||||
|
tagWebsocket = "websocket"
|
||||||
|
)
|
||||||
|
|
||||||
// New instantiates a new Server. It creates the cache and adds a Firebase
|
// New instantiates a new Server. It creates the cache and adds a Firebase
|
||||||
// subscriber (if configured).
|
// subscriber (if configured).
|
||||||
func New(conf *Config) (*Server, error) {
|
func New(conf *Config) (*Server, error) {
|
||||||
|
@ -305,9 +318,9 @@ func (s *Server) closeDatabases() {
|
||||||
func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
||||||
v, err := s.maybeAuthenticate(r) // Note: Always returns v, even when error is returned
|
v, err := s.maybeAuthenticate(r) // Note: Always returns v, even when error is returned
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Debug("%s Dispatching request", logHTTPPrefix(v, r))
|
logvr(v, r).Debug("Dispatching request")
|
||||||
if log.IsTrace() {
|
if log.IsTrace() {
|
||||||
log.Trace("%s Entire request (headers and body):\n%s", logHTTPPrefix(v, r), renderHTTPRequest(r))
|
logvr(v, r).Trace("Entire request (headers and body):\n%s", renderHTTPRequest(r))
|
||||||
}
|
}
|
||||||
err = s.handleInternal(w, r, v)
|
err = s.handleInternal(w, r, v)
|
||||||
}
|
}
|
||||||
|
@ -315,9 +328,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 {
|
||||||
log.Debug("%s WebSocket error (this error is okay, it happens a lot): %s", logHTTPPrefix(v, r), err.Error())
|
logvr(v, r).Tag(tagWebsocket).Debug("WebSocket error (this error is okay, it happens a lot): %s", err.Error())
|
||||||
} else {
|
} else {
|
||||||
log.Info("%s WebSocket error: %s", logHTTPPrefix(v, r), err.Error())
|
logvr(v, r).Tag(tagWebsocket).Info("WebSocket error: %s", err.Error())
|
||||||
}
|
}
|
||||||
return // Do not attempt to write to upgraded connection
|
return // Do not attempt to write to upgraded connection
|
||||||
}
|
}
|
||||||
|
@ -331,9 +344,21 @@ 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 {
|
||||||
log.Debug("%s Connection closed with HTTP %d (ntfy error %d): %s", logHTTPPrefix(v, r), httpErr.HTTPCode, httpErr.Code, err.Error())
|
logvr(v, r).
|
||||||
|
Fields(map[string]any{
|
||||||
|
"error": err,
|
||||||
|
"error_code": httpErr.Code,
|
||||||
|
"http_status": httpErr.HTTPCode,
|
||||||
|
}).
|
||||||
|
Debug("Connection closed with HTTP %d (ntfy error %d): %s", httpErr.HTTPCode, httpErr.Code, err.Error())
|
||||||
} else {
|
} else {
|
||||||
log.Info("%s Connection closed with HTTP %d (ntfy error %d): %s", logHTTPPrefix(v, r), httpErr.HTTPCode, httpErr.Code, err.Error())
|
logvr(v, r).
|
||||||
|
Fields(map[string]any{
|
||||||
|
"error": err,
|
||||||
|
"error_code": httpErr.Code,
|
||||||
|
"http_status": httpErr.HTTPCode,
|
||||||
|
}).
|
||||||
|
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")
|
||||||
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
|
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
|
||||||
|
@ -586,10 +611,20 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes
|
||||||
m.Message = emptyMessageBody
|
m.Message = emptyMessageBody
|
||||||
}
|
}
|
||||||
delayed := m.Time > time.Now().Unix()
|
delayed := m.Time > time.Now().Unix()
|
||||||
log.Debug("%s Received message: event=%s, user=%s, body=%d byte(s), delayed=%t, firebase=%t, cache=%t, up=%t, email=%s",
|
logvrm(v, r, m).
|
||||||
logMessagePrefix(v, m), m.Event, m.User, len(m.Message), delayed, firebase, cache, unifiedpush, email)
|
Tag(tagPublish).
|
||||||
|
Fields(map[string]any{
|
||||||
|
"message_delayed": delayed,
|
||||||
|
"message_firebase": firebase,
|
||||||
|
"message_unifiedpush": unifiedpush,
|
||||||
|
"message_email": email,
|
||||||
|
}).
|
||||||
|
Debug("Received message")
|
||||||
if log.IsTrace() {
|
if log.IsTrace() {
|
||||||
log.Trace("%s Message body: %s", logMessagePrefix(v, m), util.MaybeMarshalJSON(m))
|
logvrm(v, r, m).
|
||||||
|
Tag(tagPublish).
|
||||||
|
Field("message_body", util.MaybeMarshalJSON(m)).
|
||||||
|
Trace("Message body")
|
||||||
}
|
}
|
||||||
if !delayed {
|
if !delayed {
|
||||||
if err := t.Publish(v, m); err != nil {
|
if err := t.Publish(v, m); err != nil {
|
||||||
|
@ -605,10 +640,10 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes
|
||||||
go s.forwardPollRequest(v, m)
|
go s.forwardPollRequest(v, m)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m))
|
logvrm(v, r, m).Tag(tagPublish).Debug("Message delayed, will process later")
|
||||||
}
|
}
|
||||||
if cache {
|
if cache {
|
||||||
log.Debug("%s Adding message to cache", logMessagePrefix(v, m))
|
logvrm(v, r, m).Tag(tagPublish).Debug("Adding message to cache")
|
||||||
if err := s.messageCache.AddMessage(m); err != nil {
|
if err := s.messageCache.AddMessage(m); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -640,20 +675,20 @@ func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) sendToFirebase(v *visitor, m *message) {
|
func (s *Server) sendToFirebase(v *visitor, m *message) {
|
||||||
log.Debug("%s Publishing to Firebase", logMessagePrefix(v, m))
|
logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase")
|
||||||
if err := s.firebaseClient.Send(v, m); err != nil {
|
if err := s.firebaseClient.Send(v, m); err != nil {
|
||||||
if err == errFirebaseTemporarilyBanned {
|
if err == errFirebaseTemporarilyBanned {
|
||||||
log.Debug("%s Unable to publish to Firebase: %v", logMessagePrefix(v, m), err.Error())
|
logvm(v, m).Tag(tagFirebase).Err(err).Debug("Unable to publish to Firebase: %v", err.Error())
|
||||||
} else {
|
} else {
|
||||||
log.Warn("%s Unable to publish to Firebase: %v", logMessagePrefix(v, m), err.Error())
|
logvm(v, m).Tag(tagFirebase).Err(err).Warn("Unable to publish to Firebase: %v", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) sendEmail(v *visitor, m *message, email string) {
|
func (s *Server) sendEmail(v *visitor, m *message, email string) {
|
||||||
log.Debug("%s Sending email to %s", logMessagePrefix(v, m), email)
|
logvm(v, m).Tag(tagEmail).Field("email", email).Debug("Sending email to %s", email)
|
||||||
if err := s.smtpSender.Send(v, m, email); err != nil {
|
if err := s.smtpSender.Send(v, m, email); err != nil {
|
||||||
log.Warn("%s Unable to send email to %s: %v", logMessagePrefix(v, m), email, err.Error())
|
logvm(v, m).Tag(tagEmail).Field("email", email).Err(err).Warn("Unable to send email to %s: %v", email, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -661,10 +696,10 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
|
||||||
topicURL := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic)
|
topicURL := fmt.Sprintf("%s/%s", s.config.BaseURL, m.Topic)
|
||||||
topicHash := fmt.Sprintf("%x", sha256.Sum256([]byte(topicURL)))
|
topicHash := fmt.Sprintf("%x", sha256.Sum256([]byte(topicURL)))
|
||||||
forwardURL := fmt.Sprintf("%s/%s", s.config.UpstreamBaseURL, topicHash)
|
forwardURL := fmt.Sprintf("%s/%s", s.config.UpstreamBaseURL, topicHash)
|
||||||
log.Debug("%s Publishing poll request to %s", logMessagePrefix(v, m), forwardURL)
|
logvm(v, m).Debug("Publishing poll request to %s", forwardURL)
|
||||||
req, err := http.NewRequest("POST", forwardURL, strings.NewReader(""))
|
req, err := http.NewRequest("POST", forwardURL, strings.NewReader(""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("%s Unable to publish poll request: %v", logMessagePrefix(v, m), err.Error())
|
logvm(v, m).Err(err).Warn("Unable to publish poll request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Header.Set("X-Poll-ID", m.ID)
|
req.Header.Set("X-Poll-ID", m.ID)
|
||||||
|
@ -673,10 +708,10 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
|
||||||
}
|
}
|
||||||
response, err := httpClient.Do(req)
|
response, err := httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("%s Unable to publish poll request: %v", logMessagePrefix(v, m), err.Error())
|
logvm(v, m).Err(err).Warn("Unable to publish poll request")
|
||||||
return
|
return
|
||||||
} else if response.StatusCode != http.StatusOK {
|
} else if response.StatusCode != http.StatusOK {
|
||||||
log.Warn("%s Unable to publish poll request, unexpected HTTP status: %d", logMessagePrefix(v, m), response.StatusCode)
|
logvm(v, m).Err(err).Warn("Unable to publish poll request, unexpected HTTP status: %d")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -924,8 +959,8 @@ func (s *Server) handleSubscribeRaw(w http.ResponseWriter, r *http.Request, v *v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *visitor, contentType string, encoder messageEncoder) error {
|
func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *visitor, contentType string, encoder messageEncoder) error {
|
||||||
log.Debug("%s HTTP stream connection opened", logHTTPPrefix(v, r))
|
logvr(v, r).Debug("HTTP stream connection opened")
|
||||||
defer log.Debug("%s HTTP stream connection closed", logHTTPPrefix(v, r))
|
defer logvr(v, r).Debug("HTTP stream connection closed")
|
||||||
if !v.SubscriptionAllowed() {
|
if !v.SubscriptionAllowed() {
|
||||||
return errHTTPTooManyRequestsLimitSubscriptions
|
return errHTTPTooManyRequestsLimitSubscriptions
|
||||||
}
|
}
|
||||||
|
@ -993,7 +1028,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
|
||||||
case <-r.Context().Done():
|
case <-r.Context().Done():
|
||||||
return nil
|
return nil
|
||||||
case <-time.After(s.config.KeepaliveInterval):
|
case <-time.After(s.config.KeepaliveInterval):
|
||||||
log.Trace("%s Sending keepalive message", logHTTPPrefix(v, r))
|
logvr(v, r).Trace("Sending keepalive message")
|
||||||
v.Keepalive()
|
v.Keepalive()
|
||||||
if err := sub(v, newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message
|
if err := sub(v, newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message
|
||||||
return err
|
return err
|
||||||
|
@ -1010,8 +1045,8 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
||||||
return errHTTPTooManyRequestsLimitSubscriptions
|
return errHTTPTooManyRequestsLimitSubscriptions
|
||||||
}
|
}
|
||||||
defer v.RemoveSubscription()
|
defer v.RemoveSubscription()
|
||||||
log.Debug("%s WebSocket connection opened", logHTTPPrefix(v, r))
|
logvr(v, r).Tag(tagWebsocket).Debug("WebSocket connection opened")
|
||||||
defer log.Debug("%s WebSocket connection closed", logHTTPPrefix(v, r))
|
defer logvr(v, r).Tag(tagWebsocket).Debug("WebSocket connection closed")
|
||||||
topics, topicsStr, err := s.topicsFromPath(r.URL.Path)
|
topics, topicsStr, err := s.topicsFromPath(r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -1047,7 +1082,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.SetPongHandler(func(appData string) error {
|
conn.SetPongHandler(func(appData string) error {
|
||||||
log.Trace("%s Received WebSocket pong", logHTTPPrefix(v, r))
|
logvr(v, r).Tag(tagWebsocket).Trace("Received WebSocket pong")
|
||||||
return conn.SetReadDeadline(time.Now().Add(pongWait))
|
return conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||||
})
|
})
|
||||||
for {
|
for {
|
||||||
|
@ -1069,7 +1104,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
||||||
if err := conn.SetWriteDeadline(time.Now().Add(wsWriteWait)); err != nil {
|
if err := conn.SetWriteDeadline(time.Now().Add(wsWriteWait)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Trace("%s Sending WebSocket ping", logHTTPPrefix(v, r))
|
logvr(v, r).Tag(tagWebsocket).Trace("Sending WebSocket ping")
|
||||||
return conn.WriteMessage(websocket.PingMessage, nil)
|
return conn.WriteMessage(websocket.PingMessage, nil)
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
|
@ -1077,7 +1112,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
||||||
case <-gctx.Done():
|
case <-gctx.Done():
|
||||||
return nil
|
return nil
|
||||||
case <-cancelCtx.Done():
|
case <-cancelCtx.Done():
|
||||||
log.Trace("%s Cancel received, closing subscriber connection", logHTTPPrefix(v, r))
|
logvr(v, r).Tag(tagWebsocket).Trace("Cancel received, closing subscriber connection")
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: "subscription was canceled"}
|
return &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: "subscription was canceled"}
|
||||||
case <-time.After(s.config.KeepaliveInterval):
|
case <-time.After(s.config.KeepaliveInterval):
|
||||||
|
@ -1120,7 +1155,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) {
|
||||||
log.Trace("%s WebSocket connection closed: %s", logHTTPPrefix(v, r), err.Error())
|
logvr(v, r).Tag(tagWebsocket).Err(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
|
||||||
|
@ -1251,8 +1286,8 @@ func (s *Server) topicFromID(id string) (*topic, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) execManager() {
|
func (s *Server) execManager() {
|
||||||
log.Debug("Manager: Starting")
|
log.Tag(tagManager).Debug("Starting manager")
|
||||||
defer log.Debug("Manager: Finished")
|
defer log.Tag(tagManager).Debug("Finished manager")
|
||||||
|
|
||||||
// WARNING: Make sure to only selectively lock with the mutex, and be aware that this
|
// WARNING: Make sure to only selectively lock with the mutex, and be aware that this
|
||||||
// there is no mutex for the entire function.
|
// there is no mutex for the entire function.
|
||||||
|
@ -1393,13 +1428,13 @@ func (s *Server) runStatsResetter() {
|
||||||
for {
|
for {
|
||||||
runAt := util.NextOccurrenceUTC(s.config.VisitorStatsResetTime, time.Now())
|
runAt := util.NextOccurrenceUTC(s.config.VisitorStatsResetTime, time.Now())
|
||||||
timer := time.NewTimer(time.Until(runAt))
|
timer := time.NewTimer(time.Until(runAt))
|
||||||
log.Debug("Stats resetter: Waiting until %v to reset visitor stats", runAt)
|
log.Tag(tagResetter).Debug("Waiting until %v to reset visitor stats", runAt)
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
log.Debug("Stats resetter: Running")
|
log.Tag(tagResetter).Debug("Running stats resetter")
|
||||||
s.resetStats()
|
s.resetStats()
|
||||||
case <-s.closeChan:
|
case <-s.closeChan:
|
||||||
log.Debug("Stats resetter: Stopping timer")
|
log.Tag(tagResetter).Debug("Stopping stats resetter")
|
||||||
timer.Stop()
|
timer.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1415,7 +1450,7 @@ func (s *Server) resetStats() {
|
||||||
}
|
}
|
||||||
if s.userManager != nil {
|
if s.userManager != nil {
|
||||||
if err := s.userManager.ResetStats(); err != nil {
|
if err := s.userManager.ResetStats(); err != nil {
|
||||||
log.Warn("Failed to write to database: %s", err.Error())
|
log.Tag(tagResetter).Warn("Failed to write to database: %s", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1442,7 +1477,7 @@ func (s *Server) runDelayedSender() {
|
||||||
select {
|
select {
|
||||||
case <-time.After(s.config.DelayedSenderInterval):
|
case <-time.After(s.config.DelayedSenderInterval):
|
||||||
if err := s.sendDelayedMessages(); err != nil {
|
if err := s.sendDelayedMessages(); err != nil {
|
||||||
log.Warn("Error sending delayed messages: %s", err.Error())
|
log.Tag(tagPublish).Err(err).Warn("Error sending delayed messages")
|
||||||
}
|
}
|
||||||
case <-s.closeChan:
|
case <-s.closeChan:
|
||||||
return
|
return
|
||||||
|
@ -1460,20 +1495,20 @@ func (s *Server) sendDelayedMessages() error {
|
||||||
if s.userManager != nil && m.User != "" {
|
if s.userManager != nil && m.User != "" {
|
||||||
u, err = s.userManager.User(m.User)
|
u, err = s.userManager.User(m.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("Error sending delayed message %s: %s", m.ID, err.Error())
|
log.Context(m).Err(err).Warn("Error sending delayed message")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
v := s.visitor(m.Sender, u)
|
v := s.visitor(m.Sender, u)
|
||||||
if err := s.sendDelayedMessage(v, m); err != nil {
|
if err := s.sendDelayedMessage(v, m); err != nil {
|
||||||
log.Warn("%s Error sending delayed message: %s", logMessagePrefix(v, m), err.Error())
|
logvm(v, m).Err(err).Warn("Error sending delayed message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
|
func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
|
||||||
log.Debug("%s Sending delayed message", logMessagePrefix(v, m))
|
logvm(v, m).Debug("Sending delayed message")
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published
|
t, ok := s.topics[m.Topic] // If no subscribers, just mark message as published
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
@ -1481,7 +1516,7 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
|
||||||
go func() {
|
go func() {
|
||||||
// We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler
|
// We do not rate-limit messages here, since we've rate limited them in the PUT/POST handler
|
||||||
if err := t.Publish(v, m); err != nil {
|
if err := t.Publish(v, m); err != nil {
|
||||||
log.Warn("%s Unable to publish message: %v", logMessagePrefix(v, m), err.Error())
|
logvm(v, m).Err(err).Warn("Unable to publish message")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
@ -1595,7 +1630,7 @@ func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc
|
||||||
u := v.User()
|
u := v.User()
|
||||||
for _, t := range topics {
|
for _, t := range topics {
|
||||||
if err := s.userManager.Authorize(u, t.ID, perm); err != nil {
|
if err := s.userManager.Authorize(u, t.ID, perm); err != nil {
|
||||||
log.Info("unauthorized: %s", err.Error())
|
logvr(v, r).Err(err).Debug("Unauthorized")
|
||||||
return errHTTPForbidden
|
return errHTTPForbidden
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1609,7 +1644,7 @@ func (s *Server) maybeAuthenticate(r *http.Request) (v *visitor, err error) {
|
||||||
ip := extractIPAddress(r, s.config.BehindProxy)
|
ip := extractIPAddress(r, s.config.BehindProxy)
|
||||||
var u *user.User // may stay nil if no auth header!
|
var u *user.User // may stay nil if no auth header!
|
||||||
if u, err = s.authenticate(r); err != nil {
|
if u, err = s.authenticate(r); err != nil {
|
||||||
log.Debug("authentication failed: %s", err.Error())
|
logr(r).Debug("Authentication failed: %s", err.Error())
|
||||||
err = errHTTPUnauthorized // Always return visitor, even when error occurs!
|
err = errHTTPUnauthorized // Always return visitor, even when error occurs!
|
||||||
}
|
}
|
||||||
v = s.visitor(ip, u)
|
v = s.visitor(ip, u)
|
||||||
|
|
|
@ -155,7 +155,7 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *
|
||||||
return errHTTPBadRequestIncorrectPasswordConfirmation
|
return errHTTPBadRequestIncorrectPasswordConfirmation
|
||||||
}
|
}
|
||||||
if u.Billing.StripeSubscriptionID != "" {
|
if u.Billing.StripeSubscriptionID != "" {
|
||||||
log.Info("%s Canceling billing subscription %s", logHTTPPrefix(v, r), u.Billing.StripeSubscriptionID)
|
logvr(v, r).Tag(tagPay).Info("Canceling billing subscription for user %s", u.Name)
|
||||||
if _, err := s.stripe.CancelSubscription(u.Billing.StripeSubscriptionID); err != nil {
|
if _, err := s.stripe.CancelSubscription(u.Billing.StripeSubscriptionID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *
|
||||||
if err := s.maybeRemoveMessagesAndExcessReservations(logHTTPPrefix(v, r), u, 0); err != nil {
|
if err := s.maybeRemoveMessagesAndExcessReservations(logHTTPPrefix(v, r), u, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Info("%s Marking user %s as deleted", logHTTPPrefix(v, r), u.Name)
|
logvr(v, r).Tag(tagAccount).Info("Marking user %s as deleted", u.Name)
|
||||||
if err := s.userManager.MarkUserRemoved(u); err != nil {
|
if err := s.userManager.MarkUserRemoved(u); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -184,6 +184,7 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ
|
||||||
if err := s.userManager.ChangePassword(u.Name, req.NewPassword); err != nil {
|
if err := s.userManager.ChangePassword(u.Name, req.NewPassword); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
logvr(v, r).Tag(tagAccount).Debug("Changed password for user %s", u.Name)
|
||||||
return s.writeJSON(w, newSuccessResponse())
|
return s.writeJSON(w, newSuccessResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,10 +202,12 @@ func (s *Server) handleAccountTokenCreate(w http.ResponseWriter, r *http.Request
|
||||||
if req.Expires != nil {
|
if req.Expires != nil {
|
||||||
expires = time.Unix(*req.Expires, 0)
|
expires = time.Unix(*req.Expires, 0)
|
||||||
}
|
}
|
||||||
token, err := s.userManager.CreateToken(v.User().ID, label, expires, v.IP())
|
u := v.User()
|
||||||
|
token, err := s.userManager.CreateToken(u.ID, label, expires, v.IP())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
logvr(v, r).Tag(tagAccount).Debug("Created token for user %s", u.Name)
|
||||||
response := &apiAccountTokenResponse{
|
response := &apiAccountTokenResponse{
|
||||||
Token: token.Value,
|
Token: token.Value,
|
||||||
Label: token.Label,
|
Label: token.Label,
|
||||||
|
|
|
@ -47,11 +47,16 @@ func (c *firebaseClient) Send(v *visitor, m *message) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if log.IsTrace() {
|
if log.IsTrace() {
|
||||||
log.Trace("%s Firebase message: %s", logMessagePrefix(v, m), util.MaybeMarshalJSON(fbm))
|
logvm(v, m).
|
||||||
|
Tag(tagFirebase).
|
||||||
|
Field("firebase_message", util.MaybeMarshalJSON(fbm)).
|
||||||
|
Trace("Firebase message")
|
||||||
}
|
}
|
||||||
err = c.sender.Send(fbm)
|
err = c.sender.Send(fbm)
|
||||||
if err == errFirebaseQuotaExceeded {
|
if err == errFirebaseQuotaExceeded {
|
||||||
log.Warn("%s Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor", logMessagePrefix(v, m))
|
logvm(v, m).
|
||||||
|
Tag(tagFirebase).
|
||||||
|
Warn("Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor")
|
||||||
v.FirebaseTemporarilyDeny()
|
v.FirebaseTemporarilyDeny()
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
_ "embed" // required by go:embed
|
_ "embed" // required by go:embed
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"mime"
|
"mime"
|
||||||
"net"
|
"net"
|
||||||
|
@ -37,8 +36,18 @@ func (s *smtpSender) Send(v *visitor, m *message, to string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
auth := smtp.PlainAuth("", s.config.SMTPSenderUser, s.config.SMTPSenderPass, host)
|
auth := smtp.PlainAuth("", s.config.SMTPSenderUser, s.config.SMTPSenderPass, host)
|
||||||
log.Debug("%s Sending mail: via=%s, user=%s, pass=***, to=%s", logMessagePrefix(v, m), s.config.SMTPSenderAddr, s.config.SMTPSenderUser, to)
|
logvm(v, m).
|
||||||
log.Trace("%s Mail body: %s", logMessagePrefix(v, m), message)
|
Tag(tagEmail).
|
||||||
|
Fields(map[string]any{
|
||||||
|
"email_via": s.config.SMTPSenderAddr,
|
||||||
|
"email_user": s.config.SMTPSenderUser,
|
||||||
|
"email_to": to,
|
||||||
|
}).
|
||||||
|
Debug("Sending email")
|
||||||
|
logvm(v, m).
|
||||||
|
Tag(tagEmail).
|
||||||
|
Field("email_body", message).
|
||||||
|
Trace("Email body")
|
||||||
return smtp.SendMail(s.config.SMTPSenderAddr, auth, s.config.SMTPSenderFrom, []string{to}, []byte(message))
|
return smtp.SendMail(s.config.SMTPSenderAddr, auth, s.config.SMTPSenderFrom, []string{to}, []byte(message))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -54,7 +63,7 @@ func (s *smtpSender) withCount(v *visitor, m *message, fn func() error) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("%s Sending mail failed: %s", logMessagePrefix(v, m), err.Error())
|
logvm(v, m).Err(err).Debug("Sending mail failed")
|
||||||
s.failure++
|
s.failure++
|
||||||
} else {
|
} else {
|
||||||
s.success++
|
s.success++
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"heckel.io/ntfy/log"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
@ -41,13 +40,13 @@ func newMailBackend(conf *Config, handler func(http.ResponseWriter, *http.Reques
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *smtpBackend) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
|
func (b *smtpBackend) Login(state *smtp.ConnectionState, username, _ string) (smtp.Session, error) {
|
||||||
log.Debug("%s Incoming mail, login with user %s", logSMTPPrefix(state), username)
|
logem(state).Debug("Incoming mail, login with user %s", username)
|
||||||
return &smtpSession{backend: b, state: state}, nil
|
return &smtpSession{backend: b, state: state}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
func (b *smtpBackend) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
||||||
log.Debug("%s Incoming mail, anonymous login", logSMTPPrefix(state))
|
logem(state).Debug("Incoming mail, anonymous login")
|
||||||
return &smtpSession{backend: b, state: state}, nil
|
return &smtpSession{backend: b, state: state}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,17 +65,17 @@ type smtpSession struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) AuthPlain(username, password string) error {
|
func (s *smtpSession) AuthPlain(username, password string) error {
|
||||||
log.Debug("%s AUTH PLAIN (with username %s)", logSMTPPrefix(s.state), username)
|
logem(s.state).Debug("AUTH PLAIN (with username %s)", username)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Mail(from string, opts smtp.MailOptions) error {
|
func (s *smtpSession) Mail(from string, opts smtp.MailOptions) error {
|
||||||
log.Debug("%s MAIL FROM: %s (with options: %#v)", logSMTPPrefix(s.state), from, opts)
|
logem(s.state).Debug("%s MAIL FROM: %s (with options: %#v)", from, opts)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpSession) Rcpt(to string) error {
|
func (s *smtpSession) Rcpt(to string) error {
|
||||||
log.Debug("%s RCPT TO: %s", logSMTPPrefix(s.state), to)
|
logem(s.state).Debug("RCPT TO: %s", to)
|
||||||
return s.withFailCount(func() error {
|
return s.withFailCount(func() error {
|
||||||
conf := s.backend.config
|
conf := s.backend.config
|
||||||
addressList, err := mail.ParseAddressList(to)
|
addressList, err := mail.ParseAddressList(to)
|
||||||
|
@ -113,10 +112,11 @@ func (s *smtpSession) Data(r io.Reader) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if log.IsTrace() {
|
ev := logem(s.state).Tag(tagSMTP)
|
||||||
log.Trace("%s DATA: %s", logSMTPPrefix(s.state), string(b))
|
if ev.IsTrace() {
|
||||||
} else if log.IsDebug() {
|
ev.Field("smtp_data", string(b)).Trace("DATA")
|
||||||
log.Debug("%s DATA: %d byte(s)", logSMTPPrefix(s.state), len(b))
|
} else if ev.IsDebug() {
|
||||||
|
ev.Debug("DATA: %d byte(s)", len(b))
|
||||||
}
|
}
|
||||||
msg, err := mail.ReadMessage(bytes.NewReader(b))
|
msg, err := mail.ReadMessage(bytes.NewReader(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -198,7 +198,7 @@ func (s *smtpSession) withFailCount(fn func() error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Almost all of these errors are parse errors, and user input errors.
|
// Almost all of these errors are parse errors, and user input errors.
|
||||||
// We do not want to spam the log with WARN messages.
|
// We do not want to spam the log with WARN messages.
|
||||||
log.Debug("%s Incoming mail error: %s", logSMTPPrefix(s.state), err.Error())
|
logem(s.state).Err(err).Debug("Incoming mail error")
|
||||||
s.backend.failure++
|
s.backend.failure++
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -58,18 +58,18 @@ func (t *topic) Publish(v *visitor, m *message) error {
|
||||||
// subscribers map here. Actually sending out the messages then doesn't have to lock.
|
// subscribers map here. Actually sending out the messages then doesn't have to lock.
|
||||||
subscribers := t.subscribersCopy()
|
subscribers := t.subscribersCopy()
|
||||||
if len(subscribers) > 0 {
|
if len(subscribers) > 0 {
|
||||||
log.Debug("%s Forwarding to %d subscriber(s)", logMessagePrefix(v, m), len(subscribers))
|
logvm(v, m).Tag(tagPublish).Debug("Forwarding to %d subscriber(s)", len(subscribers))
|
||||||
for _, s := range subscribers {
|
for _, s := range subscribers {
|
||||||
// We call the subscriber functions in their own Go routines because they are blocking, and
|
// We call the subscriber functions in their own Go routines because they are blocking, and
|
||||||
// we don't want individual slow subscribers to be able to block others.
|
// we don't want individual slow subscribers to be able to block others.
|
||||||
go func(s subscriber) {
|
go func(s subscriber) {
|
||||||
if err := s(v, m); err != nil {
|
if err := s(v, m); err != nil {
|
||||||
log.Warn("%s Error forwarding to subscriber: %s", logMessagePrefix(v, m), err.Error())
|
logvm(v, m).Tag(tagPublish).Err(err).Warn("Error forwarding to subscriber")
|
||||||
}
|
}
|
||||||
}(s.subscriber)
|
}(s.subscriber)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Trace("%s No stream or WebSocket subscribers, not forwarding", logMessagePrefix(v, m))
|
logvm(v, m).Tag(tagPublish).Trace("No stream or WebSocket subscribers, not forwarding")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -42,6 +42,23 @@ type message struct {
|
||||||
User string `json:"-"` // Username of the uploader, used to associated attachments
|
User string `json:"-"` // Username of the uploader, used to associated attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *message) Context() map[string]any {
|
||||||
|
fields := map[string]any{
|
||||||
|
"message_id": m.ID,
|
||||||
|
"message_time": m.Time,
|
||||||
|
"message_event": m.Event,
|
||||||
|
"message_topic": m.Topic,
|
||||||
|
"message_body_size": len(m.Message),
|
||||||
|
}
|
||||||
|
if m.Sender != netip.IPv4Unspecified() {
|
||||||
|
fields["message_sender"] = m.Sender.String()
|
||||||
|
}
|
||||||
|
if m.User != "" {
|
||||||
|
fields["message_user"] = m.User
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
type attachment struct {
|
type attachment struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
|
|
|
@ -48,8 +48,44 @@ func readQueryParam(r *http.Request, names ...string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func logMessagePrefix(v *visitor, m *message) string {
|
func logr(r *http.Request) *log.Event {
|
||||||
return fmt.Sprintf("%s/%s/%s", v.String(), m.Topic, m.ID)
|
return log.Fields(logFieldsHTTP(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func logv(v *visitor) *log.Event {
|
||||||
|
return log.Context(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logvr(v *visitor, r *http.Request) *log.Event {
|
||||||
|
return logv(v).Fields(logFieldsHTTP(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func logvrm(v *visitor, r *http.Request, m *message) *log.Event {
|
||||||
|
return logvr(v, r).Context(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logvm(v *visitor, m *message) *log.Event {
|
||||||
|
return logv(v).Context(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func logem(state *smtp.ConnectionState) *log.Event {
|
||||||
|
return log.
|
||||||
|
Tag(tagSMTP).
|
||||||
|
Fields(map[string]any{
|
||||||
|
"smtp_hostname": state.Hostname,
|
||||||
|
"smtp_remote_addr": state.RemoteAddr.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func logFieldsHTTP(r *http.Request) map[string]any {
|
||||||
|
requestURI := r.RequestURI
|
||||||
|
if requestURI == "" {
|
||||||
|
requestURI = r.URL.Path
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"http_method": r.Method,
|
||||||
|
"http_path": requestURI,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logHTTPPrefix(v *visitor, r *http.Request) string {
|
func logHTTPPrefix(v *visitor, r *http.Request) string {
|
||||||
|
@ -67,10 +103,6 @@ func logStripePrefix(customerID, subscriptionID string) string {
|
||||||
return fmt.Sprintf("STRIPE %s", customerID)
|
return fmt.Sprintf("STRIPE %s", customerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func logSMTPPrefix(state *smtp.ConnectionState) string {
|
|
||||||
return fmt.Sprintf("SMTP %s/%s", state.Hostname, state.RemoteAddr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderHTTPRequest(r *http.Request) string {
|
func renderHTTPRequest(r *http.Request) string {
|
||||||
peekLimit := 4096
|
peekLimit := 4096
|
||||||
lines := fmt.Sprintf("%s %s %s\n", r.Method, r.URL.RequestURI(), r.Proto)
|
lines := fmt.Sprintf("%s %s %s\n", r.Method, r.URL.RequestURI(), r.Proto)
|
||||||
|
|
|
@ -150,6 +150,25 @@ func (v *visitor) stringNoLock() string {
|
||||||
return v.ip.String()
|
return v.ip.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *visitor) Context() map[string]any {
|
||||||
|
v.mu.Lock()
|
||||||
|
defer v.mu.Unlock()
|
||||||
|
fields := map[string]any{
|
||||||
|
"visitor_ip": v.ip.String(),
|
||||||
|
}
|
||||||
|
if v.user != nil {
|
||||||
|
fields["user_id"] = v.user.ID
|
||||||
|
fields["user_name"] = v.user.Name
|
||||||
|
if v.user.Billing.StripeCustomerID != "" {
|
||||||
|
fields["stripe_customer_id"] = v.user.Billing.StripeCustomerID
|
||||||
|
}
|
||||||
|
if v.user.Billing.StripeSubscriptionID != "" {
|
||||||
|
fields["stripe_subscription_id"] = v.user.Billing.StripeSubscriptionID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
func (v *visitor) RequestAllowed() error {
|
func (v *visitor) RequestAllowed() error {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.Lock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.Unlock()
|
||||||
|
@ -254,12 +273,6 @@ func (v *visitor) User() *user.User {
|
||||||
return v.user // May be nil
|
return v.user // May be nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin returns true if the visitor is a user, and an admin
|
|
||||||
func (v *visitor) Admin() bool {
|
|
||||||
u := v.User()
|
|
||||||
return u != nil && u.Role == user.RoleAdmin
|
|
||||||
}
|
|
||||||
|
|
||||||
// IP returns the visitor IP address
|
// IP returns the visitor IP address
|
||||||
func (v *visitor) IP() netip.Addr {
|
func (v *visitor) IP() netip.Addr {
|
||||||
v.mu.Lock()
|
v.mu.Lock()
|
||||||
|
@ -297,7 +310,7 @@ func (v *visitor) MaybeUserID() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) resetLimitersNoLock(messages, emails int64, enqueueUpdate bool) {
|
func (v *visitor) resetLimitersNoLock(messages, emails int64, enqueueUpdate bool) {
|
||||||
log.Debug("%s Resetting limiters for visitor", v.stringNoLock())
|
log.Context(v).Debug("%s Resetting limiters for visitor", v.stringNoLock())
|
||||||
limits := v.limitsNoLock()
|
limits := v.limitsNoLock()
|
||||||
v.requestLimiter = rate.NewLimiter(limits.RequestLimitReplenish, limits.RequestLimitBurst)
|
v.requestLimiter = rate.NewLimiter(limits.RequestLimitReplenish, limits.RequestLimitBurst)
|
||||||
v.messagesLimiter = util.NewFixedLimiterWithValue(limits.MessageLimit, messages)
|
v.messagesLimiter = util.NewFixedLimiterWithValue(limits.MessageLimit, messages)
|
||||||
|
|
Loading…
Reference in a new issue