Live video on the AT Protocol
at eli/fix-context-recursion 295 lines 9.0 kB view raw
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 "time" 18 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 TimeFormat: time.RFC3339, 70 NoColor: noColor, 71 }), 72 )) 73} 74 75func init() { 76 r, w, err := os.Pipe() 77 if err != nil { 78 panic(err) 79 } 80 realStderr := os.Stderr 81 os.Stderr = w 82 go func() { 83 scanner := bufio.NewScanner(r) 84 for scanner.Scan() { 85 match := logLineRegex.FindStringSubmatch(scanner.Text()) 86 if len(match) == 0 { 87 fmt.Fprintf(realStderr, "%s\n", scanner.Text()) 88 continue 89 } 90 level := match[1] 91 caller := match[2] 92 message := match[3] 93 if level == "I" { 94 Log(context.Background(), message, "caller", caller) 95 } else if level == "W" { 96 Warn(context.Background(), message) 97 } else if level == "E" { 98 Error(context.Background(), message) 99 } else if level == "F" { 100 Warn(context.Background(), message) 101 } else { 102 fmt.Fprintf(realStderr, "UNKNOWN LOG LEVEL: %s\n", level) 103 } 104 } 105 if err := scanner.Err(); err != nil { 106 fmt.Fprintf(realStderr, "LOG: Error reading pipe: %v\n", err) 107 } 108 109 }() 110 // set global logger with custom options 111 SetColorLogger("") 112 113 // Set default v level to 3; this is overridden in main() but is useful for tests 114 vFlag := flag.Lookup("v") 115 // nolint:errcheck 116 vFlag.Value.Set(fmt.Sprintf("%d", defaultLogLevel)) 117} 118 119type VerboseLogger struct { 120 level glog.Level 121} 122 123// implementation of our logger aware of glog -v=[0-9] levels 124func V(level glog.Level) *VerboseLogger { 125 return &VerboseLogger{level: level} 126} 127 128func (m metadata) Map() map[string]string { 129 out := map[string]string{} 130 for _, pair := range m { 131 out[pair[0]] = pair[1] 132 } 133 return out 134} 135 136func (m metadata) Flat() []any { 137 out := []any{} 138 for _, pair := range m { 139 out = append(out, pair[0]) 140 out = append(out, pair[1]) 141 } 142 return out 143} 144 145// Return a new context, adding in the provided values to the logging metadata 146func WithLogValues(ctx context.Context, args ...string) context.Context { 147 oldMetadata, _ := ctx.Value(clogContextKey).(metadata) 148 // No previous logging found, set up a new map 149 if oldMetadata == nil { 150 oldMetadata = metadata{} 151 } 152 var newMetadata = metadata{} 153 for _, pair := range oldMetadata { 154 newMetadata = append(newMetadata, []string{pair[0], pair[1]}) 155 } 156 for i := range args { 157 if i%2 == 0 { 158 continue 159 } 160 newKey := args[i-1] 161 newValue := args[i] 162 found := false 163 for _, pair := range newMetadata { 164 if pair[0] == newKey { 165 pair[1] = newValue 166 found = true 167 break 168 } 169 } 170 if !found { 171 newMetadata = append(newMetadata, []string{newKey, newValue}) 172 } 173 } 174 return context.WithValue(ctx, clogContextKey, newMetadata) 175} 176 177// Return a new context, adding in the provided values to the logging metadata 178func WithDebugValue(ctx context.Context, debug map[string]map[string]int) context.Context { 179 return context.WithValue(ctx, clogDebugKey, debug) 180} 181 182// Actual log handler; the others have wrappers to properly handle stack depth 183func (v *VerboseLogger) log(ctx context.Context, message string, fn func(string, ...any), args ...any) { 184 // I want a compile time assertion for this... but short of that let's be REALLY ANNOYING 185 if len(args)%2 != 0 { 186 for range 6 { 187 fmt.Println("!!!!!!!!!!!!!!!! FOLLOWING LOG LINE HAS AN ODD NUMBER OF ARGUMENTS !!!!!!!!!!!!!!!!") 188 } 189 } 190 meta, metaOk := ctx.Value(clogContextKey).(metadata) 191 found := false 192 highestLevel := glog.Level(0) 193 debug, debugOk := ctx.Value(clogDebugKey).(map[string]map[string]int) 194 195 // debug is {"func": {"ToHLS": 3}, "file": {"gstreamer.go": 4}} 196 // meta is {"func": "ToHLS", "file": "gstreamer.go"} 197 // we want to use the highest level between debug and meta 198 if debugOk && metaOk { 199 for mk, mv := range meta.Map() { 200 debugValuesForMetaValue, ok := debug[mk] 201 if !ok { 202 continue 203 } 204 ll, ok := debugValuesForMetaValue[mv] 205 if !ok { 206 continue 207 } 208 if glog.Level(ll) > highestLevel { 209 found = true 210 highestLevel = glog.Level(ll) 211 } 212 } 213 } 214 if found { 215 if v.level > highestLevel { 216 return 217 } 218 } else { 219 if !glog.V(v.level) { 220 return 221 } 222 } 223 224 hasCaller := false 225 226 allArgs := []any{} 227 allArgs = append(allArgs, args...) 228 allArgs = append(allArgs, meta.Flat()...) 229 for i := range args { 230 if i%2 == 0 { 231 continue 232 } 233 if args[i-1] == "caller" { 234 hasCaller = true 235 } 236 } 237 if !hasCaller { 238 allArgs = append(allArgs, "caller", caller(3)) 239 } 240 241 fn(message, allArgs...) 242} 243 244func (v *VerboseLogger) Log(ctx context.Context, message string, args ...any) { 245 if v.level >= 4 { 246 v.log(ctx, message, slog.Debug, args...) 247 } else { 248 v.log(ctx, message, slog.Info, args...) 249 } 250} 251 252func Error(ctx context.Context, message string, args ...any) { 253 V(errorLogLevel).log(ctx, message, slog.Error, args...) 254} 255 256func Warn(ctx context.Context, message string, args ...any) { 257 V(warnLogLevel).log(ctx, message, slog.Warn, args...) 258} 259 260func Log(ctx context.Context, message string, args ...any) { 261 V(defaultLogLevel).log(ctx, message, slog.Info, args...) 262} 263 264func Debug(ctx context.Context, message string, args ...any) { 265 V(debugLogLevel).log(ctx, message, slog.Debug, args...) 266} 267 268func Trace(ctx context.Context, message string, args ...any) { 269 V(traceLogLevel).log(ctx, message, slog.Debug, args...) 270} 271 272// returns true if we are at least the given level 273func Level(level glog.Level) glog.Verbose { 274 return glog.V(level) 275} 276 277func GetValue(ctx context.Context, key string) string { 278 meta, metaOk := ctx.Value(clogContextKey).(metadata) 279 if !metaOk { 280 return "" 281 } 282 return meta.Map()[key] 283} 284 285// returns filenames relative to streamplace root 286// e.g. handlers/misttriggers/triggers.go:58 287func caller(depth int) string { 288 _, myfile, _, _ := runtime.Caller(0) 289 // This assumes that the root directory of streamplace is two levels above this folder. 290 // If that changes, please update this rootDir resolution. 291 rootDir := filepath.Join(filepath.Dir(myfile), "..", "..") 292 _, file, line, _ := runtime.Caller(depth) 293 rel, _ := filepath.Rel(rootDir, file) 294 return rel + ":" + strconv.Itoa(line) 295}