Skip to content

Commit 03fca0d

Browse files
authored
Merge pull request #10 from systemd/implement-level
Implement dynamic log level based on DEBUG_INVOCATION
2 parents 344aba3 + 8e21063 commit 03fca0d

File tree

2 files changed

+88
-29
lines changed

2 files changed

+88
-29
lines changed

journal.go

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,38 @@ import (
1212
"runtime"
1313
"slices"
1414
"strconv"
15+
"sync"
1516
)
1617

18+
// Names of levels corresponding to syslog.Priority values.
1719
const (
18-
LevelNotice slog.Level = 1
19-
20+
LevelNotice slog.Level = slog.LevelInfo + 1
2021
LevelCritical slog.Level = slog.LevelError + 1
2122
LevelAlert slog.Level = slog.LevelError + 2
2223
LevelEmergency slog.Level = slog.LevelError + 3
2324
)
2425

26+
// LevelVar is similar to [slog.LevelVar] but also implements the service side of [RestartMode=debug].
27+
// It looks if the environment variable DEBUG_INVOCATION is set and if so, sets the level to slog.LevelDebug.
28+
// The zero value of LevelVar is equivalent to slog.LevelInfo.
29+
// In the future, we might extend the behaviour of LevelVar to implement [org.freedesktop.LogControl1].
30+
//
31+
// [RestartMode=debug]: https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#RestartMode=
32+
// [org.freedesktop.LogControl1]: https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.LogControl1.html
33+
type LevelVar struct {
34+
slog.LevelVar
35+
}
36+
37+
// Return v's level.
38+
func (v *LevelVar) Level() slog.Level {
39+
sync.OnceFunc(func() {
40+
if os.Getenv("DEBUG_INVOCATION") != "" {
41+
v.Set(slog.LevelDebug)
42+
}
43+
})()
44+
return v.LevelVar.Level()
45+
}
46+
2547
func levelToPriority(l slog.Level) syslog.Priority {
2648
switch l {
2749
case slog.LevelDebug:
@@ -45,26 +67,25 @@ func levelToPriority(l slog.Level) syslog.Priority {
4567
}
4668
}
4769

70+
// Options configure the Journal handler.
4871
type Options struct {
4972
Level slog.Leveler
5073

5174
// ReplaceAttr is called on all non-builtin Attrs before they are written.
5275
// This can be useful for processing attributes to be in the correct format
5376
// for log statements outside of your own code as the journal only accepts
54-
// variables that are uppercase and consist only of characters, numbers and
55-
// underscores, and may not begin with an underscore.
77+
// keys of the form ^[A-Z_][A-Z0-9_]*$.
5678
ReplaceAttr func(groups []string, a slog.Attr) slog.Attr
5779

5880
// ReplaceGroup is called on all group names before they are written. This
5981
// can be useful for processing group names to be in the correct format for
6082
// log statements outside of your own code as the journal only accepts
61-
// variables that are uppercase and consist only of characters, numbers and
62-
// underscores, and may not begin with an underscore.
83+
// keys of the form ^[A-Z_][A-Z0-9_]*$.
6384
ReplaceGroup func(group string) string
6485
}
6586

6687
// Handler sends logs to the systemd journal.
67-
// variable names must be in uppercase and consist only of characters, numbers and underscores, and may not begin with an underscore.
88+
// The journal only accepts keys of the form ^[A-Z_][A-Z0-9_]*$.
6889
type Handler struct {
6990
opts Options
7091
// NOTE: We only do single Write() calls. Either the message fits in a
@@ -79,6 +100,12 @@ type Handler struct {
79100

80101
const sndBufSize = 8 * 1024 * 1024
81102

103+
// NewHandler returns a new Handler that writes to the systemd journal.
104+
// The journal only accepts keys of the form ^[A-Z_][A-Z0-9_]*$.
105+
// If opts is nil, the default options are used.
106+
// If opts.Level is nil, the default level is a [LevelVar] which is equivalent to
107+
// slog.LevelInfo unless the environment variable DEBUG_INVOCATION is set, in
108+
// which case it is slog.LevelDebug.
82109
func NewHandler(opts *Options) (*Handler, error) {
83110
h := &Handler{}
84111

@@ -87,8 +114,7 @@ func NewHandler(opts *Options) (*Handler, error) {
87114
}
88115

89116
if h.opts.Level == nil {
90-
// TODO: Implement a leveler that checks DEBUG_INVOCATION=1
91-
h.opts.Level = slog.LevelInfo
117+
h.opts.Level = &LevelVar{}
92118
}
93119

94120
w, err := newJournalWriter()
@@ -102,30 +128,19 @@ func NewHandler(opts *Options) (*Handler, error) {
102128

103129
}
104130

105-
// Enabled implements slog.Handler.
131+
// Enabled reports whether the handler handles records at the given level.
132+
// The handler ignores records whose level is lower.
133+
// It is called early, before any arguments are processed,
134+
// to save effort if the log event should be discarded.
106135
func (h *Handler) Enabled(_ context.Context, level slog.Level) bool {
107136
return level >= h.opts.Level.Level()
108137
}
109138

110139
var identifier = []byte(path.Base(os.Args[0]))
111140

112-
// Handle handles the Record.
113-
// It will only be called when Enabled returns true.
114-
// The Context argument is as for Enabled.
115-
// It is present solely to provide Handlers access to the context's values.
116-
// Canceling the context should not affect record processing.
117-
// (Among other things, log messages may be necessary to debug a
118-
// cancellation-related problem.)
119-
//
120-
// Handle methods that produce output should observe the following rules:
121-
// - If r.Time is the zero time, ignore the time.
122-
// - If r.PC is zero, ignore it.
123-
// - Attr's values should be resolved.
124-
// - If an Attr's key and value are both the zero value, ignore the Attr.
125-
// This can be tested with attr.Equal(Attr{}).
126-
// - If a group's key is empty, inline the group's Attrs.
127-
// - If a group has no Attrs (even if it has a non-empty key),
128-
// ignore it.
141+
// Handle handles the Record and formats it as a [journal message](https://systemd.io/JOURNAL_NATIVE_PROTOCOL/).
142+
// Journal only supports keys of the form ^[A-Z_][A-Z0-9_]*$.
143+
// Any other keys will be silently dropped.
129144
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
130145
buf := make([]byte, 0, 1024)
131146
buf = h.appendKV(buf, "MESSAGE", []byte(r.Message))
@@ -227,7 +242,8 @@ func (h *Handler) appendAttr(b []byte, prefix string, a slog.Attr) []byte {
227242
return b
228243
}
229244

230-
// WithAttrs implements slog.Handler.
245+
// WithAttrs returns a new Handler whose attributes consist of
246+
// both the receiver's attributes and the arguments.
231247
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
232248
h2 := *h
233249
pre := slices.Clone(h2.preformatted)
@@ -238,7 +254,8 @@ func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
238254
return &h2
239255
}
240256

241-
// WithGroup implements slog.Handler.
257+
// WithGroup returns a new Handler with the given group appended to
258+
// the receiver's existing groups.
242259
func (h *Handler) WithGroup(name string) slog.Handler {
243260
if name == "" {
244261
return h

journal_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,45 @@ func TestCanWriteMessageToSocket(t *testing.T) {
347347
})
348348

349349
}
350+
351+
func TestLevel(t *testing.T) {
352+
l := LevelVar{}
353+
if l.Level() != slog.LevelInfo {
354+
t.Error("expected LevelInfo")
355+
}
356+
357+
h, err := NewHandler(nil)
358+
if err != nil {
359+
t.Fatal(err)
360+
}
361+
if h.opts.Level.Level() != slog.LevelInfo {
362+
t.Error("expected LevelInfo")
363+
}
364+
365+
l = LevelVar{}
366+
os.Setenv("DEBUG_INVOCATION", "1")
367+
if l.Level() != slog.LevelDebug {
368+
t.Error("expected LevelDebug")
369+
}
370+
371+
h, err = NewHandler(nil)
372+
if err != nil {
373+
t.Fatal(err)
374+
}
375+
if h.opts.Level.Level() != slog.LevelDebug {
376+
t.Error("expected LevelDebug")
377+
}
378+
379+
h, err = NewHandler(&Options{Level: slog.LevelError})
380+
if err != nil {
381+
t.Fatal(err)
382+
}
383+
if h.opts.Level.Level() != slog.LevelError {
384+
t.Error("expected LevelError")
385+
}
386+
387+
if !h.Enabled(context.TODO(), slog.LevelError) {
388+
t.Error("expected true")
389+
}
390+
391+
}

0 commit comments

Comments
 (0)