Skip to content

Instantly share code, notes, and snippets.

@ericmustin
Created April 3, 2023 05:44
Show Gist options
  • Save ericmustin/b3cf96f90c8606cba6770ae7e56961f8 to your computer and use it in GitHub Desktop.
Save ericmustin/b3cf96f90c8606cba6770ae7e56961f8 to your computer and use it in GitHub Desktop.
example ruby traces
gantt
dateFormat x
axisFormat %X:%L

title Trace 9a78cfeebd62a55a130068b0dbc01dc3

section Service emailservice
GET /return_order_deprecated :active, 1680500611600, 795ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 332b423dfe811503e71ef1f6684a12ce

section Service emailservice
GET /return_order_deprecated :active, 1680500614835, 36ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 9708bfdd6e382b7a22c5e984446b8bd9

section Service emailservice
GET /send_order_confirmation :active, 1680500603396, 7ms
send_email :done, 1680500603397, 6ms
sinatra.render_template :done, 1680500603397, 2ms
sinatra.render_template :done, 1680500603399, 0ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 0dda7a02463cb32f62553456afb498f9

section Service emailservice
GET /return_order :active, 1680500605679, 657ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 3e85c6e9a12c46d55ce1471f8169ae59

section Service emailservice
GET /send_order_confirmation :active, 1680500619706, 8ms
send_email :done, 1680500619707, 7ms
sinatra.render_template :done, 1680500619707, 2ms
sinatra.render_template :done, 1680500619710, 0ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 1af27fc535dc86aeb52482534f7af128

section Service emailservice
GET /send_order_confirmation :active, 1680500602521, 108ms
send_email :done, 1680500602527, 102ms
sinatra.render_template :done, 1680500602527, 8ms
sinatra.render_template :done, 1680500602536, 0ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 2903919cb509290067e697247af5154d

section Service emailservice
GET /return_order :active, 1680500616730, 554ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 38d7e98ed93d226644e7efea4b2117fd

section Service emailservice
GET /send_order_confirmation :active, 1680500619857, 5ms
send_email :done, 1680500619858, 4ms
sinatra.render_template :done, 1680500619858, 2ms
sinatra.render_template :done, 1680500619860, 0ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 1adc439e07186da0ac064df89b3a52df

section Service emailservice
GET /send_order_confirmation :active, 1680500620017, 5ms
send_email :done, 1680500620018, 4ms
sinatra.render_template :done, 1680500620018, 1ms
sinatra.render_template :done, 1680500620019, 0ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 6aafe333b98dddb1ee66a51a1da70f7f

section Service emailservice
GET /send_order_confirmation :active, 1680500603053, 5ms
send_email :done, 1680500603053, 4ms
sinatra.render_template :done, 1680500603053, 1ms
sinatra.render_template :done, 1680500603055, 0ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace 3322fb6e050d1a692fe5e467165d3483

section Service emailservice
GET /send_order_confirmation :active, 1680500603235, 7ms
send_email :done, 1680500603236, 6ms
sinatra.render_template :done, 1680500603236, 3ms
sinatra.render_template :done, 1680500603238, 0ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace b2ef8001a4743f87136a25f0688d1d13

section Service emailservice
GET /return_order_deprecated :active, 1680500613221, 52ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace c3fbf7d01a661171ba56d5c6ff30f9e9

section Service emailservice
GET /return_order_deprecated :active, 1680500614695, 110ms

Loading
gantt
dateFormat x
axisFormat %X:%L

title Trace d1e3b45f6def900f4cb06f5976d2c3a6

section Service emailservice
GET /send_order_confirmation :active, 1680500618205, 3ms
send_email :done, 1680500618205, 2ms
sinatra.render_template :done, 1680500618205, 1ms
sinatra.render_template :done, 1680500618206, 0ms

Loading
@ericmustin
Copy link
Author

ericmustin commented Apr 3, 2023

package main

import (
	"encoding/hex"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"text/template"
	"time"

	"go.opentelemetry.io/collector/pdata/pcommon"
	"go.opentelemetry.io/collector/pdata/ptrace"
)

const tmplStr = "```mermaid\n" + `gantt
dateFormat x
axisFormat %X:%L

title Trace {{.TraceId}}

{{range .ServiceSections -}}
section {{.Section}}
{{range .Spans -}}
{{.Name}} :{{if .Active}}active{{else}}done{{end}}{{if .Crit}} crit{{end}}, {{.StartUnix}}, {{.Duration}}ms
{{end}}
{{end -}}
` + "```\n"

type SpanTemplate struct {
	Name      string
	Active    bool
	Crit      bool
	StartUnix int64
	Duration  int64
	Service   string
}

type SpansByTrace struct {
	SpansWithResource []*SpanWithResourceAttrs
	ServiceSections   []*ServiceSection
	TraceId           string
}

type SpanWithResourceAttrs struct {
	Span          ptrace.Span
	ResourceAttrs pcommon.Map
}

type ServiceSection struct {
	Name    string
	Spans   []*SpanTemplate
	Section string
}

