Live video on the AT Protocol
1/*
2Package clog provides Context with logging metadata, as well as logging helper functions.
3*/
4package log
5
6import (
7 "context"
8 "flag"
9 "fmt"
10 "log/slog"
11 "os"
12 "path/filepath"
13 "runtime"
14 "strconv"
15 "time"
16
17 "github.com/golang/glog"
18 "github.com/lmittmann/tint"
19 "github.com/mattn/go-isatty"
20)
21
22// unique type to prevent assignment.
23type clogContextKeyType struct{}
24
25// singleton value to identify our logging metadata in context
26var clogContextKey = clogContextKeyType{}
27
28// unique type to prevent assignment.
29type clogDebugKeyType struct{}
30
31// singleton value to identify our debug cli flag
32var clogDebugKey = clogDebugKeyType{}
33
34var errorLogLevel glog.Level = 1
35var warnLogLevel glog.Level = 2
36var defaultLogLevel glog.Level = 3
37var debugLogLevel glog.Level = 4
38var traceLogLevel glog.Level = 9
39
40// basic type to represent logging container. logging context is immutable after
41// creation, so we don't have to worry about locking.
42type metadata [][]string
43
44func SetColorLogger(color string) {
45 w := os.Stderr
46 noColor := false
47 if color == "true" {
48 noColor = false
49 } else if color == "false" {
50 noColor = true
51 } else {
52 noColor = !isatty.IsTerminal(w.Fd())
53 }
54 // set global logger with custom options
55 slog.SetDefault(slog.New(
56 tint.NewHandler(w, &tint.Options{
57 Level: slog.LevelDebug,
58 TimeFormat: time.RFC3339,
59 NoColor: noColor,
60 }),
61 ))
62}
63
64func init() {
65 // set global logger with custom options
66 SetColorLogger("")
67
68 // Set default v level to 3; this is overridden in main() but is useful for tests
69 vFlag := flag.Lookup("v")
70 // nolint:errcheck
71 vFlag.Value.Set(fmt.Sprintf("%d", defaultLogLevel))
72}
73
74type VerboseLogger struct {
75 level glog.Level
76}
77
78// implementation of our logger aware of glog -v=[0-9] levels
79func V(level glog.Level) *VerboseLogger {
80 return &VerboseLogger{level: level}
81}
82
83func (m metadata) Map() map[string]string {
84 out := map[string]string{}
85 for _, pair := range m {
86 out[pair[0]] = pair[1]
87 }
88 return out
89}
90
91func (m metadata) Flat() []any {
92 out := []any{}
93 for _, pair := range m {
94 out = append(out, pair[0])
95 out = append(out, pair[1])
96 }
97 return out
98}
99
100// Return a new context, adding in the provided values to the logging metadata
101func WithLogValues(ctx context.Context, args ...string) context.Context {
102 oldMetadata, _ := ctx.Value(clogContextKey).(metadata)
103 // No previous logging found, set up a new map
104 if oldMetadata == nil {
105 oldMetadata = metadata{}
106 }
107 var newMetadata = metadata{}
108 for _, pair := range oldMetadata {
109 newMetadata = append(newMetadata, []string{pair[0], pair[1]})
110 }
111 for i := range args {
112 if i%2 == 0 {
113 continue
114 }
115 newKey := args[i-1]
116 newValue := args[i]
117 found := false
118 for _, pair := range newMetadata {
119 if pair[0] == newKey {
120 pair[1] = newValue
121 found = true
122 break
123 }
124 }
125 if !found {
126 newMetadata = append(newMetadata, []string{newKey, newValue})
127 }
128 }
129 return context.WithValue(ctx, clogContextKey, newMetadata)
130}
131
132// Return a new context, adding in the provided values to the logging metadata
133func WithDebugValue(ctx context.Context, debug map[string]map[string]int) context.Context {
134 return context.WithValue(ctx, clogDebugKey, debug)
135}
136
137// Actual log handler; the others have wrappers to properly handle stack depth
138func (v *VerboseLogger) log(ctx context.Context, message string, fn func(string, ...any), args ...any) {
139 // I want a compile time assertion for this... but short of that let's be REALLY ANNOYING
140 if len(args)%2 != 0 {
141 for range 6 {
142 fmt.Println("!!!!!!!!!!!!!!!! FOLLOWING LOG LINE HAS AN ODD NUMBER OF ARGUMENTS !!!!!!!!!!!!!!!!")
143 }
144 }
145 meta, metaOk := ctx.Value(clogContextKey).(metadata)
146 found := false
147 highestLevel := glog.Level(0)
148 debug, debugOk := ctx.Value(clogDebugKey).(map[string]map[string]int)
149
150 // debug is {"func": {"ToHLS": 3}, "file": {"gstreamer.go": 4}}
151 // meta is {"func": "ToHLS", "file": "gstreamer.go"}
152 // we want to use the highest level between debug and meta
153 if debugOk && metaOk {
154 for mk, mv := range meta.Map() {
155 debugValuesForMetaValue, ok := debug[mk]
156 if !ok {
157 continue
158 }
159 ll, ok := debugValuesForMetaValue[mv]
160 if !ok {
161 continue
162 }
163 if glog.Level(ll) > highestLevel {
164 found = true
165 highestLevel = glog.Level(ll)
166 } else {
167 }
168 }
169 }
170 if found {
171 if v.level > highestLevel {
172 return
173 }
174 } else {
175 if !glog.V(v.level) {
176 return
177 }
178 }
179
180 hasCaller := false
181
182 allArgs := []any{}
183 allArgs = append(allArgs, args...)
184 allArgs = append(allArgs, meta.Flat()...)
185 for i := range args {
186 if i%2 == 0 {
187 continue
188 }
189 if args[i-1] == "caller" {
190 hasCaller = true
191 }
192 }
193 if !hasCaller {
194 allArgs = append(allArgs, "caller", caller(3))
195 }
196
197 fn(message, allArgs...)
198}
199
200func (v *VerboseLogger) Log(ctx context.Context, message string, args ...any) {
201 if v.level >= 4 {
202 v.log(ctx, message, slog.Debug, args...)
203 } else {
204 v.log(ctx, message, slog.Info, args...)
205 }
206}
207
208func Error(ctx context.Context, message string, args ...any) {
209 V(errorLogLevel).log(ctx, message, slog.Error, args...)
210}
211
212func Warn(ctx context.Context, message string, args ...any) {
213 V(warnLogLevel).log(ctx, message, slog.Warn, args...)
214}
215
216func Log(ctx context.Context, message string, args ...any) {
217 V(defaultLogLevel).log(ctx, message, slog.Info, args...)
218}
219
220func Debug(ctx context.Context, message string, args ...any) {
221 V(debugLogLevel).log(ctx, message, slog.Debug, args...)
222}
223
224func Trace(ctx context.Context, message string, args ...any) {
225 V(traceLogLevel).log(ctx, message, slog.Debug, args...)
226}
227
228// returns true if we are at least the given level
229func Level(level glog.Level) glog.Verbose {
230 return glog.V(level)
231}
232
233// returns filenames relative to streamplace root
234// e.g. handlers/misttriggers/triggers.go:58
235func caller(depth int) string {
236 _, myfile, _, _ := runtime.Caller(0)
237 // This assumes that the root directory of streamplace is two levels above this folder.
238 // If that changes, please update this rootDir resolution.
239 rootDir := filepath.Join(filepath.Dir(myfile), "..", "..")
240 _, file, line, _ := runtime.Caller(depth)
241 rel, _ := filepath.Rel(rootDir, file)
242 return rel + ":" + strconv.Itoa(line)
243}