mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
* feat: Add Strict flag to LoadSpecs API Strict flag which can be used to toggle between true (raising errors if a document is invalid) and false (ignoring invalid documents, perhaps logging a warning). * Granular error handling multidocs in secrets and configmaps * Fix failing test
274 lines
7.3 KiB
Go
274 lines
7.3 KiB
Go
package traces
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/sdk/trace"
|
|
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
|
"golang.org/x/text/language"
|
|
"golang.org/x/text/message"
|
|
)
|
|
|
|
var (
|
|
_ trace.SpanExporter = (*Exporter)(nil)
|
|
once sync.Once
|
|
exporter *Exporter
|
|
printer = message.NewPrinter(language.English)
|
|
)
|
|
|
|
const legend = "Succeeded (S), eXcluded (X), Failed (F)\n"
|
|
|
|
// FUTURE WORK: This exporter should only be used by troubleshoot CLIs
|
|
// until the following issue is addressed:
|
|
// 1. The cache of spans grows infinitely at the moment. This is OK for short lived
|
|
// invocations such as CLI applications running one-shot commands. For long running
|
|
// applications, this will be a problem.
|
|
// * We can drop non-troubleshoot spans from the cache. There is an OTEP to add
|
|
// "inheritable attributes" to spans which is unsupported as of today. Once/if
|
|
// it gets accepted and implemented, it will allow us to add a "troubleshoot"
|
|
// attribute to the "root" span which gets propagated to downstream spans which
|
|
// we can use to filter out non-troubleshoot spans.
|
|
// https://github.com/dynatrace-oss-contrib/oteps/blob/feature/context-scoped-attributes/text/0207-context-scoped-attributes.md
|
|
// https://github.com/open-telemetry/opentelemetry-specification/issues/1337
|
|
// * We can "search" for the "root" span by traversing up the span tree and drop
|
|
// all spans that are not descendants of the "root" span. This can be slow, but
|
|
// we can do it in a background goroutine (adds a bit of complexity).
|
|
// 2. At the moment, the summary of an execution is the only case this exporter
|
|
// is being used for.
|
|
|
|
// GetExporterInstance creates a singleton exporter instance
|
|
func GetExporterInstance() *Exporter {
|
|
once.Do(func() {
|
|
exporter = &Exporter{
|
|
allSpans: make([]trace.ReadOnlySpan, 1024),
|
|
}
|
|
})
|
|
return exporter
|
|
}
|
|
|
|
// Exporter is an implementation of trace.SpanExporter that writes to a destination.
|
|
type Exporter struct {
|
|
spansMu sync.Mutex
|
|
allSpans []trace.ReadOnlySpan
|
|
|
|
stoppedMu sync.RWMutex
|
|
stopped bool
|
|
}
|
|
|
|
// ExportSpans writes spans to an in-memory cache
|
|
// This function can/will be called on every span.End() at worst.
|
|
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
|
|
// This is a no-op if the context is canceled.
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
|
|
e.stoppedMu.RLock()
|
|
stopped := e.stopped
|
|
e.stoppedMu.RUnlock()
|
|
if stopped {
|
|
return nil
|
|
}
|
|
|
|
if len(spans) == 0 {
|
|
return nil
|
|
}
|
|
|
|
e.spansMu.Lock()
|
|
defer e.spansMu.Unlock()
|
|
|
|
// Cache received span updates allSpans
|
|
e.allSpans = append(e.allSpans, spans...)
|
|
|
|
return nil
|
|
}
|
|
|
|
func isType(stub *tracetest.SpanStub, t string) bool {
|
|
if stub == nil {
|
|
return false
|
|
}
|
|
|
|
for _, attr := range stub.Attributes {
|
|
if string(attr.Key) == "type" && strings.Contains(attr.Value.AsString(), t) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func formatStubName(stub *tracetest.SpanStub) string {
|
|
if stub == nil {
|
|
return ""
|
|
}
|
|
|
|
for _, attr := range stub.Attributes {
|
|
if stub.Status.Code == codes.Error {
|
|
return stub.Name + " (F)"
|
|
}
|
|
if string(attr.Key) == constants.EXCLUDED && attr.Value.AsBool() {
|
|
return stub.Name + " (X)"
|
|
}
|
|
}
|
|
return stub.Name + " (S)" // Assume success
|
|
}
|
|
|
|
// maxInt returns the larger of x or y.
|
|
func maxInt(x, y int) int {
|
|
if x < y {
|
|
return y
|
|
}
|
|
return x
|
|
}
|
|
|
|
// GetSummary returns the runtime summary of the execution
|
|
// so far. Call this function after your "root" span has ended
|
|
// and the program operations needing tracing have completed.
|
|
func (e *Exporter) GetSummary() string {
|
|
e.spansMu.Lock()
|
|
stubs := tracetest.SpanStubsFromReadOnlySpans(e.allSpans)
|
|
e.spansMu.Unlock()
|
|
|
|
// No spans to log
|
|
if len(stubs) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// TODO: We may want to collect more information about the
|
|
// execution of the program. For example, we can collect
|
|
// the number of times a collector was executed, whether
|
|
// it was successful or not, if it was skipped, etc.
|
|
collectors := make(map[string]time.Duration)
|
|
redactors := make(map[string]time.Duration)
|
|
analysers := make(map[string]time.Duration)
|
|
|
|
totalDuration := time.Duration(0)
|
|
|
|
for i := range stubs {
|
|
stub := &stubs[i]
|
|
|
|
// Summary of span stubs
|
|
duration := stub.EndTime.Sub(stub.StartTime)
|
|
stubName := formatStubName(stub)
|
|
switch {
|
|
case stub.Name == constants.TROUBLESHOOT_ROOT_SPAN_NAME:
|
|
totalDuration = duration
|
|
case isType(stub, "Collect"):
|
|
collectors[stubName] = duration
|
|
case isType(stub, "Redactors"):
|
|
redactors[stub.Name] = duration
|
|
case isType(stub, "Analyze"):
|
|
analysers[stubName] = duration
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
|
|
sb := strings.Builder{}
|
|
|
|
collectorsSummary(collectors, &sb)
|
|
redactorsSummary(redactors, &sb)
|
|
analysersSummary(analysers, &sb)
|
|
sb.WriteString(printer.Sprintf("\nDuration: %dms\n", totalDuration/time.Millisecond))
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// summary of collector runtimes
|
|
func collectorsSummary(summary map[string]time.Duration, sb *strings.Builder) {
|
|
padding, keys := sortedKeysAndPadding(summary)
|
|
|
|
sb.WriteString("============ Collectors summary =============\n")
|
|
sb.WriteString(legend)
|
|
sb.WriteString("=============================================\n")
|
|
if len(summary) == 0 {
|
|
sb.WriteString("No collectors executed\n")
|
|
return
|
|
}
|
|
|
|
for _, name := range keys {
|
|
sb.WriteString(printer.Sprintf("%-*s : %dms\n", padding, name, summary[name]/time.Millisecond))
|
|
}
|
|
}
|
|
|
|
// summary of redactor runtime
|
|
func redactorsSummary(summary map[string]time.Duration, sb *strings.Builder) {
|
|
padding, keys := sortedKeysAndPadding(summary)
|
|
|
|
sb.WriteString("\n============ Redactors summary =============\n")
|
|
if len(summary) == 0 {
|
|
sb.WriteString("No redactors executed\n")
|
|
return
|
|
}
|
|
|
|
for _, name := range keys {
|
|
sb.WriteString(printer.Sprintf("%-*s : %dms\n", padding, name, summary[name]/time.Millisecond))
|
|
}
|
|
}
|
|
|
|
// summary of analyser runtime
|
|
func analysersSummary(summary map[string]time.Duration, sb *strings.Builder) {
|
|
padding, keys := sortedKeysAndPadding(summary)
|
|
|
|
sb.WriteString("\n============= Analyzers summary =============\n")
|
|
sb.WriteString(legend)
|
|
sb.WriteString("=============================================\n")
|
|
if len(summary) == 0 {
|
|
sb.WriteString("No analyzers executed\n")
|
|
return
|
|
}
|
|
|
|
for _, name := range keys {
|
|
sb.WriteString(printer.Sprintf("%-*s : %dms\n", padding, name, summary[name]/time.Millisecond))
|
|
}
|
|
}
|
|
|
|
func sortedKeysAndPadding(summary map[string]time.Duration) (int, []string) {
|
|
keys := make([]string, 0, len(summary))
|
|
padding := 0
|
|
for k := range summary {
|
|
padding = maxInt(padding, len(k))
|
|
keys = append(keys, k)
|
|
}
|
|
sort.SliceStable(keys, func(l, r int) bool {
|
|
return summary[keys[l]] > summary[keys[r]]
|
|
})
|
|
return padding, keys
|
|
}
|
|
|
|
// Shutdown is called to stop the exporter, it preforms no action.
|
|
func (e *Exporter) Shutdown(ctx context.Context) error {
|
|
e.stoppedMu.Lock()
|
|
e.stopped = true
|
|
e.stoppedMu.Unlock()
|
|
|
|
e.Reset()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *Exporter) Reset() {
|
|
e.spansMu.Lock()
|
|
e.allSpans = e.allSpans[:0] // clear the slice
|
|
e.spansMu.Unlock()
|
|
}
|
|
|
|
// MarshalLog is the marshaling function used by the logging system to represent this exporter.
|
|
func (e *Exporter) MarshalLog() interface{} {
|
|
return struct {
|
|
Type string
|
|
}{
|
|
Type: "troubleshoot",
|
|
}
|
|
}
|