github.com/yourbase/yb@v0.7.1/cmd/yb/output.go (about) 1 // Copyright 2021 YourBase Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // SPDX-License-Identifier: Apache-2.0 16 17 package main 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "io" 24 "os" 25 "strings" 26 "sync" 27 "time" 28 29 "github.com/yourbase/yb/internal/config" 30 "go.opentelemetry.io/otel/api/trace" 31 exporttrace "go.opentelemetry.io/otel/sdk/export/trace" 32 "zombiezen.com/go/log" 33 ) 34 35 const ( 36 shortTimeFormat = "15:04:05" 37 longTimeFormat = "15:04:05 MST" 38 ) 39 40 // setupLogPrefix is the log prefix used with withLogPrefix when running a 41 // target's setup. 42 const setupLogPrefix = ".deps" 43 44 type logger struct { 45 color termStyles 46 47 mu sync.Mutex 48 buf []byte 49 } 50 51 var logInitOnce sync.Once 52 53 func initLog(cfg config.Getter, showDebug bool) { 54 logInitOnce.Do(func() { 55 log.SetDefault(&log.LevelFilter{ 56 Min: configuredLogLevel(cfg, showDebug), 57 Output: &logger{ 58 color: termStylesFromEnv(), 59 }, 60 }) 61 }) 62 } 63 64 func (l *logger) Log(ctx context.Context, entry log.Entry) { 65 var out io.Writer = os.Stderr 66 if entry.Level < log.Warn { 67 ctxOut, _ := ctx.Value(logOutputKey{}).(io.Writer) 68 if ctxOut != nil { 69 out = ctxOut 70 } 71 } 72 var colorSequence string 73 switch { 74 case entry.Level < log.Info: 75 colorSequence = l.color.debug() 76 case entry.Level >= log.Error: 77 colorSequence = l.color.failure() 78 } 79 prefix, _ := ctx.Value(logPrefixKey{}).(string) 80 81 l.mu.Lock() 82 defer l.mu.Unlock() 83 84 l.buf = l.buf[:0] 85 l.buf = append(l.buf, colorSequence...) 86 for _, line := range strings.Split(strings.TrimSuffix(entry.Msg, "\n"), "\n") { 87 l.buf = appendLogPrefix(l.buf, entry.Time, prefix) 88 switch { 89 case entry.Level >= log.Error: 90 l.buf = append(l.buf, "❌ "...) 91 case entry.Level >= log.Warn: 92 l.buf = append(l.buf, "⚠️ "...) 93 } 94 l.buf = append(l.buf, line...) 95 l.buf = append(l.buf, '\n') 96 } 97 if colorSequence != "" { 98 l.buf = append(l.buf, l.color.reset()...) 99 } 100 101 out.Write(l.buf) 102 } 103 104 func (l *logger) LogEnabled(entry log.Entry) bool { 105 return true 106 } 107 108 func configuredLogLevel(cfg config.Getter, showDebug bool) log.Level { 109 if showDebug { 110 return log.Debug 111 } 112 l := config.Get(cfg, "defaults", "log-level") 113 switch strings.ToLower(l) { 114 case "debug": 115 return log.Debug 116 case "warn", "warning": 117 return log.Warn 118 case "error": 119 return log.Error 120 } 121 return log.Info 122 } 123 124 type logOutputKey struct{} 125 126 func withLogOutput(parent context.Context, w io.Writer) context.Context { 127 return context.WithValue(parent, logOutputKey{}, w) 128 } 129 130 type logPrefixKey struct{} 131 132 func withLogPrefix(parent context.Context, prefix string) context.Context { 133 parentPrefix, _ := parent.Value(logPrefixKey{}).(string) 134 return context.WithValue(parent, logPrefixKey{}, parentPrefix+prefix) 135 } 136 137 // linePrefixWriter prepends a timestamp and a prefix string to every line 138 // written to it and writes to an underlying writer. 139 type linePrefixWriter struct { 140 dst io.Writer 141 prefix string 142 buf []byte 143 wrote bool 144 now func() time.Time 145 } 146 147 func newLinePrefixWriter(w io.Writer, prefix string) *linePrefixWriter { 148 return &linePrefixWriter{ 149 dst: w, 150 prefix: prefix, 151 now: time.Now, 152 } 153 } 154 155 // Write writes p to the underlying writer, inserting line prefixes as required. 156 // Write will issue at most one Write call per line on the underlying writer. 157 func (lp *linePrefixWriter) Write(p []byte) (int, error) { 158 now := lp.now() 159 origLen := len(p) 160 for next := []byte(nil); len(p) > 0; p = next { 161 line := p 162 next = nil 163 lineEnd := bytes.IndexByte(p, '\n') 164 if lineEnd != -1 { 165 line, next = p[:lineEnd+1], p[lineEnd+1:] 166 } 167 if lp.wrote { 168 // Pass through rest of line if we already wrote the prefix. 169 n, err := lp.dst.Write(line) 170 lp.wrote = lineEnd == -1 171 if err != nil { 172 return origLen - (len(p) + n), err 173 } 174 continue 175 } 176 lp.buf = appendLogPrefix(lp.buf[:0], now, lp.prefix) 177 lp.buf = append(lp.buf, line...) 178 n, err := lp.dst.Write(lp.buf) 179 lp.wrote = n > 0 && (n < len(lp.buf) || lineEnd == -1) 180 if err != nil { 181 n -= len(lp.buf) - len(line) // exclude prefix length 182 return origLen - (len(p) + n), err 183 } 184 } 185 return origLen, nil 186 } 187 188 // appendLogPrefix formats the given timestamp and label and appends the result 189 // to dst. 190 func appendLogPrefix(dst []byte, t time.Time, prefix string) []byte { 191 if prefix == "" { 192 return dst 193 } 194 dst = t.AppendFormat(dst, shortTimeFormat) 195 buf := bytes.NewBuffer(dst) 196 fmt.Fprintf(buf, " %-18s| ", prefix) 197 return buf.Bytes() 198 } 199 200 // A traceSink records spans in memory. The zero value is an empty sink. 201 type traceSink struct { 202 mu sync.Mutex 203 rootSpans []*exporttrace.SpanData 204 children map[trace.SpanID][]*exporttrace.SpanData 205 } 206 207 // ExportSpan saves the trace span. It is safe to be called concurrently. 208 func (sink *traceSink) ExportSpan(_ context.Context, span *exporttrace.SpanData) { 209 sink.mu.Lock() 210 defer sink.mu.Unlock() 211 if !span.ParentSpanID.IsValid() { 212 sink.rootSpans = append(sink.rootSpans, span) 213 return 214 } 215 if sink.children == nil { 216 sink.children = make(map[trace.SpanID][]*exporttrace.SpanData) 217 } 218 sink.children[span.ParentSpanID] = append(sink.children[span.ParentSpanID], span) 219 } 220 221 const ( 222 traceDumpStartWidth = 14 223 traceDumpEndWidth = 14 224 traceDumpElapsedWidth = 14 225 ) 226 227 // dump formats the recorded traces as a hierarchial table of spans in the order 228 // received. It is safe to call concurrently, including with ExportSpan. 229 func (sink *traceSink) dump() string { 230 sb := new(strings.Builder) 231 fmt.Fprintf(sb, "%-*s %-*s %*s\n", 232 traceDumpStartWidth, "Start", 233 traceDumpEndWidth, "End", 234 traceDumpElapsedWidth, "Elapsed", 235 ) 236 sink.mu.Lock() 237 sink.dumpLocked(sb, trace.SpanID{}, 0) 238 sink.mu.Unlock() 239 return sb.String() 240 } 241 242 func (sink *traceSink) dumpLocked(sb *strings.Builder, parent trace.SpanID, depth int) { 243 const indent = " " 244 list := sink.rootSpans 245 if parent.IsValid() { 246 list = sink.children[parent] 247 } 248 if depth >= 3 { 249 if len(list) > 0 { 250 writeSpaces(sb, traceDumpStartWidth+traceDumpEndWidth+traceDumpElapsedWidth+3) 251 for i := 0; i < depth; i++ { 252 sb.WriteString(indent) 253 } 254 sb.WriteString("...\n") 255 } 256 return 257 } 258 for _, span := range list { 259 elapsed := span.EndTime.Sub(span.StartTime) 260 fmt.Fprintf(sb, "%-*s %-*s %*.3fs %s\n", 261 traceDumpStartWidth, span.StartTime.Format(longTimeFormat), 262 traceDumpEndWidth, span.EndTime.Format(longTimeFormat), 263 traceDumpElapsedWidth-1, elapsed.Seconds(), 264 strings.Repeat(indent, depth)+span.Name, 265 ) 266 sink.dumpLocked(sb, span.SpanContext.SpanID, depth+1) 267 } 268 } 269 270 func writeSpaces(w io.ByteWriter, n int) { 271 for i := 0; i < n; i++ { 272 w.WriteByte(' ') 273 } 274 }