349 lines
7.8 KiB
Go
349 lines
7.8 KiB
Go
package diagnose
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel/codes"
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
const (
|
|
status_unknown = "[ ] "
|
|
status_ok = "\u001b[32m[ ok ]\u001b[0m "
|
|
status_failed = "\u001b[31m[failed]\u001b[0m "
|
|
status_warn = "\u001b[33m[ warn ]\u001b[0m "
|
|
status_skipped = "\u001b[90m[ skip ]\u001b[0m "
|
|
same_line = "\x0d"
|
|
ErrorStatus = 2
|
|
WarningStatus = 1
|
|
OkStatus = 0
|
|
SkippedStatus = -1
|
|
)
|
|
|
|
var errUnimplemented = errors.New("unimplemented")
|
|
|
|
type status int
|
|
|
|
func (s status) String() string {
|
|
switch s {
|
|
case OkStatus:
|
|
return "ok"
|
|
case WarningStatus:
|
|
return "warn"
|
|
case ErrorStatus:
|
|
return "fail"
|
|
}
|
|
return "invalid"
|
|
}
|
|
|
|
func (s status) MarshalJSON() ([]byte, error) {
|
|
return []byte(fmt.Sprint("\"", s.String(), "\"")), nil
|
|
}
|
|
|
|
type Result struct {
|
|
Time time.Time `json:"time"`
|
|
Name string `json:"name"`
|
|
Status status `json:"status"`
|
|
Warnings []string `json:"warnings,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
Children []*Result `json:"children,omitempty"`
|
|
}
|
|
|
|
func (r *Result) finalize() status {
|
|
maxStatus := r.Status
|
|
if len(r.Children) > 0 {
|
|
sort.SliceStable(r.Children, func(i, j int) bool {
|
|
return r.Children[i].Time.Before(r.Children[j].Time)
|
|
})
|
|
for _, c := range r.Children {
|
|
cms := c.finalize()
|
|
if cms > maxStatus {
|
|
maxStatus = cms
|
|
}
|
|
}
|
|
if maxStatus > r.Status {
|
|
r.Status = maxStatus
|
|
}
|
|
}
|
|
return maxStatus
|
|
}
|
|
|
|
func (r *Result) ZeroTimes() {
|
|
var zero time.Time
|
|
r.Time = zero
|
|
for _, c := range r.Children {
|
|
c.ZeroTimes()
|
|
}
|
|
}
|
|
|
|
// TelemetryCollector is an otel SpanProcessor that gathers spans and once the outermost
|
|
// span ends, walks the otel traces in order to produce a top-down tree of Diagnose results.
|
|
type TelemetryCollector struct {
|
|
ui io.Writer
|
|
spans map[trace.SpanID]sdktrace.ReadOnlySpan
|
|
rootSpan sdktrace.ReadOnlySpan
|
|
results map[trace.SpanID]*Result
|
|
RootResult *Result
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewTelemetryCollector creates a SpanProcessor that collects OpenTelemetry spans
|
|
// and aggregates them into a tree structure for use by Diagnose.
|
|
// It also outputs the status of main sections to that writer.
|
|
func NewTelemetryCollector(w io.Writer) *TelemetryCollector {
|
|
return &TelemetryCollector{
|
|
ui: w,
|
|
spans: make(map[trace.SpanID]sdktrace.ReadOnlySpan),
|
|
results: make(map[trace.SpanID]*Result),
|
|
}
|
|
}
|
|
|
|
// OnStart tracks spans by id for later retrieval
|
|
func (t *TelemetryCollector) OnStart(_ context.Context, s sdktrace.ReadWriteSpan) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.spans[s.SpanContext().SpanID()] = s
|
|
if isMainSection(s) {
|
|
fmt.Fprintf(t.ui, status_unknown+s.Name())
|
|
}
|
|
}
|
|
|
|
func isMainSection(s sdktrace.ReadOnlySpan) bool {
|
|
for _, a := range s.Attributes() {
|
|
if a.Key == "diagnose" && a.Value.AsString() == "main-section" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (t *TelemetryCollector) OnEnd(e sdktrace.ReadOnlySpan) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if !e.Parent().HasSpanID() {
|
|
// First walk the span structs to construct the top down tree results we want
|
|
for _, s := range t.spans {
|
|
r := t.getOrBuildResult(s.SpanContext().SpanID())
|
|
if r != nil {
|
|
if s.Parent().HasSpanID() {
|
|
p := t.getOrBuildResult(s.Parent().SpanID())
|
|
if p != nil {
|
|
p.Children = append(p.Children, r)
|
|
}
|
|
} else {
|
|
t.RootResult = r
|
|
}
|
|
}
|
|
}
|
|
|
|
// Then walk the results sorting children by time
|
|
t.RootResult.finalize()
|
|
} else if isMainSection(e) {
|
|
r := t.getOrBuildResult(e.SpanContext().SpanID())
|
|
if r != nil {
|
|
fmt.Print(same_line)
|
|
fmt.Fprintln(t.ui, r.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// required to implement SpanProcessor, but noops for our purposes
|
|
func (t *TelemetryCollector) Shutdown(_ context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
// required to implement SpanProcessor, but noops for our purposes
|
|
func (t *TelemetryCollector) ForceFlush(_ context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (t *TelemetryCollector) getOrBuildResult(id trace.SpanID) *Result {
|
|
s := t.spans[id]
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
r, ok := t.results[id]
|
|
if !ok {
|
|
r = &Result{
|
|
Name: s.Name(),
|
|
Message: s.StatusMessage(),
|
|
Time: s.StartTime(),
|
|
}
|
|
for _, e := range s.Events() {
|
|
switch e.Name {
|
|
case warningEventName:
|
|
for _, a := range e.Attributes {
|
|
if a.Key == messageKey {
|
|
r.Warnings = append(r.Warnings, a.Value.AsString())
|
|
}
|
|
}
|
|
case skippedEventName:
|
|
r.Status = SkippedStatus
|
|
case "fail":
|
|
var message string
|
|
var action string
|
|
for _, a := range e.Attributes {
|
|
switch a.Key {
|
|
case actionKey:
|
|
action = a.Value.AsString()
|
|
case errorMessageKey:
|
|
message = a.Value.AsString()
|
|
}
|
|
}
|
|
if message != "" && action != "" {
|
|
r.Children = append(r.Children, &Result{
|
|
Name: action,
|
|
Status: ErrorStatus,
|
|
Message: message,
|
|
})
|
|
|
|
}
|
|
case spotCheckOkEventName:
|
|
var checkName string
|
|
var message string
|
|
for _, a := range e.Attributes {
|
|
switch a.Key {
|
|
case nameKey:
|
|
checkName = a.Value.AsString()
|
|
case messageKey:
|
|
message = a.Value.AsString()
|
|
}
|
|
}
|
|
if checkName != "" {
|
|
r.Children = append(r.Children,
|
|
&Result{
|
|
Name: checkName,
|
|
Status: OkStatus,
|
|
Message: message,
|
|
Time: e.Time,
|
|
})
|
|
}
|
|
case spotCheckWarnEventName:
|
|
var checkName string
|
|
var message string
|
|
for _, a := range e.Attributes {
|
|
switch a.Key {
|
|
case nameKey:
|
|
checkName = a.Value.AsString()
|
|
case messageKey:
|
|
message = a.Value.AsString()
|
|
}
|
|
}
|
|
if checkName != "" {
|
|
r.Children = append(r.Children,
|
|
&Result{
|
|
Name: checkName,
|
|
Status: WarningStatus,
|
|
Message: message,
|
|
Time: e.Time,
|
|
})
|
|
}
|
|
case spotCheckErrorEventName:
|
|
var checkName string
|
|
var message string
|
|
for _, a := range e.Attributes {
|
|
switch a.Key {
|
|
case nameKey:
|
|
checkName = a.Value.AsString()
|
|
case messageKey:
|
|
message = a.Value.AsString()
|
|
}
|
|
}
|
|
if checkName != "" {
|
|
r.Children = append(r.Children,
|
|
&Result{
|
|
Name: checkName,
|
|
Status: ErrorStatus,
|
|
Message: message,
|
|
Time: e.Time,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
switch s.StatusCode() {
|
|
case codes.Unset:
|
|
if len(r.Warnings) > 0 {
|
|
r.Status = WarningStatus
|
|
} else if r.Status != SkippedStatus {
|
|
r.Status = OkStatus
|
|
}
|
|
case codes.Ok:
|
|
if r.Status != SkippedStatus {
|
|
r.Status = OkStatus
|
|
}
|
|
case codes.Error:
|
|
r.Status = ErrorStatus
|
|
}
|
|
t.results[id] = r
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Write outputs a human readable version of the results tree
|
|
func (r *Result) Write(writer io.Writer) error {
|
|
var sb strings.Builder
|
|
r.write(&sb, 0)
|
|
_, err := writer.Write([]byte(sb.String()))
|
|
return err
|
|
}
|
|
|
|
func (r *Result) write(sb *strings.Builder, depth int) {
|
|
for i := 0; i < depth; i++ {
|
|
sb.WriteString(" ")
|
|
}
|
|
sb.WriteString(r.String())
|
|
sb.WriteRune('\n')
|
|
for _, c := range r.Children {
|
|
c.write(sb, depth+1)
|
|
}
|
|
}
|
|
|
|
func (r *Result) String() string {
|
|
var sb strings.Builder
|
|
if len(r.Warnings) == 0 {
|
|
switch r.Status {
|
|
case OkStatus:
|
|
sb.WriteString(status_ok)
|
|
case WarningStatus:
|
|
sb.WriteString(status_warn)
|
|
case ErrorStatus:
|
|
sb.WriteString(status_failed)
|
|
case SkippedStatus:
|
|
sb.WriteString(status_skipped)
|
|
}
|
|
sb.WriteString(r.Name)
|
|
|
|
if r.Message != "" || len(r.Warnings) > 0 {
|
|
sb.WriteString(": ")
|
|
}
|
|
sb.WriteString(r.Message)
|
|
}
|
|
warnings := r.Warnings
|
|
if r.Message == "" && len(warnings) > 0 {
|
|
sb.WriteString(status_warn)
|
|
sb.WriteString(r.Name)
|
|
sb.WriteString(": ")
|
|
sb.WriteString(warnings[0])
|
|
|
|
warnings = warnings[1:]
|
|
}
|
|
for _, w := range warnings {
|
|
sb.WriteRune('\n')
|
|
//TODO: Indentation
|
|
sb.WriteString(status_warn)
|
|
sb.WriteString(r.Name)
|
|
sb.WriteString(": ")
|
|
sb.WriteString(w)
|
|
}
|
|
return sb.String()
|
|
|
|
}
|