Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ The template has the following schema:
// The number of distinct values to generate for each attribute (optional, default: 50)
cardinality: int
}
// Default resource attributes for all resources in the trace (optional)
resource: {
// Fixed attributs that are added to each resource (optional)
attributes: { string : any },
// Parameters to configure the creation of random resource attributes (optional)
randomAttributes: {
// The number of random attributes to generate
count: int,
// The number of distinct values to generate for each attribute (optional, default: 50)
cardinality: int
}
}
},
// Templates for the individual spans
spans: [
Expand Down Expand Up @@ -129,6 +141,19 @@ The template has the following schema:
count: int,
// The number of distinct values to generate for each attribute (optional, default: 50)
cardinality: int
},
// Additional attributes for the resource associated with this span. Resource attribute definitions
// of different spans with the same service name will me merged into a singe resource (optional)
resource: {
// Fixed attributs that are added to the resource (optional)
attributes: { string : any },
// Parameters to configure the creation of random resource attributes (optional)
randomAttributes: {
// The number of random attributes to generate
count: int,
// The number of distinct values to generate for each attribute (optional, default: 50)
cardinality: int
}
}
},
...
Expand Down
12 changes: 7 additions & 5 deletions examples/template/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,25 @@ const traceDefaults = {
attributes: {"one": "three"},
randomAttributes: {count: 2, cardinality: 5},
randomEvents: {count: 0.1, exceptionCount: 0.2, randomAttributes: {count: 6, cardinality: 20}},
resource: { randomAttributes: {count: 3} },
}