func jsonToTemplateData(jsonData []byte) (map[string]*SpansByTrace, error) {
	// Split input data into separate JSON objects
	objectStrings := strings.Split(string(jsonData), "\n")
	if len(objectStrings) == 0 {
		return nil, errors.New("input contains no JSON objects")
	}

	unmarshaller := &ptrace.JSONUnmarshaler{}

	spansByTid := make(map[string]*SpansByTrace)
	// Unmarshal each JSON object into a Span and store in a map
	for _, objString := range objectStrings {

		if objString == "" {
			continue
		}
		td, err := unmarshaller.UnmarshalTraces([]byte(objString))

		if err != nil {
			return nil, fmt.Errorf("failed to unmarshal JSON object: %v", err)
		}

		rss := td.ResourceSpans()

		for i := 0; i < rss.Len(); i++ {
			rs := rss.At(i)

			resource := rs.Resource()
			resourceAttributes := resource.Attributes()

			ilss := rs.ScopeSpans()

			for j := 0; j < ilss.Len(); j++ {
				ils := ilss.At(j)
				spanz := ils.Spans()

				for k := 0; k < spanz.Len(); k++ {
					span := spanz.At(k)

					var tid string
					if traceID := span.TraceID(); !traceID.IsEmpty() {
						tid = hex.EncodeToString(traceID[:])
					}
					spanWRA := &SpanWithResourceAttrs{
						Span:          span,
						ResourceAttrs: resourceAttributes,
					}
					if spansByTrace, ok := spansByTid[tid]; ok {
						spansByTrace.SpansWithResource = append(spansByTrace.SpansWithResource, spanWRA)
					} else {
						spansBT := &SpansByTrace{
							SpansWithResource: []*SpanWithResourceAttrs{spanWRA},
							TraceId:           tid,
						}
						spansByTid[tid] = spansBT
					}
				}
			}
		}
	}

	// ReOrder spans per trace based on startTime
	for _, spansByTrace := range spansByTid {
		sort.Slice(spansByTrace.SpansWithResource, func(i, j int) bool {
			return spansByTrace.SpansWithResource[i].Span.StartTimestamp().AsTime().Before(spansByTrace.SpansWithResource[j].Span.StartTimestamp().AsTime())
		})

		// now that its sorted, generate our template for the trace
		for _, spanWithResourceAttr := range spansByTrace.SpansWithResource {
			span := spanWithResourceAttr.Span

			template := SpanTemplate{
				Name:      span.Name(),
				Active:    isActive(span.Kind().String()),
				Crit:      isCrit(span.Status().Code().String()),
				StartUnix: int64(span.StartTimestamp()) / int64(time.Millisecond),
				Duration:  int64(span.EndTimestamp()-span.StartTimestamp()) / int64(time.Millisecond),
			}

			var serviceName string
			if attrVal, ok := spanWithResourceAttr.ResourceAttrs.Get("service.name"); ok {
				serviceName = attrVal.Str()
			} else {
				serviceName = "default_service"
			}

			if (len(spansByTrace.ServiceSections) == 0) ||
				(spansByTrace.ServiceSections[len(spansByTrace.ServiceSections)-1].Name != serviceName) {
				sectionTitle := fmt.Sprintf("Service %s", serviceName)
				spansByTrace.ServiceSections = append(spansByTrace.ServiceSections, &ServiceSection{
					Name:    serviceName,
					Spans:   []*SpanTemplate{&template},
					Section: sectionTitle,
				})
			} else {
				spansByTrace.ServiceSections[len(spansByTrace.ServiceSections)-1].Spans = append(
					spansByTrace.ServiceSections[len(spansByTrace.ServiceSections)-1].Spans,
					&template,
				)
			}
		}
	}

	return spansByTid, nil
}

func isActive(kind string) bool {
	switch kind {
	case "Client", "Server", "Producer", "Consumer":
		return true
	default:
		return false
	}
}

func isCrit(status string) bool {
	if strings.ToLower(status) == "error" {
		return true
	}
	return false
}

func generateMarkdown(inputFilePath string, outputDirPath string) error {
	inputFile, err := os.Open(inputFilePath)
	if err != nil {
		return err
	}
	defer inputFile.Close()

	jsonData, err := ioutil.ReadAll(inputFile)
	if err != nil {
		return err
	}

	spansByTrace, err := jsonToTemplateData(jsonData)
	if err != nil {
		return err
	}

	outputFileName := filepath.Base(inputFilePath) + ".md"
	outputFilePath := filepath.Join(outputDirPath, outputFileName)
	outputFile, err := os.Create(outputFilePath)
	if err != nil {
		return err
	}
	defer outputFile.Close()

	funcMap := template.FuncMap{
		"isActive": isActive,
		"isCrit":   isCrit,
	}
	tmpl, err := template.New("gantt").Funcs(funcMap).Parse(tmplStr)
	if err != nil {
		return err
	}

	for _, trace := range spansByTrace {
		err = tmpl.Execute(outputFile, trace)
		if err != nil {
			return err
		}
	}

	return nil
}

func main() {
	inputDirPath := "input"
	outputDirPath := "output"

	err := filepath.Walk(inputDirPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() && filepath.Ext(path) == ".json" {
			err = generateMarkdown(path, outputDirPath)
			if err != nil {
				fmt.Printf("Failed to generate markdown for file %s: %v", path, err)
			} else {
				fmt.Printf("Successfully generated markdown for file %s", path)
			}
		}
		return nil
	})
	if err != nil {
		fmt.Printf("Failed to walk input directory: %v", err)
	}
}

@mhausenblas
Copy link

Thanks for sharing! In Go 1.20 this doesn't compile for me with ./main.go:88:8: serviceName declared and not used. It's easy enough to fix, just wanted to let you know …

@ericmustin
Copy link
Author

@mhausenblas whoops! late night coding, updated :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment