Skip to content

Commit

Permalink
feat: support span events (#2911)
Browse files Browse the repository at this point in the history
* feat: add span events as an attribute called "events" (#2886)

* feat: add span events as an attribute called "events"

* update attribute name to "span.events"

* fix test

* feat: support events in Jaeger, Tempo, and all OTLP datastores (both gRPC and HTTP variants) (#2887)

support events in jaeger and tempo (grpc and http)

* feat: events app insights (#2905)

* feat: support span events in app insights

* don't ignore error and fix function names

* docs: span events (#2920)

* add docs and test in expression executor

* Update docs/docs/cli/creating-test-specifications.md

Co-authored-by: Julianne Fermi <julianne@kubeshop.io>

---------

Co-authored-by: Julianne Fermi <julianne@kubeshop.io>

---------

Co-authored-by: Julianne Fermi <julianne@kubeshop.io>
  • Loading branch information
mathnogueira and jfermi committed Jul 11, 2023
1 parent baa93c2 commit 0c190bc
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 138 deletions.
16 changes: 16 additions & 0 deletions docs/docs/cli/creating-test-specifications.md
Expand Up @@ -86,3 +86,19 @@ For more information about selectors or assertions, take a look at the documenta
| `>=` | Check if value from left side is larger or equal to the one on the right side of the operation. |
| `contains` | Check if value on the right side of the operation is contained inside of the value of the left side of the operation. |
| `not-contains` | Check if value on the right side of the operation is not contained inside of the value of the left side of the operation. |

## Testing Span Events

As an MVP of how to test span events, we are injecting `all spans` events into the span attributes as a JSON array. To assert your span events, use the `json_path` filter to select and test the **write events**.

```yaml
specs:
- selector: span[name = "my span"]
assertions:
- attr:span.events | json_path '$[?(@.name = "event name")].attributes.key' = "expected_value"
```

### Query breakdown

* **@.name = "event name"**: select the event with name "**event name**"
* $[?(@.name = "event name")]**.attributes.key**: select the attribute "**key**" from the event with name "**event name**"
5 changes: 5 additions & 0 deletions server/expression/executor_test.go
Expand Up @@ -291,6 +291,11 @@ func TestArrayExecution(t *testing.T) {
Query: `'{ "array": [{ "name": "john", "age": 37 }, { "name": "jonas", "age": 38 }]}' | json_path '$.array[*].age' = [37, 38]`,
ShouldPass: true,
},
{
Name: "arrays_can_be_filtered_by_value",
Query: `'[{ "name": "john", "age": 37 }, { "name": "jonas", "age": 38 }]' | json_path '$[?(@.name == "john")].age' = 37`,
ShouldPass: true,
},
{
Name: "should_check_if_array_contains_value",
Query: `[31,35,39] contains 35`,
Expand Down
16 changes: 16 additions & 0 deletions server/model/spans.go
Expand Up @@ -73,11 +73,27 @@ type Span struct {
EndTime time.Time
Attributes Attributes
Kind SpanKind
Events []SpanEvent

Parent *Span `json:"-"`
Children []*Span `json:"-"`
}

func (s *Span) injectEventsIntoAttributes() {
if s.Events == nil {
s.Events = make([]SpanEvent, 0)
}

eventsJson, _ := json.Marshal(s.Events)
s.Attributes["span.events"] = string(eventsJson)
}

type SpanEvent struct {
Name string `json:"name"`
Timestamp time.Time `json:"timestamp"`
Attributes Attributes `json:"attributes"`
}

type encodedSpan struct {
ID string
Name string
Expand Down
2 changes: 2 additions & 0 deletions server/model/traces.go
Expand Up @@ -28,6 +28,8 @@ func NewTrace(traceID string, spans []Span) Trace {

rootSpans := make([]*Span, 0)
for _, span := range spanMap {
span.injectEventsIntoAttributes()

parentID := span.Attributes[TracetestMetadataFieldParentID]
parentSpan, found := spanMap[parentID]
if !found {
Expand Down
130 changes: 86 additions & 44 deletions server/model/traces_test.go
@@ -1,6 +1,7 @@
package model_test

import (
"encoding/json"
"testing"
"time"

Expand All @@ -13,10 +14,10 @@ import (
)

func TestTraces(t *testing.T) {
rootSpan := newSpan("Root", nil)
childSpan1 := newSpan("child 1", &rootSpan)
childSpan2 := newSpan("child 2", &rootSpan)
grandchildSpan := newSpan("grandchild", &childSpan2)
rootSpan := newSpan("Root")
childSpan1 := newSpan("child 1", withParent(&rootSpan))
childSpan2 := newSpan("child 2", withParent(&rootSpan))
grandchildSpan := newSpan("grandchild", withParent(&childSpan2))

spans := []model.Span{rootSpan, childSpan1, childSpan2, grandchildSpan}
trace := model.NewTrace("trace", spans)
Expand All @@ -29,12 +30,12 @@ func TestTraces(t *testing.T) {
}

func TestTraceWithMultipleRoots(t *testing.T) {
root1 := newSpan("Root 1", nil)
root1Child := newSpan("Child from root 1", &root1)
root2 := newSpan("Root 2", nil)
root2Child := newSpan("Child from root 2", &root2)
root3 := newSpan("Root 3", nil)
root3Child := newSpan("Child from root 3", &root3)
root1 := newSpan("Root 1")
root1Child := newSpan("Child from root 1", withParent(&root1))
root2 := newSpan("Root 2")
root2Child := newSpan("Child from root 2", withParent(&root2))
root3 := newSpan("Root 3")
root3Child := newSpan("Child from root 3", withParent(&root3))

spans := []model.Span{root1, root1Child, root2, root2Child, root3, root3Child}
trace := model.NewTrace("trace", spans)
Expand Down Expand Up @@ -84,15 +85,15 @@ func TestTraceWithMultipleRootsFromOtel(t *testing.T) {
}

func TestInjectingNewRootWhenSingleRoot(t *testing.T) {
rootSpan := newSpan("Root", nil)
childSpan1 := newSpan("child 1", &rootSpan)
childSpan2 := newSpan("child 2", &rootSpan)
grandchildSpan := newSpan("grandchild", &childSpan2)
rootSpan := newSpan("Root")
childSpan1 := newSpan("child 1", withParent(&rootSpan))
childSpan2 := newSpan("child 2", withParent(&rootSpan))
grandchildSpan := newSpan("grandchild", withParent(&childSpan2))

spans := []model.Span{rootSpan, childSpan1, childSpan2, grandchildSpan}
trace := model.NewTrace("trace", spans)

newRoot := newSpan("new Root", nil)
newRoot := newSpan("new Root")
newTrace := trace.InsertRootSpan(newRoot)

assert.Len(t, newTrace.Flat, 5)
Expand All @@ -102,12 +103,12 @@ func TestInjectingNewRootWhenSingleRoot(t *testing.T) {
}

func TestInjectingNewRootWhenMultipleRoots(t *testing.T) {
root1 := newSpan("Root 1", nil)
root1Child := newSpan("Child from root 1", &root1)
root2 := newSpan("Root 2", nil)
root2Child := newSpan("Child from root 2", &root2)
root3 := newSpan("Root 3", nil)
root3Child := newSpan("Child from root 3", &root3)
root1 := newSpan("Root 1")
root1Child := newSpan("Child from root 1", withParent(&root1))
root2 := newSpan("Root 2")
root2Child := newSpan("Child from root 2", withParent(&root2))
root3 := newSpan("Root 3")
root3Child := newSpan("Child from root 3", withParent(&root3))

spans := []model.Span{root1, root1Child, root2, root2Child, root3, root3Child}
trace := model.NewTrace("trace", spans)
Expand All @@ -116,7 +117,7 @@ func TestInjectingNewRootWhenMultipleRoots(t *testing.T) {
require.NotNil(t, oldRoot.Parent)
}

newRoot := newSpan("new Root", nil)
newRoot := newSpan("new Root")
newTrace := trace.InsertRootSpan(newRoot)

assert.Len(t, newTrace.Flat, 7)
Expand All @@ -133,12 +134,12 @@ func TestInjectingNewRootWhenMultipleRoots(t *testing.T) {
}

func TestNoTemporaryRootIfTracetestRootExists(t *testing.T) {
root1 := newSpan("Root 1", nil)
root1Child := newSpan("Child from root 1", &root1)
root2 := newSpan(model.TriggerSpanName, nil)
root2Child := newSpan("Child from root 2", &root2)
root3 := newSpan("Root 3", nil)
root3Child := newSpan("Child from root 3", &root3)
root1 := newSpan("Root 1")
root1Child := newSpan("Child from root 1", withParent(&root1))
root2 := newSpan(model.TriggerSpanName)
root2Child := newSpan("Child from root 2", withParent(&root2))
root3 := newSpan("Root 3")
root3Child := newSpan("Child from root 3", withParent(&root3))

spans := []model.Span{root1, root1Child, root2, root2Child, root3, root3Child}
trace := model.NewTrace("trace", spans)
Expand All @@ -148,12 +149,12 @@ func TestNoTemporaryRootIfTracetestRootExists(t *testing.T) {
}

func TestNoTemporaryRootIfATemporaryRootExists(t *testing.T) {
root1 := newSpan("Root 1", nil)
root1Child := newSpan("Child from root 1", &root1)
root2 := newSpan(model.TemporaryRootSpanName, nil)
root2Child := newSpan("Child from root 2", &root2)
root3 := newSpan("Root 3", nil)
root3Child := newSpan("Child from root 3", &root3)
root1 := newSpan("Root 1")
root1Child := newSpan("Child from root 1", withParent(&root1))
root2 := newSpan(model.TemporaryRootSpanName)
root2Child := newSpan("Child from root 2", withParent(&root2))
root3 := newSpan("Root 3")
root3Child := newSpan("Child from root 3", withParent(&root3))

spans := []model.Span{root1, root1Child, root2, root2Child, root3, root3Child}
trace := model.NewTrace("trace", spans)
Expand All @@ -163,12 +164,12 @@ func TestNoTemporaryRootIfATemporaryRootExists(t *testing.T) {
}

func TestTriggerSpanShouldBeRootWhenTemporaryRootExistsToo(t *testing.T) {
root1 := newSpan(model.TriggerSpanName, nil)
root1Child := newSpan("Child from root 1", &root1)
root2 := newSpan(model.TemporaryRootSpanName, nil)
root2Child := newSpan("Child from root 2", &root2)
root3 := newSpan("Root 3", nil)
root3Child := newSpan("Child from root 3", &root3)
root1 := newSpan(model.TriggerSpanName)
root1Child := newSpan("Child from root 1", withParent(&root1))
root2 := newSpan(model.TemporaryRootSpanName)
root2Child := newSpan("Child from root 2", withParent(&root2))
root3 := newSpan("Root 3")
root3Child := newSpan("Child from root 3", withParent(&root3))

spans := []model.Span{root1, root1Child, root2, root2Child, root3, root3Child}
trace := model.NewTrace("trace", spans)
Expand All @@ -177,18 +178,59 @@ func TestTriggerSpanShouldBeRootWhenTemporaryRootExistsToo(t *testing.T) {
assert.Equal(t, root1.Name, trace.RootSpan.Name)
}

func newSpan(name string, parent *model.Span) model.Span {
func TestEventsAreInjectedIntoAttributes(t *testing.T) {
rootSpan := newSpan("Root", withEvents([]model.SpanEvent{
{Name: "event 1", Attributes: model.Attributes{"attribute1": "value"}},
{Name: "event 2", Attributes: model.Attributes{"attribute2": "value"}},
}))
childSpan1 := newSpan("child 1", withParent(&rootSpan))
childSpan2 := newSpan("child 2", withParent(&rootSpan))
grandchildSpan := newSpan("grandchild", withParent(&childSpan2))

spans := []model.Span{rootSpan, childSpan1, childSpan2, grandchildSpan}
trace := model.NewTrace("trace", spans)

require.NotEmpty(t, trace.RootSpan.Attributes["span.events"])

events := []model.SpanEvent{}
err := json.Unmarshal([]byte(trace.RootSpan.Attributes["span.events"]), &events)
require.NoError(t, err)

assert.Equal(t, "event 1", events[0].Name)
assert.Equal(t, "value", events[0].Attributes["attribute1"])
assert.Equal(t, "event 2", events[1].Name)
assert.Equal(t, "value", events[1].Attributes["attribute2"])
}

type option func(*model.Span)

func withParent(parent *model.Span) option {
return func(s *model.Span) {
s.Parent = parent
}
}

func withEvents(events []model.SpanEvent) option {
return func(s *model.Span) {
s.Events = events
}
}

func newSpan(name string, options ...option) model.Span {
span := model.Span{
ID: id.NewRandGenerator().SpanID(),
Name: name,
Parent: parent,
Attributes: make(model.Attributes),
StartTime: time.Now(),
EndTime: time.Now().Add(1 * time.Second),
}

if parent != nil {
span.Attributes[model.TracetestMetadataFieldParentID] = parent.ID.String()
for _, option := range options {
option(&span)
}

if span.Parent != nil {
span.Attributes[model.TracetestMetadataFieldParentID] = span.Parent.ID.String()
}

return span
Expand Down

0 comments on commit 0c190bc

Please sign in to comment.