const traceTemplates = [
{
defaults: traceDefaults,
spans: [
{service: "shop-backend", name: "list-articles", duration: {min: 200, max: 900}},
{service: "shop-backend", name: "authenticate", duration: {min: 50, max: 100}},
{service: "auth-service", name: "authenticate"},
{service: "shop-backend", name: "list-articles", duration: {min: 200, max: 900}, resource: { attributes: {"namespace": "shop"} }},
{service: "shop-backend", name: "authenticate", duration: {min: 50, max: 100}, resource: { randomAttributes: {count: 4} }},
{service: "auth-service", name: "authenticate", resource: { randomAttributes: {count: 2}, attributes: {"namespace": "auth"} }},
{service: "shop-backend", name: "fetch-articles", parentIdx: 0},
{
service: "article-service",
name: "list-articles",
links: [{attributes: {"link-type": "parent-child"}, randomAttributes: {count: 2, cardinality: 5}}]
links: [{attributes: {"link-type": "parent-child"}, randomAttributes: {count: 2, cardinality: 5}}],
resource: { attributes: {"namespace": "shop" }}
},
{service: "article-service", name: "select-articles", attributeSemantics: tracing.SEMANTICS_DB},
{service: "postgres", name: "query-articles", attributeSemantics: tracing.SEMANTICS_DB, randomAttributes: {count: 5}},
{service: "postgres", name: "query-articles", attributeSemantics: tracing.SEMANTICS_DB, randomAttributes: {count: 5}, resource: { attributes: {"namespace": "db"} }},
]
},
{
Expand Down
68 changes: 59 additions & 9 deletions pkg/tracegen/templated.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type SpanDefaults struct {
RandomEvents *EventParams `js:"randomEvents"`
// Random links generated for each span
RandomLinks *LinkParams `js:"randomLinks"`
// Resource controls the default attributes for all resources.
Resource *ResourceTemplate `js:"resource"`
}

// SpanTemplate parameters that define how a span is created.
Expand Down Expand Up @@ -85,6 +87,17 @@ type SpanTemplate struct {
RandomEvents *EventParams `js:"randomEvents"`
// Generate random links for the span
RandomLinks *LinkParams `js:"randomLinks"`
// Resource controls the attributes generated for the resource. Spans with the same Service will have the same
// resource. Multiple resource definitions will be merged.
Resource *ResourceTemplate `js:"resource"`
}

type ResourceTemplate struct {
// Attributes that are added to this resource.
Attributes map[string]interface{} `js:"attributes"`
// RandomAttributes parameters to configure the creation of random attributes. If missing, no random attributes
// are added to the resource.
RandomAttributes *AttributeParams `js:"randomAttributes"`
}

// TraceTemplate describes how all a trace and it's spans are generated.
Expand Down Expand Up @@ -163,11 +176,13 @@ type internalSpanTemplate struct {
}

type internalResourceTemplate struct {
service string
hostName string
hostIP string
transport string
hostPort int
service string
hostName string
hostIP string
transport string
hostPort int
attributes map[string]interface{}
randomAttributes map[string][]interface{}
}

type internalLinkTemplate struct {
Expand Down Expand Up @@ -233,6 +248,13 @@ func (g *TemplatedGenerator) generateResourceSpans(resSpanSlice ptrace.ResourceS
resSpans.Resource().Attributes().PutStr("k6", "true")
resSpans.Resource().Attributes().PutStr("service.name", tmpl.service)

for k, v := range tmpl.attributes {
_ = resSpans.Resource().Attributes().PutEmpty(k).FromRaw(v)
}
for k, v := range tmpl.randomAttributes {
_ = resSpans.Resource().Attributes().PutEmpty(k).FromRaw(random.SelectElement(v))
}

scopeSpans := resSpans.ScopeSpans().AppendEmpty()
scopeSpans.Scope().SetName("k6-scope-name/" + random.String(15))
scopeSpans.Scope().SetVersion("k6-scope-version:v" + strconv.Itoa(random.IntBetween(0, 99)) + "." + strconv.Itoa(random.IntBetween(0, 99)))
Expand Down Expand Up @@ -465,10 +487,12 @@ func (g *TemplatedGenerator) initialize(template *TraceTemplate) error {
}

// get or generate the corresponding ResourceSpans
_, found := g.resources[tmpl.Service]
res, found := g.resources[tmpl.Service]
if !found {
res := g.initializeResource(&tmpl)
res = g.initializeResource(&tmpl, &template.Defaults)
g.resources[tmpl.Service] = res
} else {
g.amendInitializedResource(res, &tmpl)
}

// span template parent index must reference a previous span
Expand Down Expand Up @@ -505,14 +529,40 @@ func (g *TemplatedGenerator) initialize(template *TraceTemplate) error {
return nil
}

func (g *TemplatedGenerator) initializeResource(tmpl *SpanTemplate) *internalResourceTemplate {
return &internalResourceTemplate{
func (g *TemplatedGenerator) initializeResource(tmpl *SpanTemplate, defaults *SpanDefaults) *internalResourceTemplate {
res := internalResourceTemplate{
service: tmpl.Service,
hostName: fmt.Sprintf("%s.local", tmpl.Service),
hostIP: random.IPAddr(),
hostPort: random.Port(),
transport: "ip_tcp",
}

// use defaults if no resource attributes are set
if tmpl.Resource == nil {
tmpl.Resource = defaults.Resource
}

if tmpl.Resource != nil {
res.randomAttributes = initializeRandomAttributes(tmpl.Resource.RandomAttributes)
res.attributes = tmpl.Resource.Attributes
}

return &res
}

func (g *TemplatedGenerator) amendInitializedResource(res *internalResourceTemplate, tmpl *SpanTemplate) {
if tmpl.Resource == nil {
return
}

if tmpl.Resource.RandomAttributes != nil {
randAttr := initializeRandomAttributes(tmpl.Resource.RandomAttributes)
res.randomAttributes = util.MergeMaps(res.randomAttributes, randAttr)
}
if tmpl.Resource.Attributes != nil {
res.attributes = util.MergeMaps(res.attributes, tmpl.Resource.Attributes)
}
}

func (g *TemplatedGenerator) initializeSpan(idx int, parent *internalSpanTemplate, defaults *SpanDefaults, tmpl, child *SpanTemplate) (*internalSpanTemplate, error) {
Expand Down
152 changes: 120 additions & 32 deletions pkg/tracegen/templated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/pdata/pcommon"
"go.opentelemetry.io/collector/pdata/ptrace"
)
Expand All @@ -31,23 +32,66 @@ func TestTemplatedGenerator_Traces(t *testing.T) {
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 {
assert.GreaterOrEqual(t, attributesWithPrefix(span, "k6."), template.Defaults.RandomAttributes.Count)
for range testRounds {
count := 0
for i, span := range iterSpans(gen.Traces()) {
count++
requireAttributeCountGreaterOrEqual(t, span.Attributes(), 3, "k6.")
if template.Spans[i].Name != nil {
assert.Equal(t, *template.Spans[i].Name, span.Name())
}
if span.Kind() != ptrace.SpanKindInternal {
assert.GreaterOrEqual(t, attributesWithPrefix(span, "net."), 3)
requireAttributeCountGreaterOrEqual(t, span.Attributes(), 3, "net.")
if *template.Defaults.AttributeSemantics == SemanticsHTTP {
assert.GreaterOrEqual(t, attributesWithPrefix(span, "http."), 5)
requireAttributeCountGreaterOrEqual(t, span.Attributes(), 5, "http.")
}
}
}
assert.Equal(t, len(template.Spans), count, "unexpected number of spans")
}
}
}

func TestTemplatedGenerator_Resource(t *testing.T) {
template := TraceTemplate{
Defaults: SpanDefaults{
Attributes: map[string]interface{}{"span-attr": "val-01"},
Resource: &ResourceTemplate{RandomAttributes: &AttributeParams{Count: 2}},
},
Spans: []SpanTemplate{
{Service: "test-service-a", Name: ptr("action-a-a"), Resource: &ResourceTemplate{
Attributes: map[string]interface{}{"res-attr-01": "res-val-01"},
RandomAttributes: &AttributeParams{Count: 5},
}},
{Service: "test-service-a", Name: ptr("action-a-b"), Resource: &ResourceTemplate{
Attributes: map[string]interface{}{"res-attr-02": "res-val-02"},
}},
{Service: "test-service-b", Name: ptr("action-b-a"), Resource: &ResourceTemplate{
Attributes: map[string]interface{}{"res-attr-03": "res-val-03"},
}},
{Service: "test-service-b", Name: ptr("action-b-b")},
},
}

gen, err := NewTemplatedGenerator(&template)
require.NoError(t, err)

for range testRounds {
for _, res := range iterResources(gen.Traces()) {
srv, found := res.Attributes().Get("service.name")
require.True(t, found, "service.name not found")

switch srv.Str() {
case "test-service-a":
requireAttributeCountEqual(t, res.Attributes(), 5, "k6.")
requireAttributeEqual(t, res.Attributes(), "res-attr-01", "res-val-01")
requireAttributeEqual(t, res.Attributes(), "res-attr-02", "res-val-02")
case "test-service-b":
requireAttributeCountEqual(t, res.Attributes(), 3, "k6.")
requireAttributeEqual(t, res.Attributes(), "res-attr-03", "res-val-03")
default:
require.Fail(t, "unexpected service name %s", srv.Str())
}
}
}
}
Expand Down Expand Up @@ -76,12 +120,10 @@ func TestTemplatedGenerator_EventsLinks(t *testing.T) {
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 _, span := range spans {
for range testRounds {
count := 0
for _, span := range iterSpans(gen.Traces()) {
count++
events := span.Events()
links := span.Links()
checkEventsLinksLength := func(expectedTemplate, expectedRandom int, spanName string) {
Expand Down Expand Up @@ -144,33 +186,79 @@ func TestTemplatedGenerator_EventsLinks(t *testing.T) {
assert.True(t, found, "exception event not found")
}
}
assert.Equal(t, len(template.Spans), count, "unexpected number of spans")
}
}
}

func attributesWithPrefix(span ptrace.Span, prefix string) int {
var count int
span.Attributes().Range(func(k string, _ pcommon.Value) bool {
if strings.HasPrefix(k, prefix) {
count++
func iterSpans(traces ptrace.Traces) func(func(i int, e ptrace.Span) bool) {
count := 0
return func(f func(i int, e ptrace.Span) bool) {
var elem ptrace.Span
for i := 0; i < traces.ResourceSpans().Len(); i++ {
rs := traces.ResourceSpans().At(i)
for j := 0; j < rs.ScopeSpans().Len(); j++ {
ss := rs.ScopeSpans().At(j)
for k := 0; k < ss.Spans().Len(); k++ {
elem = ss.Spans().At(k)
if !f(count, elem) {
return
}
count++
}
}
}
return true
})
return count
}
}

func collectSpansFromTrace(traces ptrace.Traces) []ptrace.Span {
var spans []ptrace.Span
for i := 0; i < traces.ResourceSpans().Len(); i++ {
rs := traces.ResourceSpans().At(i)
for j := 0; j < rs.ScopeSpans().Len(); j++ {
ss := rs.ScopeSpans().At(j)
for k := 0; k < ss.Spans().Len(); k++ {
spans = append(spans, ss.Spans().At(k))
func iterResources(traces ptrace.Traces) func(func(i int, e pcommon.Resource) bool) {
return func(f func(i int, e pcommon.Resource) bool) {
var elem pcommon.Resource
for i := 0; i < traces.ResourceSpans().Len(); i++ {
rs := traces.ResourceSpans().At(i)
elem = rs.Resource()
if !f(i, elem) {
return
}
}
}
return spans
}

func requireAttributeCountGreaterOrEqual(t *testing.T, attributes pcommon.Map, compare int, prefixes ...string) {
t.Helper()
count := countAttributes(attributes, prefixes...)
require.GreaterOrEqual(t, count, compare, "expected at least %d attributes, got %d", compare, count)
}

func requireAttributeCountEqual(t *testing.T, attributes pcommon.Map, expected int, prefixes ...string) {
t.Helper()
count := countAttributes(attributes, prefixes...)
require.GreaterOrEqual(t, expected, count, "expected at least %d attributes, got %d", expected, count)
}

func requireAttributeEqual(t *testing.T, attributes pcommon.Map, key string, expected any) {
t.Helper()
val, found := attributes.Get(key)
require.True(t, found, "attribute %s not found", key)
require.Equal(t, expected, val.AsRaw(), "value %v expected for attribute %s but was %v", expected, key, val.AsRaw())
}

func countAttributes(attributes pcommon.Map, prefixes ...string) int {
var count int
attributes.Range(func(k string, _ pcommon.Value) bool {
if len(prefixes) == 0 {
count++
return true
}

for _, prefix := range prefixes {
if strings.HasPrefix(k, prefix) {
count++
}
}
return true
})
return count
}

func ptr[T any](v T) *T {
Expand Down