File: structured_logger.go

package info (click to toggle)
snapd 2.72-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 80,412 kB
  • sloc: sh: 16,506; ansic: 16,211; python: 11,213; makefile: 1,919; exp: 190; awk: 58; xml: 22
file content (162 lines) | stat: -rw-r--r-- 4,286 bytes parent folder | download | duplicates (2)
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// -*- Mode: Go; indent-tabs-mode: t -*-
//go:build structuredlogging

/*
 * Copyright (C) 2025 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package logger

import (
	"context"
	"io"
	"log"
	"log/slog"
	"path/filepath"
	"runtime"
	"time"

	"github.com/snapcore/snapd/osutil"
)

type StructuredLog struct {
	log   *slog.Logger
	debug bool
	trace bool
	quiet bool
	flags int
}

const (
	levelTrace  = slog.Level(-8)
	levelNotice = slog.Level(2)
)

var levelNames = map[slog.Level]string{
	levelTrace:  "TRACE",
	levelNotice: "NOTICE",
}

func (l *StructuredLog) debugEnabled() bool {
	return l.debug || osutil.GetenvBool("SNAPD_DEBUG") || l.traceEnabled()
}

// Debug only prints if SNAPD_DEBUG or SNAPD_TRACE is set
func (l *StructuredLog) Debug(msg string) {
	if l.debugEnabled() {
		var pcs [1]uintptr
		runtime.Callers(3, pcs[:])
		r := slog.NewRecord(time.Now(), slog.LevelDebug, msg, pcs[0])
		l.log.Handler().Handle(context.Background(), r)
	}
}

// Notice alerts the user about something, as well as putting in syslog
func (l *StructuredLog) Notice(msg string) {
	if !l.quiet {
		var pcs [1]uintptr
		runtime.Callers(3, pcs[:])
		r := slog.NewRecord(time.Now(), levelNotice, msg, pcs[0])
		l.log.Handler().Handle(context.Background(), r)
	}
}

// NoGuardDebug always prints the message, w/o gating it based on environment
// variables or other configurations.
func (l *StructuredLog) NoGuardDebug(msg string) {
	var pcs [1]uintptr
	runtime.Callers(3, pcs[:])
	r := slog.NewRecord(time.Now(), slog.LevelDebug, msg, pcs[0])
	l.log.Handler().Handle(context.Background(), r)
}

func (l *StructuredLog) traceEnabled() bool {
	if l.trace {
		return true
	}
	if osutil.GetenvBool("SNAPD_TRACE") {
		l.trace = true
		return true
	}
	return false
}

// Trace only prints if SNAPD_TRACE is set and structured logging is active
func (l *StructuredLog) Trace(msg string, attrs ...any) {
	if l.traceEnabled() {
		var pcs [1]uintptr
		runtime.Callers(3, pcs[:])
		r := slog.NewRecord(time.Now(), levelTrace, msg, pcs[0])
		r.Add(attrs...)
		l.log.Handler().Handle(context.Background(), r)
	}
}

// // New creates a log.Logger using the given io.Writer and flag, using the
// // options from opts.
func New(w io.Writer, flag int, opts *LoggerOptions) Logger {
	if opts == nil {
		opts = &LoggerOptions{}
	}
	if !osutil.GetenvBool("SNAPD_JSON_LOGGING") {
		return newLog(w, flag, opts)
	}
	options := &slog.HandlerOptions{
		AddSource: true,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			// The simple logger uses the flag to determine what gets
			// added to the logs. slog uses attributes. To keep the
			// same functionality as with the simple log, here we check
			// the flags if the timestamp should be removed and if the
			// filename only should be considered instead of full path.
			if a.Key == slog.TimeKey && (flag&log.Ldate) != log.Ldate {
				// Remove timestamp
				return slog.Attr{}
			}
			if a.Key == slog.SourceKey && (flag&log.Lshortfile) == log.Lshortfile {
				// Remove all but the file name of the source file
				source, ok := a.Value.Any().(*slog.Source)
				if !ok {
					return a
				}
				if source != nil {
					source.File = filepath.Base(source.File)
				}
				return a
			}
			if a.Key == slog.LevelKey {
				// Add TRACE and NOTICE level names
				level, ok := a.Value.Any().(slog.Level)
				if !ok {
					return a
				}
				levelLabel, exists := levelNames[level]
				if !exists {
					levelLabel = level.String()
				}
				a.Value = slog.StringValue(levelLabel)
			}
			return a
		},
	}
	logger := &StructuredLog{
		log:   slog.New(slog.NewJSONHandler(w, options)),
		debug: opts.ForceDebug || debugEnabledOnKernelCmdline(),
		flags: flag,
		trace: false,
	}
	return logger
}