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 "bufio"
8 "context"
9 "flag"
10 "fmt"
11 "log/slog"
12 "os"
13 "path/filepath"
14 "regexp"
15 "runtime"
16 "strconv"
17
18 "github.com/bluesky-social/indigo/util"
19 "github.com/golang/glog"
20 "github.com/lmittmann/tint"
21 "github.com/mattn/go-isatty"
22)
23
24// unique type to prevent assignment.
25type clogContextKeyType struct{}
26
27// singleton value to identify our logging metadata in context
28var clogContextKey = clogContextKeyType{}
29
30// unique type to prevent assignment.
31type clogDebugKeyType struct{}
32
33// singleton value to identify our debug cli flag
34var clogDebugKey = clogDebugKeyType{}
35
36var errorLogLevel glog.Level = 1
37var warnLogLevel glog.Level = 2
38var defaultLogLevel glog.Level = 3
39var debugLogLevel glog.Level = 4
40var traceLogLevel glog.Level = 9
41
42// basic type to represent logging container. logging context is immutable after
43// creation, so we don't have to worry about locking.
44type metadata [][]string
45
46// LOG: I0809 17:23:01.775710 255904 broadcast.go:1162] manifestID=didplcdkh4rwafdcda4ko7lewe43ml-84zq1aff-4ren nonce=9899027318953397582 seqNo=2 clientIP=127.0.0.1 Trying to transcode segment using sessions=1
47// LOG: I0809 17:23:01.777792 255904 segment_rpc.go:551] manifestID=didplcdkh4rwafdcda4ko7lewe43ml-84zq1aff-4ren nonce=9899027318953397582 seqNo=2 orchSessionID=cdc9e3b9 orchestrator=https://127.0.0.1:9001 clientIP=127.0.0.1 Submitting segment bytes=1017268 orch=https://127.0.0.1:9001 timeout=8s uploadTimeout=2s segDur=0.832
48// LOG: I0809 17:23:01.778444 255904 discovery.go:211] manifestID=didplcdkh4rwafdcda4ko7lewe43ml-84zq1aff-4ren nonce=9899027318953397582 clientIP=127.0.0.1 Done fetching orch info numOrch=1 responses=1/1 timedOut=false
49// LOG: I0809 17:23:01.782914 255904 segment_rpc.go:587] manifestID=didplcdkh4rwafdcda4ko7lewe43ml-84zq1aff-4ren nonce=9899027318953397582 seqNo=2 orchSessionID=cdc9e3b9 orchestrator=https://127.0.0.1:9001 clientIP=127.0.0.1 Uploaded segment orch=https://127.0.0.1:9001 dur=5.106541ms
50// LOG: I0809 17:23:01.892735 255904 segment_rpc.go:668] manifestID=didplcdkh4rwafdcda4ko7lewe43ml-84zq1aff-4ren nonce=9899027318953397582 seqNo=2 orchSessionID=cdc9e3b9 orchestrator=https://127.0.0.1:9001 clientIP=127.0.0.1 Successfully transcoded segment segName= seqNo=2 orch=https://127.0.0.1:9001 dur=109.762457ms
51// LOG: I0809 17:23:01.893821 255904 mediaserver.go:1046] manifestID=didplcdkh4rwafdcda4ko7lewe43ml-84zq1aff-4ren nonce=9899027318953397582 seqNo=2 orchSessionID=cdc9e3b9 orchestrator=https://127.0.0.1:9001 clientIP=127.0.0.1 Finished transcoding push request at url=http://127.0.0.1:8935/live/didplcdkh4rwafdcda4ko7lewe43ml-84zq1aff-4ren/2.ts took=160.894832ms
52
53var realStderr = os.Stderr
54var logLineRegex = regexp.MustCompile(`^([IWEF])\d+ \d{2}:\d{2}:\d{2}\.\d{6}\s+\d+\s+([^:]+:\d+)\]\s*(.*)$`)
55
56func SetColorLogger(color string) {
57 noColor := false
58 if color == "true" {
59 noColor = false
60 } else if color == "false" {
61 noColor = true
62 } else {
63 noColor = !isatty.IsTerminal(realStderr.Fd())
64 }
65 // set global logger with custom options
66 slog.SetDefault(slog.New(
67 tint.NewHandler(realStderr, &tint.Options{
68 Level: slog.LevelDebug,
69 ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
70 if a.Key != "time" {
71 return a
72 }
73 t := a.Value.Time().UTC()
74 return slog.Attr{
75 Key: "time",
76 Value: slog.TimeValue(t),
77 }
78 },
79 TimeFormat: util.ISO8601,
80 NoColor: noColor,
81 }),
82 ))
83}
84
85func MonkeypatchStderr() {
86 r, w, err := os.Pipe()
87 if err != nil {
88 panic(err)
89 }
90 realStderr := os.Stderr
91 os.Stderr = w
92 ctx := WithLogValues(context.Background(), "component", "livepeer")
93 go func() {
94 scanner := bufio.NewScanner(r)
95 for scanner.Scan() {
96 match := logLineRegex.FindStringSubmatch(scanner.Text())
97 if len(match) == 0 {
98 fmt.Fprintf(realStderr, "%s\n", scanner.Text())
99 continue
100 }
101 level := match[1]
102 caller := match[2]
103 message := match[3]
104 if level == "I" {
105 Log(ctx, message, "caller", caller)
106 } else if level == "W" {
107 Warn(ctx, message)
108 } else if level == "E" {
109 Error(ctx, message)
110 } else if level == "F" {
111 Warn(ctx, message)
112 } else {
113 fmt.Fprintf(realStderr, "UNKNOWN LOG LEVEL: %s %s\n", level, message)
114 }
115 }
116 if err := scanner.Err(); err != nil {
117 fmt.Fprintf(realStderr, "LOG: Error reading pipe: %v\n", err)
118 }
119
120 }()
121 // set global logger with custom options
122 SetColorLogger("")
123
124 // Set default v level to 3; this is overridden in main() but is useful for tests
125 vFlag := flag.Lookup("v")
126 // nolint:errcheck
127 vFlag.Value.Set(fmt.Sprintf("%d", defaultLogLevel))
128}
129
130type VerboseLogger struct {
131 level glog.Level
132}
133
134// implementation of our logger aware of glog -v=[0-9] levels
135func V(level glog.Level) *VerboseLogger {
136 return &VerboseLogger{level: level}
137}
138
139func (m metadata) Map() map[string]string {
140 out := map[string]string{}
141 for _, pair := range m {
142 out[pair[0]] = pair[1]
143 }
144 return out
145}
146
147func (m metadata) Flat() []any {
148 out := []any{}
149 for _, pair := range m {
150 out = append(out, pair[0])
151 out = append(out, pair[1])
152 }
153 return out
154}
155
156// Return a new context, adding in the provided values to the logging metadata
157func WithLogValues(ctx context.Context, args ...string) context.Context {
158 oldMetadata, _ := ctx.Value(clogContextKey).(metadata)
159 // No previous logging found, set up a new map
160 if oldMetadata == nil {
161 oldMetadata = metadata{}
162 }
163 var newMetadata = metadata{}
164 for _, pair := range oldMetadata {
165 newMetadata = append(newMetadata, []string{pair[0], pair[1]})
166 }
167 for i := range args {
168 if i%2 == 0 {
169 continue
170 }
171 newKey := args[i-1]
172 newValue := args[i]
173 found := false
174 for _, pair := range newMetadata {
175 if pair[0] == newKey {
176 pair[1] = newValue
177 found = true
178 break
179 }
180 }
181 if !found {
182 newMetadata = append(newMetadata, []string{newKey, newValue})
183 }
184 }
185 return context.WithValue(ctx, clogContextKey, newMetadata)
186}
187
188// Return a new context, adding in the provided values to the logging metadata
189func WithDebugValue(ctx context.Context, debug map[string]map[string]int) context.Context {
190 return context.WithValue(ctx, clogDebugKey, debug)
191}
192
193// Actual log handler; the others have wrappers to properly handle stack depth
194func (v *VerboseLogger) log(ctx context.Context, message string, fn func(string, ...any), args ...any) {
195 // I want a compile time assertion for this... but short of that let's be REALLY ANNOYING
196 if len(args)%2 != 0 {
197 for range 6 {
198 fmt.Println("!!!!!!!!!!!!!!!! FOLLOWING LOG LINE HAS AN ODD NUMBER OF ARGUMENTS !!!!!!!!!!!!!!!!")
199 }
200 }
201 meta, metaOk := ctx.Value(clogContextKey).(metadata)
202 found := false
203 highestLevel := glog.Level(0)
204 debug, debugOk := ctx.Value(clogDebugKey).(map[string]map[string]int)
205
206 // debug is {"func": {"ToHLS": 3}, "file": {"gstreamer.go": 4}}
207 // meta is {"func": "ToHLS", "file": "gstreamer.go"}
208 // we want to use the highest level between debug and meta
209 if debugOk && metaOk {
210 for mk, mv := range meta.Map() {
211 debugValuesForMetaValue, ok := debug[mk]
212 if !ok {
213 continue
214 }
215 ll, ok := debugValuesForMetaValue[mv]
216 if !ok {
217 continue
218 }
219 if glog.Level(ll) > highestLevel {
220 found = true
221 highestLevel = glog.Level(ll)
222 }
223 }
224 }
225 if found {
226 if v.level > highestLevel {
227 return
228 }
229 } else {
230 if !glog.V(v.level) {
231 return
232 }
233 }
234
235 hasCaller := false
236
237 allArgs := []any{}
238 allArgs = append(allArgs, args...)
239 allArgs = append(allArgs, meta.Flat()...)
240 for i := range args {
241 if i%2 == 0 {
242 continue
243 }
244 if args[i-1] == "caller" {
245 hasCaller = true
246 }
247 }
248 if !hasCaller {
249 allArgs = append(allArgs, "caller", caller(3))
250 }
251
252 fn(message, allArgs...)
253}
254
255func (v *VerboseLogger) Log(ctx context.Context, message string, args ...any) {
256 if v.level >= 4 {
257 v.log(ctx, message, slog.Debug, args...)
258 } else {
259 v.log(ctx, message, slog.Info, args...)
260 }
261}
262
263func Error(ctx context.Context, message string, args ...any) {
264 V(errorLogLevel).log(ctx, message, slog.Error, args...)
265}
266
267func Warn(ctx context.Context, message string, args ...any) {
268 V(warnLogLevel).log(ctx, message, slog.Warn, args...)
269}
270
271func Log(ctx context.Context, message string, args ...any) {
272 V(defaultLogLevel).log(ctx, message, slog.Info, args...)
273}
274
275func Debug(ctx context.Context, message string, args ...any) {
276 V(debugLogLevel).log(ctx, message, slog.Info, args...)
277}
278
279func Trace(ctx context.Context, message string, args ...any) {
280 V(traceLogLevel).log(ctx, message, slog.Info, args...)
281}
282
283// returns true if we are at least the given level
284func Level(level glog.Level) glog.Verbose {
285 return glog.V(level)
286}
287
288func GetValue(ctx context.Context, key string) string {
289 meta, metaOk := ctx.Value(clogContextKey).(metadata)
290 if !metaOk {
291 return ""
292 }
293 return meta.Map()[key]
294}
295
296// returns filenames relative to streamplace root
297// e.g. handlers/misttriggers/triggers.go:58
298func caller(depth int) string {
299 _, myfile, _, _ := runtime.Caller(0)
300 // This assumes that the root directory of streamplace is two levels above this folder.
301 // If that changes, please update this rootDir resolution.
302 rootDir := filepath.Join(filepath.Dir(myfile), "..", "..")
303 _, file, line, _ := runtime.Caller(depth)
304 rel, _ := filepath.Rel(rootDir, file)
305 return rel + ":" + strconv.Itoa(line)
306}