diff --git a/examples/template/template.js b/examples/template/template.js index c31818e..056c3e0 100644 --- a/examples/template/template.js +++ b/examples/template/template.js @@ -21,7 +21,9 @@ const client = new tracing.Client({ const traceDefaults = { attributeSemantics: tracing.SEMANTICS_HTTP, attributes: {"one": "three"}, - randomAttributes: {count: 2, cardinality: 5} + randomAttributes: {count: 2, cardinality: 5}, + eventDefaults: {generateExceptionOnError: true, count: 1.0, randomAttributes: {count: 2, cardinality: 3}}, + linkDefaults: {linkToPreviousSpanIndex: true, count: 1.0, randomAttributes: {count: 2, cardinality: 3}}, } const traceTemplates = [ @@ -63,6 +65,16 @@ const traceTemplates = [ {service: "auth-service", name: "authenticate", attributes: {"http.status_code": 403}}, ] }, + { + defaults: traceDefaults, + spans: [ + {service: "shop-backend", attributes: {"http.status_code": 403}}, + {service: "shop-backend", name: "authenticate", attributes: {"http.request.header.accept": ["application/json"]}}, + {service: "auth-service", name: "authenticate", attributes: {"http.status_code": 403}}, + {service: "test-random", name: "events", randomEvents: {exceptionCount: 1, count: 2, randomAttributes: {count: 5, cardinality: 2}}}, + {service: "test-random", name: "links", randomLinks: {linkToPreviousSpanIndex: true, count: 2, randomAttributes: {count: 3, cardinality: 2}}} + ] + }, ] export default function () { diff --git a/pkg/tracegen/templated.go b/pkg/tracegen/templated.go index 403c488..90b663e 100644 --- a/pkg/tracegen/templated.go +++ b/pkg/tracegen/templated.go @@ -36,7 +36,7 @@ type Range struct { // AttributeParams describe how random attributes should be created. type AttributeParams struct { - // Count the number of attributes to creat. + // Count the number of attributes to create. Count int // Cardinality how many distinct values are created for each attribute. Cardinality *int @@ -50,6 +50,10 @@ type SpanDefaults struct { Attributes map[string]interface{} `js:"attributes"` // RandomAttributes random attributes generated for each span. RandomAttributes *AttributeParams `js:"randomAttributes"` + // Random events generated for each span + EventDefaults EventDefaults `js:"eventDefaults"` + // Random links generated for each span + LinkDefaults LinkDefaults `js:"linkDefaults"` } // SpanTemplate parameters that define how a span is created. @@ -72,6 +76,14 @@ type SpanTemplate struct { // RandomAttributes parameters to configure the creation of random attributes. If missing, no random attributes // are added to the span. RandomAttributes *AttributeParams `js:"randomAttributes"` + // List of events for the span with specific parameters + Events []Event `js:"events"` + // List of links for the span with specific parameters + Links []Link `js:"links"` + // Generate random events for the span + RandomEvents RandomEvents `js:"randomEvents"` + // Generate random links for the span + RandomLinks RandomLinks `js:"randomLinks"` } // TraceTemplate describes how all a trace and it's spans are generated. @@ -82,6 +94,73 @@ type TraceTemplate struct { Spans []SpanTemplate `js:"spans"` } +type Link struct { + // LinkToPreviousSpanIndex true will set TraceID and SpanID the same as the previous span + LinkToPreviousSpanIndex bool `js:"linkToPreviousSpanIndex"` + // Attributes for this link + Attributes map[string]interface{} `js:"attributes"` + // Generate random attributes for this link + RandomAttributes *AttributeParams `js:"randomAttributes"` +} + +type Event struct { + // Name of event + Name string `js:"name"` + // Attributes for this event + Attributes map[string]interface{} `js:"attributes"` + // Generate random attributes for this event + RandomAttributes *AttributeParams `js:"randomAttributes"` +} + +type LinkDefaults struct { + // LinkToPreviousSpanIndex true will set TraceID and SpanID the same as the previous span + // Unless it is the first span then a random TraceID and a random SpanID will be used + LinkToPreviousSpanIndex bool `js:"linkToPreviousSpanIndex"` + // Rate of random links per each span + Rate float32 `js:"rate"` + // Generate random attributes for this link + RandomAttributes *AttributeParams `js:"randomAttributes"` +} + +type EventDefaults struct { + // Generate exception event if status code of the span is >= 400 + GenerateExceptionOnError bool `js:"generateExceptionOnError"` + // Rate of random events per each span + Rate float32 `js:"rate"` + // Generate random attributes for this event + RandomAttributes *AttributeParams `js:"randomAttributes"` +} + +type RandomEvents struct { + // Number of random exception events to generate for the span + ExceptionCount int `js:"exceptionCount"` + // Number of non-exception events to generate for the span + Count int `js:"count"` + // Generate random attributes for these events + RandomAttributes *AttributeParams `js:"randomAttributes"` +} + +type RandomLinks struct { + // LinkToPreviousSpanIndex true will set TraceID and SpanID the same as the previous span + LinkToPreviousSpanIndex bool `js:"linkToPreviousSpanIndex"` + // Number of links to generate for the span + Count int `js:"count"` + // Generate random attributes for these links + RandomAttributes *AttributeParams `js:"randomAttributes"` +} + +type internalLinkDefaults struct { + LinkToPreviousSpanIndex bool + Count int + RandomAttributes *AttributeParams +} + +type internalEventDefaults struct { + GenerateExceptionOnError bool + Count int + RandomAttributes *AttributeParams +} + // NewTemplatedGenerator creates a new trace generator. func NewTemplatedGenerator(template *TraceTemplate) (*TemplatedGenerator, error) { gen := &TemplatedGenerator{} @@ -102,15 +181,18 @@ type TemplatedGenerator struct { } type internalSpanTemplate struct { - idx int - resource *internalResourceTemplate - parent *internalSpanTemplate - name string - kind ptrace.SpanKind - duration *Range - attributeSemantics *OTelSemantics - attributes map[string]interface{} - randomAttributes map[string][]interface{} + idx int + resource *internalResourceTemplate + parent *internalSpanTemplate + name string + kind ptrace.SpanKind + duration *Range + attributeSemantics *OTelSemantics + attributes map[string]interface{} + randomAttributes map[string][]interface{} + events []Event + links []Link + generateExceptionEvents bool } type internalResourceTemplate struct { @@ -226,6 +308,58 @@ func (g *TemplatedGenerator) generateSpan(scopeSpans ptrace.ScopeSpans, tmpl *in } } + // events + + span.Events().EnsureCapacity(len(tmpl.events)) + for _, e := range tmpl.events { + event := span.Events().AppendEmpty() + event.SetName(e.Name) + event.SetTimestamp(pcommon.NewTimestampFromTime(end)) + for k, v := range e.Attributes { + _ = event.Attributes().PutEmpty(k).FromRaw(v) + } + } + + if tmpl.generateExceptionEvents { + var status int64 + parentAttr := pcommon.NewMap() + if parent != nil { + parentAttr = parent.Attributes() + } + if st, found := span.Attributes().Get("http.status_code"); found { + status = st.Int() + } else if st, found = parentAttr.Get("http.status_code"); found { + status = st.Int() + } else { + status = random.HTTPStatusSuccess() + span.Attributes().PutInt("http.status_code", status) + } + if status >= 400 { + exceptionEvent := generateExceptionEvent() + event := span.Events().AppendEmpty() + event.SetName(exceptionEvent.Name) + for k, v := range exceptionEvent.Attributes { + _ = event.Attributes().PutEmpty(k).FromRaw(v) + } + } + } + + // links + span.Links().EnsureCapacity(len(tmpl.links)) + for _, l := range tmpl.links { + link := span.Links().AppendEmpty() + if l.LinkToPreviousSpanIndex && parent != nil { + link.SetTraceID(traceID) + link.SetSpanID(parent.SpanID()) + } else { + link.SetTraceID(random.TraceID()) + link.SetSpanID(random.SpanID()) + } + for k, v := range l.Attributes { + _ = link.Attributes().PutEmpty(k).FromRaw(v) + } + } + return span } @@ -407,6 +541,49 @@ func (g *TemplatedGenerator) initializeSpan(idx int, parent *internalSpanTemplat } span.attributes = util.MergeMaps(defaults.Attributes, tmpl.Attributes) + eventDefaultsRate := defaults.EventDefaults.Rate + var eventDefaults internalEventDefaults + // if rate is more than 1, use whole integers + if eventDefaultsRate > 1 { + eventDefaults = internalEventDefaults{ + GenerateExceptionOnError: defaults.EventDefaults.GenerateExceptionOnError, + RandomAttributes: defaults.EventDefaults.RandomAttributes, + Count: int(eventDefaultsRate), + } + } else if idx%int(1/eventDefaultsRate) == 0 { + // if rate is less than one + eventDefaults = internalEventDefaults{ + GenerateExceptionOnError: defaults.EventDefaults.GenerateExceptionOnError, + RandomAttributes: defaults.EventDefaults.RandomAttributes, + Count: 1, + } + } + + // generate all non-exception events + span.events = g.initializeEvents(tmpl.Events, tmpl.RandomEvents, eventDefaults) + + // need span status to determine if an exception event should occur + span.generateExceptionEvents = defaults.EventDefaults.GenerateExceptionOnError + + linkDefaultsRate := defaults.LinkDefaults.Rate + var linkDefaults internalLinkDefaults + if linkDefaultsRate > 1 { + linkDefaults = internalLinkDefaults{ + LinkToPreviousSpanIndex: defaults.LinkDefaults.LinkToPreviousSpanIndex, + RandomAttributes: defaults.LinkDefaults.RandomAttributes, + Count: int(linkDefaultsRate), + } + } else if idx%int(1/linkDefaultsRate) == 0 { + linkDefaults = internalLinkDefaults{ + LinkToPreviousSpanIndex: defaults.LinkDefaults.LinkToPreviousSpanIndex, + RandomAttributes: defaults.LinkDefaults.RandomAttributes, + Count: 1, + } + } + + // initialize all links but need + span.links = g.initializeLinks(tmpl.Links, tmpl.RandomLinks, linkDefaults) + // set span name if tmpl.Name != nil { span.name = *tmpl.Name @@ -501,3 +678,128 @@ func initializeRandomAttributes(attributeParams *AttributeParams) map[string][]i return attributes } + +func (g *TemplatedGenerator) initializeEvents(tmplEvents []Event, randomEvents RandomEvents, eventDefaults internalEventDefaults) []Event { + count := len(tmplEvents) + randomEvents.Count + eventDefaults.Count + + if count == 0 { + return []Event{} + } + + events := make([]Event, 0, count) + + for _, e := range tmplEvents { + event := generateEvent(e.Name, e.Attributes, e.RandomAttributes) + events = append(events, event) + } + + for i := 0; i < randomEvents.ExceptionCount; i++ { + event := generateExceptionEvent() + events = append(events, event) + } + + for i := 0; i < randomEvents.Count; i++ { + event := generateEvent("", nil, randomEvents.RandomAttributes) + events = append(events, event) + } + + for i := 0; i < eventDefaults.Count; i++ { + event := generateEvent("", nil, eventDefaults.RandomAttributes) + events = append(events, event) + } + + return events +} + +func generateExceptionEvent() Event { + return Event{ + Name: "exception", + Attributes: map[string]interface{}{ + "exception.escape": false, + "exception.message": generateRandomExceptionMsg(), + "exception.stacktrace": generateRandomExceptionStackTrace(), + "exception.type": random.K6String(10) + ".error", + }, + } +} + +func generateEvent(name string, attributes map[string]interface{}, randomAttr *AttributeParams) Event { + event := Event{ + Attributes: make(map[string]interface{}), + } + + if name != "" { + event.Name = name + } else { + event.Name = "event " + random.K6String(10) + } + + for k, v := range attributes { + event.Attributes[k] = v + } + + for k, v := range initializeRandomAttributes(randomAttr) { + event.Attributes[k] = random.SelectElement(v) + } + return event +} + +func generateRandomExceptionMsg() string { + return "error: " + random.K6String(20) +} + +func generateRandomExceptionStackTrace() string { + var ( + panics = []string{"runtime error: index out of range", "runtime error: can't divide by 0"} + functions = []string{"main.main()", "trace.makespan()", "account.login()", "payment.collect()"} + ) + + return "panic: " + random.SelectElement(panics) + "\n" + random.SelectElement(functions) +} + +func (g *TemplatedGenerator) initializeLinks(tmplLinks []Link, randomLinks RandomLinks, linkDefaults internalLinkDefaults) []Link { + count := len(tmplLinks) + randomLinks.Count + linkDefaults.Count + + if count == 0 { + return []Link{} + } + + links := make([]Link, 0, count) + newLink := func(linkToPar bool) *Link { + return &Link{ + LinkToPreviousSpanIndex: linkToPar, + Attributes: make(map[string]interface{}), + } + } + + for _, l := range tmplLinks { + link := newLink(l.LinkToPreviousSpanIndex) + + for k, v := range l.Attributes { + link.Attributes[k] = v + } + + for k, v := range initializeRandomAttributes(l.RandomAttributes) { + link.Attributes[k] = random.SelectElement(v) + } + links = append(links, *link) + } + + for i := 0; i < randomLinks.Count; i++ { + link := newLink(randomLinks.LinkToPreviousSpanIndex) + for k, v := range initializeRandomAttributes(randomLinks.RandomAttributes) { + link.Attributes[k] = random.SelectElement(v) + } + links = append(links, *link) + } + + for i := 0; i < linkDefaults.Count; i++ { + link := newLink(linkDefaults.LinkToPreviousSpanIndex) + for k, v := range initializeRandomAttributes(linkDefaults.RandomAttributes) { + link.Attributes[k] = random.SelectElement(v) + } + links = append(links, *link) + } + + return links +} diff --git a/pkg/tracegen/templated_test.go b/pkg/tracegen/templated_test.go index cc1e95a..617b69f 100644 --- a/pkg/tracegen/templated_test.go +++ b/pkg/tracegen/templated_test.go @@ -52,6 +52,99 @@ func TestTemplatedGenerator_Traces(t *testing.T) { } } +func TestTemplatedGenerator_EventsLinks(t *testing.T) { + attributeSemantics := []OTelSemantics{SemanticsHTTP} + template := TraceTemplate{ + Defaults: SpanDefaults{ + Attributes: map[string]interface{}{"fixed.attr": "some-value"}, + RandomAttributes: &AttributeParams{Count: 3}, + LinkDefaults: LinkDefaults{LinkToPreviousSpanIndex: true, Rate: 0.5, RandomAttributes: &AttributeParams{Count: 3}}, + EventDefaults: EventDefaults{GenerateExceptionOnError: true, Rate: 0.5, RandomAttributes: &AttributeParams{Count: 3}}, + }, + Spans: []SpanTemplate{ + // do not change order of the first one + {Service: "test-service", Name: ptr("only_default")}, + {Service: "test-service", Name: ptr("default_and_template"), Events: []Event{{Name: "event-name", RandomAttributes: &AttributeParams{Count: 2}}}, Links: []Link{{LinkToPreviousSpanIndex: true, Attributes: map[string]interface{}{"link-attr-key": "link-attr-value"}}}}, + {Service: "test-service", Name: ptr("default_and_random"), RandomEvents: RandomEvents{Count: 2, RandomAttributes: &AttributeParams{Count: 1}}, RandomLinks: RandomLinks{LinkToPreviousSpanIndex: false, Count: 2, RandomAttributes: &AttributeParams{Count: 1}}}, + {Service: "test-service", Name: ptr("default_template_random"), Events: []Event{{Name: "event-name", RandomAttributes: &AttributeParams{Count: 2}}}, Links: []Link{{LinkToPreviousSpanIndex: true, Attributes: map[string]interface{}{"link-attr-key": "link-attr-value"}}}, RandomEvents: RandomEvents{Count: 2, RandomAttributes: &AttributeParams{Count: 1}}, RandomLinks: RandomLinks{LinkToPreviousSpanIndex: false, Count: 2, RandomAttributes: &AttributeParams{Count: 1}}}, + {Service: "test-service", Name: ptr("default_generate_on_error"), Attributes: map[string]interface{}{"http.status_code": 400}}, + }, + } + + for _, semantics := range attributeSemantics { + template.Defaults.AttributeSemantics = &semantics + gen, err := NewTemplatedGenerator(&template) + assert.NoError(t, err) + + for i := 0; i < testRounds; i++ { + traces := gen.Traces() + spans := collectSpansFromTrace(traces) + + assert.Len(t, spans, len(template.Spans)) + for i, span := range spans { + events := span.Events() + links := span.Links() + checkEventsLinksLength := func(spanIndex, expectedTemplate, expectedRandom int, spanName string) { + expected := expectedTemplate + expectedRandom + // because default rate is 0.5 + if spanIndex%2 == 0 { + assert.Equal(t, expected+1, events.Len(), "test name: %s events", spanName) + assert.Equal(t, expected+1, links.Len(), "test name: %s links", spanName) + } else { + assert.Equal(t, expected, events.Len(), "test name: %s events", spanName) + assert.Equal(t, expected, links.Len(), "test name: %s links", spanName) + } + } + + switch span.Name() { + case "only_default": + checkEventsLinksLength(i, 0, 0, span.Name()) + if i%2 == 0 { + // check default event with 3 random attributes + event := events.At(0) + assert.Equal(t, 3, len(event.Attributes().AsRaw())) + + // check default link with 3 random attributes + // and not matching trace id and parent span id because this is + // the first span, there is no previous span + link := links.At(0) + assert.Equal(t, 3, len(link.Attributes().AsRaw())) + assert.NotEqual(t, span.TraceID().String(), link.TraceID()) + assert.NotEqual(t, span.ParentSpanID(), link.SpanID()) + } + case "default_and_template": + checkEventsLinksLength(i, 1, 0, span.Name()) + case "default_and_random": + checkEventsLinksLength(i, 0, 2, span.Name()) + case "default_template_random": + checkEventsLinksLength(i, 1, 2, span.Name()) + case "default_generate_on_error": + // # events and links should not match in this scenario + if i%2 == 0 { + assert.Equal(t, 2, events.Len()) + assert.Equal(t, 1, links.Len()) + } else { + assert.Equal(t, 1, events.Len()) + assert.Equal(t, 0, links.Len()) + } + found := false + for i := 0; i < events.Len(); i++ { + event := events.At(i) + if event.Name() == "exception" { + found = true + assert.NotNil(t, event.Attributes().AsRaw()["exception.escape"]) + assert.NotNil(t, event.Attributes().AsRaw()["exception.message"]) + assert.NotNil(t, event.Attributes().AsRaw()["exception.stacktrace"]) + assert.NotNil(t, event.Attributes().AsRaw()["exception.type"]) + } + } + assert.True(t, found, "exception event not found") + } + } + } + } +} + func attributesWithPrefix(span ptrace.Span, prefix string) int { var count int span.Attributes().Range(func(k string, _ pcommon.Value) bool {