Live video on the AT Protocol
at eli/docker-linting 243 lines 6.1 kB view raw
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}