-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathlogging.go
More file actions
144 lines (129 loc) · 3.29 KB
/
Copy pathlogging.go
File metadata and controls
144 lines (129 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package main
import (
"fmt"
"io"
"log/slog"
"os"
"os/signal"
"strings"
"sync"
"syscall"
)
// LoggingConfig controls where and how the server writes its log stream.
type LoggingConfig struct {
Level string `toml:"level"` // debug, info, warn, error
Format string `toml:"format"` // json, text
Output string `toml:"output"` // stdout, stderr, or a file path
}
func parseLevel(s string) (slog.Level, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "", "info":
return slog.LevelInfo, nil
case "debug":
return slog.LevelDebug, nil
case "warn", "warning":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
}
return 0, fmt.Errorf("unknown log level %q (want debug, info, warn or error)", s)
}
// reopenWriter writes to a log file that can be reopened while the process
// runs. logrotate renames the file and signals the process; without reopening,
// the server would keep writing to the rotated-away inode forever.
type reopenWriter struct {
path string
mu sync.Mutex
f *os.File
}
func newReopenWriter(path string) (*reopenWriter, error) {
w := &reopenWriter{path: path}
if err := w.reopen(); err != nil {
return nil, err
}
return w, nil
}
func (w *reopenWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.f.Write(p)
}
func (w *reopenWriter) reopen() error {
f, err := os.OpenFile(w.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o640)
if err != nil {
return err
}
w.mu.Lock()
old := w.f
w.f = f
w.mu.Unlock()
if old != nil {
old.Close()
}
return nil
}
func (w *reopenWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
return w.f.Close()
}
// watchSIGHUP reopens the log file every time the process is signalled, so
// logrotate's default "rename then HUP" workflow works. The signal is never
// delivered on Windows, where the goroutine simply idles.
func (w *reopenWriter) watchSIGHUP(log *slog.Logger) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGHUP)
go func() {
for range ch {
if err := w.reopen(); err != nil {
log.Error("failed to reopen log file", "path", w.path, "error", err)
continue
}
log.Info("reopened log file", "path", w.path)
}
}()
}
// setupLogging builds the process logger and installs it as the slog default.
// The returned closer releases the log file, if one was opened.
func setupLogging(cfg LoggingConfig) (*slog.Logger, io.Closer, error) {
level, err := parseLevel(cfg.Level)
if err != nil {
return nil, nil, err
}
var (
w io.Writer
closer io.Closer
file *reopenWriter
)
switch strings.ToLower(strings.TrimSpace(cfg.Output)) {
case "", "stdout":
w = os.Stdout
case "stderr":
w = os.Stderr
default:
file, err = newReopenWriter(cfg.Output)
if err != nil {
return nil, nil, fmt.Errorf("failed to open log file: %w", err)
}
w, closer = file, file
}
opts := &slog.HandlerOptions{Level: level}
var h slog.Handler
switch strings.ToLower(strings.TrimSpace(cfg.Format)) {
case "", "json":
h = slog.NewJSONHandler(w, opts)
case "text":
h = slog.NewTextHandler(w, opts)
default:
if closer != nil {
closer.Close()
}
return nil, nil, fmt.Errorf("unknown log format %q (want json or text)", cfg.Format)
}
log := slog.New(h)
slog.SetDefault(log)
if file != nil {
file.watchSIGHUP(log)
}
return log, closer, nil
}