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  }