Skip to content

Commit

Permalink
Merge pull request #170 from appoptics/AO-18760-ao-exporter
Browse files Browse the repository at this point in the history
NH-6889 AO-18760 : AppOptics trace exporter for OpenTelemetry collector
  • Loading branch information
twiz718 authored Apr 18, 2022
2 parents 578efd8 + 836b9c8 commit 7c8eb32
Show file tree
Hide file tree
Showing 14 changed files with 880 additions and 155 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ require (
require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/stretchr/objx v0.3.0 // indirect
go.opentelemetry.io/otel v1.3.0
go.opentelemetry.io/otel/sdk v1.3.0
go.opentelemetry.io/otel/trace v1.3.0
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
Expand Down
15 changes: 14 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.1 h1:DX7uPQ4WgAWfoh+NGGlbJQswnYIVvz0SRlLS3rPZQDA=
github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.0 h1:j4LrlVXgrbIWO83mmQUnK0Hi+YnbD+vzrE1z/EphbFE=
github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
Expand All @@ -54,8 +59,9 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw=
Expand Down Expand Up @@ -83,6 +89,12 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.opentelemetry.io/otel v1.3.0 h1:APxLf0eiBwLl+SOXiJJCVYzA1OOJNyAoV8C5RNRyy7Y=
go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
go.opentelemetry.io/otel/sdk v1.3.0 h1:3278edCoH89MEJ0Ky8WQXVmDQv3FX4ZJ3Pp+9fJreAI=
go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=
go.opentelemetry.io/otel/trace v1.3.0 h1:doy8Hzb1RJ+I3yFhtDmwNc7tIyw1tNMOIsyPzp1NOGY=
go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
Expand Down Expand Up @@ -128,6 +140,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
14 changes: 13 additions & 1 deletion v1/ao/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

package ao

import "context"
import (
"context"
"github.com/appoptics/appoptics-apm-go/v1/ao/internal/log"
"github.com/appoptics/appoptics-apm-go/v1/ao/internal/reporter"
)

type contextKeyT interface{}

Expand All @@ -19,6 +23,14 @@ func newSpanContext(ctx context.Context, l Span) context.Context {
return context.WithValue(ctx, contextSpanKey, l)
}

func FromXTraceIDContext(ctx context.Context, xTraceID string) context.Context {
aoCtx, err := reporter.NewContextFromMetadataString(xTraceID)
if err != nil {
log.Warningf("xTrace ID %v is invalid \n", xTraceID)
}
return context.WithValue(ctx, contextSpanKey, contextSpan{aoCtx: aoCtx})
}

// FromContext returns the Span bound to the context, if any.
func FromContext(ctx context.Context) Span {
l, ok := fromContext(ctx)
Expand Down
202 changes: 202 additions & 0 deletions v1/ao/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package ao //

import (
"context"
"encoding/hex"
"fmt"
"strings"
"time"

"go.opentelemetry.io/otel/attribute"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

type Exporter struct {
shutdownDelay int // number of seconds to sleep when Shutdown is called, to allow spans to send before short test script exits.
}

const (
xtraceVersionHeader = "2B"
sampledFlags = "01"
otEventNameKey = "ot.event_name"
otStatusCodeKey = "ot.span_status.code"
otSpanStatusDescKey = "ot.span_status.description"
)

func fromAttributeValue(attributeValue attribute.Value) interface{} {
switch attributeValue.Type() {
case attribute.STRING:
return attributeValue.AsString()
case attribute.INT64:
return attributeValue.AsInt64()
case attribute.FLOAT64:
return attributeValue.AsFloat64()
case attribute.BOOL:
return attributeValue.AsBool()
case attribute.STRINGSLICE:
return attributeValue.AsStringSlice()
case attribute.INT64SLICE:
return attributeValue.AsInt64Slice()
case attribute.FLOAT64SLICE:
return attributeValue.AsFloat64Slice()
case attribute.BOOLSLICE:
return attributeValue.AsBoolSlice()
default:
return nil
}
}

var wsKeyMap = map[string]string{
"http.method": "HTTPMethod",
"http.url": "URL",
"http.status_code": "Status",
}
var queryKeyMap = map[string]string{
"db.connection_string": "RemoteHost",
"db.name": "Database",
"db.statement": "Query",
"db.system": "Flavor",
}

func extractWebserverKvs(span sdktrace.ReadOnlySpan) []interface{} {
return extractSpecKvs(span, wsKeyMap, "ws")
}

func extractQueryKvs(span sdktrace.ReadOnlySpan) []interface{} {
return extractSpecKvs(span, queryKeyMap, "query")
}

func extractSpecKvs(span sdktrace.ReadOnlySpan, lookup map[string]string, specValue string) []interface{} {
attrMap := span.Attributes()
result := []interface{}{}
for otKey, aoKey := range lookup {
for _, attr := range attrMap {
if string(attr.Key) == otKey {
result = append(result, aoKey)
result = append(result, fromAttributeValue(attr.Value))
}
}
}
if len(result) > 0 {
result = append(result, "Spec")
result = append(result, specValue)
}
return result
}

func extractKvs(span sdktrace.ReadOnlySpan) []interface{} {
var kvs []interface{}
for _, attributeValue := range span.Attributes() {
if _, ok := wsKeyMap[string(attributeValue.Key)]; ok { // in wsKeyMap, skip it and handle later
continue
}
if _, ok := queryKeyMap[string(attributeValue.Key)]; ok { // in queryKeyMap, skip it and handle later
continue
}
// all other keys
kvs = append(kvs, string(attributeValue.Key))
kvs = append(kvs, fromAttributeValue(attributeValue.Value))
}

spanStatus := span.Status()
kvs = append(kvs, otStatusCodeKey)
kvs = append(kvs, uint32(spanStatus.Code))
if spanStatus.Code == 1 { // if the span status code is an error, send the description. otel will ignore the description on any other status code
kvs = append(kvs, otSpanStatusDescKey)
kvs = append(kvs, spanStatus.Description)
}
if !span.Parent().IsValid() { // root span, attempt to extract webserver KVs
kvs = append(kvs, extractWebserverKvs(span)...)
}
kvs = append(kvs, extractQueryKvs(span)...)

return kvs
}

func extractInfoEvents(span sdktrace.ReadOnlySpan) [][]interface{} {
events := span.Events()
kvs := make([][]interface{}, len(events))

for i, event := range events {
kvs[i] = make([]interface{}, 0)
kvs[i] = append(kvs[i], otEventNameKey)
kvs[i] = append(kvs[i], string(event.Name))
for _, attr := range event.Attributes {
kvs[i] = append(kvs[i], string(attr.Key))
kvs[i] = append(kvs[i], fromAttributeValue(attr.Value))
}
}

return kvs
}

func getXTraceID(traceID []byte, spanID []byte) string {
taskId := strings.ToUpper(strings.ReplaceAll(fmt.Sprintf("%0-40v", hex.EncodeToString(traceID)), " ", "0"))
opId := strings.ToUpper(strings.ReplaceAll(fmt.Sprintf("%0-16v", hex.EncodeToString(spanID)), " ", "0"))
return xtraceVersionHeader + taskId + opId + sampledFlags
}

func exportSpan(ctx context.Context, s sdktrace.ReadOnlySpan) {
traceID := s.SpanContext().TraceID()
spanID := s.SpanContext().SpanID()
xTraceID := getXTraceID(traceID[:], spanID[:])

startOverrides := Overrides{
ExplicitTS: s.StartTime(),
ExplicitMdStr: xTraceID,
}

endOverrides := Overrides{
ExplicitTS: s.EndTime(),
}

kvs := extractKvs(s)

infoEvents := extractInfoEvents(s)

if s.Parent().IsValid() { // this is a child span, not a start of a trace but rather a continuation of an existing one
parentSpanID := s.Parent().SpanID()
parentXTraceID := getXTraceID(traceID[:], parentSpanID[:])
traceContext := FromXTraceIDContext(ctx, parentXTraceID)
aoSpan, _ := BeginSpanWithOverrides(traceContext, s.Name(), SpanOptions{}, startOverrides)

// report otel Span Events as AO Info KVs
for _, infoEventKvs := range infoEvents {
aoSpan.InfoWithOverrides(Overrides{ExplicitTS: s.StartTime()}, SpanOptions{}, infoEventKvs...)
}

aoSpan.EndWithOverrides(endOverrides, kvs...)
} else { // no parent means this is the beginning of the trace (root span)
trace := NewTraceWithOverrides(s.Name(), startOverrides, nil)
trace.SetStartTime(s.StartTime()) //this is for histogram only

// report otel Span Events as AO Info KVs
for _, infoEventKvs := range infoEvents {
trace.InfoWithOverrides(Overrides{ExplicitTS: s.StartTime()}, SpanOptions{}, infoEventKvs...)
}
trace.EndWithOverrides(endOverrides, kvs...)
}
}

func (e *Exporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
WaitForReady(ctx)
for _, s := range spans {
exportSpan(ctx, s)
}
return nil
}

func (e *Exporter) Shutdown(ctx context.Context) error {
// Most applications should never set this value, it is only useful for testing short running (cli) scripts.
if e.shutdownDelay != 0 {
time.Sleep(time.Duration(e.shutdownDelay) * time.Second)
}

Shutdown(ctx)
return nil
}

// NewExporter creates an instance of the Solarwinds AppOptics exporter for OTEL traces.
func NewExporter(shutdownDelay int) *Exporter {
return &Exporter{shutdownDelay: shutdownDelay}
}
Loading

0 comments on commit 7c8eb32

Please sign in to comment.