From 640a916049c9676a57a3a22620f37dd3427c7467 Mon Sep 17 00:00:00 2001 From: stuart nelson Date: Wed, 9 Feb 2022 14:40:54 +0100 Subject: [PATCH] Add otel type (#1210) * add otel + marshal/unmarshal otel type will be populated with data when using `apmotel` module cf. https://github.com/elastic/apm-agent-go/pull/1203 --- context.go | 17 +++++++++++++++ model/marshal_fastjson.go | 41 ++++++++++++++++++++++++++++++++++ model/marshal_test.go | 46 +++++++++++++++++++++++++++++++++++++++ model/model.go | 12 ++++++++++ modelwriter.go | 2 ++ spancontext.go | 17 +++++++++++++++ 6 files changed, 135 insertions(+) diff --git a/context.go b/context.go index dedf0a675..a4f61ca93 100644 --- a/context.go +++ b/context.go @@ -39,6 +39,7 @@ type Context struct { user model.User service model.Service serviceFramework model.Framework + otel *model.OTel captureHeaders bool captureBodyMask CaptureBodyMode sanitizedFieldNames wildcard.Matchers @@ -83,6 +84,22 @@ func (c *Context) reset() { } } +// SetOTelAttributes sets the provided OpenTelemetry attributes. +func (c *Context) SetOTelAttributes(m map[string]interface{}) { + if c.otel == nil { + c.otel = &model.OTel{} + } + c.otel.Attributes = m +} + +// SetOTelSpanKind sets the provided SpanKind. +func (c *Context) SetOTelSpanKind(spanKind string) { + if c.otel == nil { + c.otel = &model.OTel{} + } + c.otel.SpanKind = spanKind +} + // SetTag calls SetLabel(key, value). // // SetTag is deprecated, and will be removed in a future major version. diff --git a/model/marshal_fastjson.go b/model/marshal_fastjson.go index 6b763d309..ae85e48e9 100644 --- a/model/marshal_fastjson.go +++ b/model/marshal_fastjson.go @@ -525,6 +525,12 @@ func (v *Transaction) MarshalFastJSON(w *fastjson.Writer) error { } w.RawByte(']') } + if v.OTel != nil { + w.RawString(",\"otel\":") + if err := v.OTel.MarshalFastJSON(w); err != nil && firstErr == nil { + firstErr = err + } + } if v.Outcome != "" { w.RawString(",\"outcome\":") w.String(v.Outcome) @@ -551,6 +557,35 @@ func (v *Transaction) MarshalFastJSON(w *fastjson.Writer) error { return firstErr } +func (v *OTel) MarshalFastJSON(w *fastjson.Writer) error { + var firstErr error + w.RawByte('{') + w.RawString("\"span_kind\":") + w.String(v.SpanKind) + if v.Attributes != nil { + w.RawString(",\"attributes\":") + w.RawByte('{') + { + first := true + for k, v := range v.Attributes { + if first { + first = false + } else { + w.RawByte(',') + } + w.String(k) + w.RawByte(':') + if err := fastjson.Marshal(w, v); err != nil && firstErr == nil { + firstErr = err + } + } + } + w.RawByte('}') + } + w.RawByte('}') + return firstErr +} + func (v *SpanCount) MarshalFastJSON(w *fastjson.Writer) error { w.RawByte('{') w.RawString("\"dropped\":") @@ -634,6 +669,12 @@ func (v *Span) MarshalFastJSON(w *fastjson.Writer) error { firstErr = err } } + if v.OTel != nil { + w.RawString(",\"otel\":") + if err := v.OTel.MarshalFastJSON(w); err != nil && firstErr == nil { + firstErr = err + } + } if v.Outcome != "" { w.RawString(",\"outcome\":") w.String(v.Outcome) diff --git a/model/marshal_test.go b/model/marshal_test.go index ed8ae50fc..a10dd3d00 100644 --- a/model/marshal_test.go +++ b/model/marshal_test.go @@ -115,6 +115,12 @@ func TestMarshalTransaction(t *testing.T) { "outcome": "success", }, }, + "otel": map[string]interface{}{ + "span_kind": "MESSAGING", + "attributes": map[string]interface{}{ + "messaging.system": "messaging", + }, + }, } assert.Equal(t, expect, decoded) } @@ -143,6 +149,12 @@ func TestMarshalSpan(t *testing.T) { "user": "barb", }, }, + "otel": map[string]interface{}{ + "span_kind": "MESSAGING", + "attributes": map[string]interface{}{ + "messaging.system": "messaging", + }, + }, }, decoded) w.Reset() @@ -152,6 +164,14 @@ func TestMarshalSpan(t *testing.T) { span.ParentID = model.SpanID{} // parent_id is optional span.TransactionID = model.SpanID{} // transaction_id is optional span.Context = fakeHTTPSpanContext() + span.OTel = &model.OTel{ + SpanKind: "SERVER", + Attributes: map[string]interface{}{ + "numeric.data": 123.456, + "boolean.data": true, + "slice.data": []string{"one", "two"}, + }, + } span.MarshalFastJSON(&w) decoded = mustUnmarshalJSON(w) @@ -167,6 +187,14 @@ func TestMarshalSpan(t *testing.T) { "url": "http://testing.invalid:8000/path?query#fragment", }, }, + "otel": map[string]interface{}{ + "span_kind": "SERVER", + "attributes": map[string]interface{}{ + "numeric.data": 123.456, + "boolean.data": true, + "slice.data": []interface{}{"one", "two"}, + }, + }, }, decoded) } @@ -193,6 +221,12 @@ func TestMarshalSpanHTTPStatusCode(t *testing.T) { "status_code": 200.0, }, }, + "otel": map[string]interface{}{ + "span_kind": "MESSAGING", + "attributes": map[string]interface{}{ + "messaging.system": "messaging", + }, + }, }, decoded) } @@ -669,6 +703,12 @@ func fakeTransaction() model.Transaction { }, }, }, + OTel: &model.OTel{ + SpanKind: "MESSAGING", + Attributes: map[string]interface{}{ + "messaging.system": "messaging", + }, + }, } } @@ -683,6 +723,12 @@ func fakeSpan() model.Span { Duration: 3, Type: "db.postgresql.query", Context: fakeDatabaseSpanContext(), + OTel: &model.OTel{ + SpanKind: "MESSAGING", + Attributes: map[string]interface{}{ + "messaging.system": "messaging", + }, + }, } } diff --git a/model/model.go b/model/model.go index 0a11448e3..0272e95c0 100644 --- a/model/model.go +++ b/model/model.go @@ -275,6 +275,15 @@ type Transaction struct { // Outcome holds the transaction outcome: success, failure, or unknown. Outcome string `json:"outcome,omitempty"` + + // OTel holds information bridged from OpenTelemetry. + OTel *OTel `json:"otel,omitempty"` +} + +// OTel holds bridged OpenTelemetry information. +type OTel struct { + SpanKind string `json:"span_kind"` + Attributes map[string]interface{} `json:"attributes,omitempty"` } // SpanCount holds statistics on spans within a transaction. @@ -366,6 +375,9 @@ type Span struct { // Composite is set when the span is a composite span and represents an // aggregated set of spans as defined by `composite.compression_strategy`. Composite *CompositeSpan `json:"composite,omitempty"` + + // OTel holds information bridged from OpenTelemetry. + OTel *OTel `json:"otel,omitempty"` } // SpanContext holds contextual information relating to the span. diff --git a/modelwriter.go b/modelwriter.go index 23a4b9832..692acf01f 100644 --- a/modelwriter.go +++ b/modelwriter.go @@ -123,6 +123,7 @@ func (w *modelWriter) buildModelTransaction(out *model.Transaction, tx *Transact out.Duration = td.Duration.Seconds() * 1000 out.SpanCount.Started = td.spansCreated out.SpanCount.Dropped = td.spansDropped + out.OTel = td.Context.otel if dss := buildDroppedSpansStats(td.droppedSpansStats); len(dss) > 0 { out.DroppedSpansStats = dss } @@ -150,6 +151,7 @@ func (w *modelWriter) buildModelSpan(out *model.Span, span *Span, sd *SpanData) out.Duration = sd.Duration.Seconds() * 1000 out.Outcome = normalizeOutcome(sd.Outcome) out.Context = sd.Context.build() + out.OTel = sd.Context.otel if sd.composite.count > 1 { out.Composite = sd.composite.build() } diff --git a/spancontext.go b/spancontext.go index a239d3099..01204ad2e 100644 --- a/spancontext.go +++ b/spancontext.go @@ -37,6 +37,7 @@ type SpanContext struct { databaseRowsAffected int64 database model.DatabaseSpanContext http model.HTTPSpanContext + otel *model.OTel // If SetDestinationService has been called, we do not auto-set its // resource value on span end. @@ -104,6 +105,22 @@ func (c *SpanContext) reset() { } } +// SetOTelAttributes sets the provided OpenTelemetry attributes. +func (c *SpanContext) SetOTelAttributes(m map[string]interface{}) { + if c.otel == nil { + c.otel = &model.OTel{} + } + c.otel.Attributes = m +} + +// SetOTelSpanKind sets the provided SpanKind. +func (c *SpanContext) SetOTelSpanKind(spanKind string) { + if c.otel == nil { + c.otel = &model.OTel{} + } + c.otel.SpanKind = spanKind +} + // SetTag calls SetLabel(key, value). // // SetTag is deprecated, and will be removed in a future major version.