diff --git a/sdk/telemetry/attr_test.go b/sdk/telemetry/attr_test.go index be1632349..adbabc303 100644 --- a/sdk/telemetry/attr_test.go +++ b/sdk/telemetry/attr_test.go @@ -3,16 +3,10 @@ package telemetry -import ( - "encoding/json" - "testing" +import "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - decoded = []Attr{ +func TestAttrEncoding(t *testing.T) { + attrs := []Attr{ String("user", "Alice"), Bool("admin", true), Int64("floor", -2), @@ -22,17 +16,141 @@ var ( Bytes("secret", []byte("NUI4RUZGRjc5ODAzODEwM0QyNjlCNjMzODEzRkM2MEM=")), } - encoded = []byte(`[{"key":"user","value":{"stringValue":"Alice"}},{"key":"admin","value":{"boolValue":true}},{"key":"floor","value":{"intValue":"-2"}},{"key":"impact","value":{"doubleValue":0.21362}},{"key":"reports","value":{"arrayValue":{"values":[{"stringValue":"Bob"},{"stringValue":"Dave"}]}}},{"key":"favorites","value":{"kvlistValue":{"values":[{"key":"food","value":{"stringValue":"hot dog"}},{"key":"number","value":{"intValue":"13"}}]}}},{"key":"secret","value":{"bytesValue":"TlVJNFJVWkdSamM1T0RBek9ERXdNMFF5TmpsQ05qTXpPREV6UmtNMk1FTT0="}}]`) -) - -func TestAttrUnmarshal(t *testing.T) { - var got []Attr - require.NoError(t, json.Unmarshal(encoded, &got)) - assert.Equal(t, decoded, got) -} + t.Run("CamelCase", runJSONEncodingTests(attrs, []byte(`[ + { + "key": "user", + "value": { + "stringValue": "Alice" + } + }, + { + "key": "admin", + "value": { + "boolValue": true + } + }, + { + "key": "floor", + "value": { + "intValue": "-2" + } + }, + { + "key": "impact", + "value": { + "doubleValue": 0.21362 + } + }, + { + "key": "reports", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "Bob" + }, + { + "stringValue": "Dave" + } + ] + } + } + }, + { + "key": "favorites", + "value": { + "kvlistValue": { + "values": [ + { + "key": "food", + "value": { + "stringValue": "hot dog" + } + }, + { + "key": "number", + "value": { + "intValue": "13" + } + } + ] + } + } + }, + { + "key": "secret", + "value": { + "bytesValue": "TlVJNFJVWkdSamM1T0RBek9ERXdNMFF5TmpsQ05qTXpPREV6UmtNMk1FTT0=" + } + } + ]`))) -func TestAttrMarshal(t *testing.T) { - got, err := json.Marshal(decoded) - require.NoError(t, err) - assert.Equal(t, string(encoded), string(got)) + t.Run("SnakeCase/Unmarshal", runJSONUnmarshalTest(attrs, []byte(`[ + { + "key": "user", + "value": { + "string_value": "Alice" + } + }, + { + "key": "admin", + "value": { + "bool_value": true + } + }, + { + "key": "floor", + "value": { + "int_value": "-2" + } + }, + { + "key": "impact", + "value": { + "double_value": 0.21362 + } + }, + { + "key": "reports", + "value": { + "array_value": { + "values": [ + { + "string_value": "Bob" + }, + { + "string_value": "Dave" + } + ] + } + } + }, + { + "key": "favorites", + "value": { + "kvlist_value": { + "values": [ + { + "key": "food", + "value": { + "string_value": "hot dog" + } + }, + { + "key": "number", + "value": { + "int_value": "13" + } + } + ] + } + } + }, + { + "key": "secret", + "value": { + "bytes_value": "TlVJNFJVWkdSamM1T0RBek9ERXdNMFF5TmpsQ05qTXpPREV6UmtNMk1FTT0=" + } + } + ]`))) } diff --git a/sdk/telemetry/conv_test.go b/sdk/telemetry/conv_test.go new file mode 100644 index 000000000..6a65b787f --- /dev/null +++ b/sdk/telemetry/conv_test.go @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package telemetry + +import ( + "bytes" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const schema100 = "http://go.opentelemetry.io/schema/v1.0.0" + +var y2k = time.Unix(0, time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC).UnixNano()) // No location. + +func runJSONEncodingTests[T any](decoded T, encoded []byte) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + + t.Run("Unmarshal", runJSONUnmarshalTest(decoded, encoded)) + t.Run("Marshal", runJSONMarshalTest(decoded, encoded)) + } +} + +func runJSONMarshalTest[T any](decoded T, encoded []byte) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + + got, err := json.Marshal(decoded) + require.NoError(t, err) + + var want bytes.Buffer + require.NoError(t, json.Compact(&want, encoded)) + assert.Equal(t, want.String(), string(got)) + } +} + +func runJSONUnmarshalTest[T any](decoded T, encoded []byte) func(*testing.T) { + return func(t *testing.T) { + t.Helper() + + var got T + require.NoError(t, json.Unmarshal(encoded, &got)) + assert.Equal(t, decoded, got) + } +} diff --git a/sdk/telemetry/resource_test.go b/sdk/telemetry/resource_test.go new file mode 100644 index 000000000..1be443d3a --- /dev/null +++ b/sdk/telemetry/resource_test.go @@ -0,0 +1,37 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package telemetry + +import "testing" + +func TestResourceEncoding(t *testing.T) { + res := &Resource{ + Attrs: []Attr{String("key", "val")}, + DroppedAttrs: 10, + } + + t.Run("CamelCase", runJSONEncodingTests(res, []byte(`{ + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "val" + } + } + ], + "droppedAttributesCount": 10 + }`))) + + t.Run("SnakeCase/Unmarshal", runJSONUnmarshalTest(res, []byte(`{ + "attributes": [ + { + "key": "key", + "value": { + "string_value": "val" + } + } + ], + "dropped_attributes_count": 10 + }`))) +} diff --git a/sdk/telemetry/scope.go b/sdk/telemetry/scope.go index d6381d841..b6f2e28d4 100644 --- a/sdk/telemetry/scope.go +++ b/sdk/telemetry/scope.go @@ -3,6 +3,14 @@ package telemetry +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" +) + // Scope is the identifying values of the instrumentation scope. type Scope struct { Name string `json:"name,omitempty"` @@ -10,3 +18,50 @@ type Scope struct { Attrs []Attr `json:"attributes,omitempty"` DroppedAttrs uint32 `json:"droppedAttributesCount,omitempty"` } + +// UnmarshalJSON decodes the OTLP formatted JSON contained in data into r. +func (s *Scope) UnmarshalJSON(data []byte) error { + decoder := json.NewDecoder(bytes.NewReader(data)) + + t, err := decoder.Token() + if err != nil { + return err + } + if t != json.Delim('{') { + return errors.New("invalid Scope type") + } + + for decoder.More() { + keyIface, err := decoder.Token() + if err != nil { + if errors.Is(err, io.EOF) { + // Empty. + return nil + } + return err + } + + key, ok := keyIface.(string) + if !ok { + return fmt.Errorf("invalid Scope field: %#v", keyIface) + } + + switch key { + case "name": + err = decoder.Decode(&s.Name) + case "version": + err = decoder.Decode(&s.Version) + case "attributes": + err = decoder.Decode(&s.Attrs) + case "droppedAttributesCount", "dropped_attributes_count": + err = decoder.Decode(&s.DroppedAttrs) + default: + // Skip unknown. + } + + if err != nil { + return err + } + } + return nil +} diff --git a/sdk/telemetry/scope_test.go b/sdk/telemetry/scope_test.go new file mode 100644 index 000000000..8c9527938 --- /dev/null +++ b/sdk/telemetry/scope_test.go @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package telemetry + +import "testing" + +func TestScopeEncoding(t *testing.T) { + scope := &Scope{ + Name: "go.opentelemetry.io/auto/sdk/telemetry/test", + Version: "v0.0.1", + Attrs: []Attr{String("department", "ops")}, + DroppedAttrs: 1, + } + + t.Run("CamelCase", runJSONEncodingTests(scope, []byte(`{ + "name": "go.opentelemetry.io/auto/sdk/telemetry/test", + "version": "v0.0.1", + "attributes": [ + { + "key": "department", + "value": { + "stringValue": "ops" + } + } + ], + "droppedAttributesCount": 1 + }`))) + + t.Run("SnakeCase/Unmarshal", runJSONUnmarshalTest(scope, []byte(`{ + "name": "go.opentelemetry.io/auto/sdk/telemetry/test", + "version": "v0.0.1", + "attributes": [ + { + "key": "department", + "value": { + "string_value": "ops" + } + } + ], + "dropped_attributes_count": 1 + }`))) +} diff --git a/sdk/telemetry/span.go b/sdk/telemetry/span.go index 4cb4dca14..a13a6b733 100644 --- a/sdk/telemetry/span.go +++ b/sdk/telemetry/span.go @@ -5,6 +5,7 @@ package telemetry import ( "bytes" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -69,7 +70,7 @@ type Span struct { // Empty value is equivalent to an unknown span name. // // This field is required. - Name string `json:"name,omitempty"` + Name string `json:"name"` // Distinguishes between spans generated in a particular context. For example, // two spans with the same name may be distinguished using `CLIENT` (caller) // and `SERVER` (callee) to identify queueing latency associated with the span. @@ -123,15 +124,35 @@ type Span struct { // MarshalJSON encodes s into OTLP formatted JSON. func (s Span) MarshalJSON() ([]byte, error) { + startT := s.StartTime.UnixNano() + if s.StartTime.IsZero() || startT < 0 { + startT = 0 + } + + endT := s.EndTime.UnixNano() + if s.EndTime.IsZero() || endT < 0 { + endT = 0 + } + + // Override non-empty default SpanID marshal and omitempty. + var parentSpanId string + if !s.ParentSpanID.IsEmpty() { + b := make([]byte, hex.EncodedLen(spanIDSize)) + hex.Encode(b, s.ParentSpanID[:]) + parentSpanId = string(b) + } + type Alias Span return json.Marshal(struct { Alias - StartTime uint64 `json:"startTimeUnixNano,omitempty"` - EndTime uint64 `json:"endTimeUnixNano,omitempty"` + ParentSpanID string `json:"parentSpanId,omitempty"` + StartTime uint64 `json:"startTimeUnixNano,omitempty"` + EndTime uint64 `json:"endTimeUnixNano,omitempty"` }{ - Alias: Alias(s), - StartTime: uint64(s.StartTime.UnixNano()), - EndTime: uint64(s.EndTime.UnixNano()), + Alias: Alias(s), + ParentSpanID: parentSpanId, + StartTime: uint64(startT), + EndTime: uint64(endT), }) } @@ -280,13 +301,18 @@ type SpanEvent struct { // MarshalJSON encodes e into OTLP formatted JSON. func (e SpanEvent) MarshalJSON() ([]byte, error) { + t := e.Time.UnixNano() + if e.Time.IsZero() || t < 0 { + t = 0 + } + type Alias SpanEvent return json.Marshal(struct { Alias Time uint64 `json:"timeUnixNano,omitempty"` }{ Alias: Alias(e), - Time: uint64(e.Time.UnixNano()), + Time: uint64(t), }) } @@ -416,6 +442,8 @@ func (sl *SpanLink) UnmarshalJSON(data []byte) error { err = decoder.Decode(&sl.Attrs) case "droppedAttributesCount", "dropped_attributes_count": err = decoder.Decode(&sl.DroppedAttrs) + case "flags": + err = decoder.Decode(&sl.Flags) default: // Skip unknown. } diff --git a/sdk/telemetry/span_test.go b/sdk/telemetry/span_test.go new file mode 100644 index 000000000..3192d15bb --- /dev/null +++ b/sdk/telemetry/span_test.go @@ -0,0 +1,200 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package telemetry + +import ( + "testing" + "time" +) + +func TestSpanEncoding(t *testing.T) { + span := &Span{ + TraceID: [16]byte{0x1}, + SpanID: [8]byte{0x2}, + TraceState: "test=a", + ParentSpanID: [8]byte{0x1}, + Flags: 1, + Name: "span.a", + Kind: SpanKindClient, + StartTime: y2k, + EndTime: y2k.Add(time.Second), + Attrs: []Attr{String("key", "val")}, + DroppedAttrs: 2, + Events: []*SpanEvent{{ + Name: "name", + }}, + DroppedEvents: 3, + Links: []*SpanLink{{ + TraceID: TraceID{0x2}, + SpanID: SpanID{0x1}, + }}, + DroppedLinks: 4, + Status: &Status{ + Message: "okay", + Code: StatusCodeOK, + }, + } + + t.Run("CamelCase", runJSONEncodingTests(span, []byte(`{ + "traceId": "01000000000000000000000000000000", + "spanId": "0200000000000000", + "traceState": "test=a", + "flags": 1, + "name": "span.a", + "kind": 3, + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "val" + } + } + ], + "droppedAttributesCount": 2, + "events": [ + { + "name": "name" + } + ], + "droppedEventsCount": 3, + "links": [ + { + "traceId": "02000000000000000000000000000000", + "spanId": "0100000000000000" + } + ], + "droppedLinksCount": 4, + "status": { + "message": "okay", + "code": 1 + }, + "parentSpanId": "0100000000000000", + "startTimeUnixNano": 946684800000000000, + "endTimeUnixNano": 946684801000000000 + }`))) + + t.Run("SnakeCase/Unmarshal", runJSONUnmarshalTest(span, []byte(`{ + "trace_id": "01000000000000000000000000000000", + "span_id": "0200000000000000", + "trace_state": "test=a", + "flags": 1, + "name": "span.a", + "kind": 3, + "attributes": [ + { + "key": "key", + "value": { + "string_value": "val" + } + } + ], + "dropped_attributes_count": 2, + "events": [ + { + "name": "name" + } + ], + "dropped_events_count": 3, + "links": [ + { + "trace_id": "02000000000000000000000000000000", + "span_id": "0100000000000000" + } + ], + "dropped_links_count": 4, + "status": { + "message": "okay", + "code": 1 + }, + "parent_span_id": "0100000000000000", + "start_time_unix_nano": 946684800000000000, + "end_time_unix_nano": 946684801000000000 + }`))) + + t.Run("RequiredFields", runJSONMarshalTest(new(Span), []byte(`{ + "traceId": "", + "spanId": "", + "name": "" + }`))) +} + +func TestSpanEventEncoding(t *testing.T) { + event := &SpanEvent{ + Time: y2k.Add(10 * time.Microsecond), + Name: "span.event", + Attrs: []Attr{Float64("impact", 0.4372)}, + DroppedAttrs: 2, + } + + t.Run("CamelCase", runJSONEncodingTests(event, []byte(`{ + "name": "span.event", + "attributes": [ + { + "key": "impact", + "value": { + "doubleValue": 0.4372 + } + } + ], + "droppedAttributesCount": 2, + "timeUnixNano": 946684800000010000 + }`))) + + t.Run("SnakeCase/Unmarshal", runJSONUnmarshalTest(event, []byte(`{ + "name": "span.event", + "attributes": [ + { + "key": "impact", + "value": { + "double_value": 0.4372 + } + } + ], + "dropped_attributes_count": 2, + "time_unix_nano": 946684800000010000 + }`))) +} + +func TestSpanLinkEncoding(t *testing.T) { + link := &SpanLink{ + TraceID: TraceID{0x2}, + SpanID: SpanID{0x1}, + TraceState: "test=green", + Attrs: []Attr{Int("queue", 17)}, + DroppedAttrs: 8, + Flags: 1, + } + + t.Run("CamelCase", runJSONEncodingTests(link, []byte(`{ + "traceId": "02000000000000000000000000000000", + "spanId": "0100000000000000", + "traceState": "test=green", + "attributes": [ + { + "key": "queue", + "value": { + "intValue": "17" + } + } + ], + "droppedAttributesCount": 8, + "flags": 1 + }`))) + + t.Run("SnakeCase/Unmarshal", runJSONUnmarshalTest(link, []byte(`{ + "trace_id": "02000000000000000000000000000000", + "span_id": "0100000000000000", + "trace_state": "test=green", + "attributes": [ + { + "key": "queue", + "value": { + "int_value": "17" + } + } + ], + "dropped_attributes_count": 8, + "flags": 1 + }`))) +} diff --git a/sdk/telemetry/test/conversion_test.go b/sdk/telemetry/test/conversion_test.go index 3e783b2c8..ff5a2e4e0 100644 --- a/sdk/telemetry/test/conversion_test.go +++ b/sdk/telemetry/test/conversion_test.go @@ -67,8 +67,7 @@ var ( TraceState: "test=green", Attrs: []telemetry.Attr{telemetry.Int("queue", 17)}, DroppedAttrs: 8, - // https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1196 - // Flags: 1, + Flags: 1, } pLink = func() ptrace.SpanLink { l := ptrace.NewSpanLink() @@ -77,8 +76,7 @@ var ( l.TraceState().FromRaw("test=green") l.Attributes().PutInt("queue", 17) l.SetDroppedAttributesCount(8) - // https://github.com/open-telemetry/opentelemetry-go-instrumentation/pull/1196 - // l.SetFlags(1) + l.SetFlags(1) return l }() diff --git a/sdk/telemetry/traces_test.go b/sdk/telemetry/traces_test.go new file mode 100644 index 000000000..7e247f92d --- /dev/null +++ b/sdk/telemetry/traces_test.go @@ -0,0 +1,145 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package telemetry + +import ( + "testing" + "time" +) + +func TestTracesEncoding(t *testing.T) { + traces := &Traces{ + ResourceSpans: []*ResourceSpans{{}}, + } + + t.Run("CamelCase", runJSONEncodingTests(traces, []byte(`{ + "resourceSpans": [ + { + "resource": {} + } + ] + }`))) + + t.Run("SnakeCase/Unmarshal", runJSONUnmarshalTest(traces, []byte(`{ + "resource_spans": [ + { + "resource": {} + } + ] + }`))) +} + +func TestResourceSpansEncoding(t *testing.T) { + rs := &ResourceSpans{ + Resource: Resource{ + Attrs: []Attr{String("key", "val")}, + }, + ScopeSpans: []*ScopeSpans{{}}, + SchemaURL: schema100, + } + + t.Run("CamelCase", runJSONEncodingTests(rs, []byte(`{ + "resource": { + "attributes": [ + { + "key": "key", + "value": { + "stringValue": "val" + } + } + ] + }, + "scopeSpans": [ + { + "scope": null + } + ], + "schemaUrl": "http://go.opentelemetry.io/schema/v1.0.0" + }`))) + + t.Run("SnakeCase/Unmarshal", runJSONUnmarshalTest(rs, []byte(`{ + "resource": { + "attributes": [ + { + "key": "key", + "value": { + "string_value": "val" + } + } + ] + }, + "scope_spans": [ + { + "scope": null + } + ], + "schema_url": "http://go.opentelemetry.io/schema/v1.0.0" + }`))) +} + +func TestScopeSpansEncoding(t *testing.T) { + ss := &ScopeSpans{ + Scope: &Scope{Name: "scope"}, + Spans: []*Span{{ + TraceID: [16]byte{0x1}, + SpanID: [8]byte{0x2}, + Name: "A", + StartTime: y2k, + EndTime: y2k.Add(time.Second), + }, { + TraceID: [16]byte{0x1}, + SpanID: [8]byte{0x3}, + Name: "B", + StartTime: y2k.Add(time.Second), + EndTime: y2k.Add(2 * time.Second), + }}, + SchemaURL: schema100, + } + + t.Run("CamelCase", runJSONEncodingTests(ss, []byte(`{ + "scope": { + "name": "scope" + }, + "spans": [ + { + "traceId": "01000000000000000000000000000000", + "spanId": "0200000000000000", + "name": "A", + "startTimeUnixNano": 946684800000000000, + "endTimeUnixNano": 946684801000000000 + }, + { + "traceId": "01000000000000000000000000000000", + "spanId": "0300000000000000", + "name": "B", + "startTimeUnixNano": 946684801000000000, + "endTimeUnixNano": 946684802000000000 + } + ], + "schemaUrl": "http://go.opentelemetry.io/schema/v1.0.0" + }`))) + + t.Run("SnakeCase/Unmarshal", runJSONUnmarshalTest(ss, []byte(`{ + "scope": { + "name": "scope" + }, + "spans": [ + { + "trace_id": "01000000000000000000000000000000", + "span_id": "0200000000000000", + "name": "A", + "start_time_unix_nano": 946684800000000000, + "end_time_unix_nano": 946684801000000000 + }, + { + "trace_id": "01000000000000000000000000000000", + "span_id": "0300000000000000", + "name": "B", + "start_time_unix_nano": 946684801000000000, + "end_time_unix_nano": 946684802000000000 + } + ], + "schema_url": "http://go.opentelemetry.io/schema/v1.0.0" + }`))) +}