Skip to content

Commit

Permalink
support aws sdk go for v2 instrumentation (#621)
Browse files Browse the repository at this point in the history
* support aws sdk go for v2 instrumentation

* fix span.name

* update span name in initialize middleware

* move set attributes from deserialize to init.after

* fix comment

* update README and code style

* fix code review comments

* fix code review comments

Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>
  • Loading branch information
wangzlei and Aneurysm9 committed Mar 23, 2021
1 parent c332299 commit 9d0d9d9
Show file tree
Hide file tree
Showing 13 changed files with 698 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,24 @@ updates:
schedule:
interval: "weekly"
day: "sunday"
- package-ecosystem: "gomod"
directory: "/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws"
labels:
- dependencies
- go
- "Skip Changelog"
schedule:
interval: "weekly"
day: "sunday"
- package-ecosystem: "gomod"
directory: "/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/example"
labels:
- dependencies
- go
- "Skip Changelog"
schedule:
interval: "weekly"
day: "sunday"
-
package-ecosystem: "gomod"
directory: "/instrumentation/github.com/bradfitz/gomemcache/memcache/otelmemcache"
Expand Down
40 changes: 40 additions & 0 deletions instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/attributes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package otelaws

import "go.opentelemetry.io/otel/attribute"

const (
OperationKey attribute.Key = "aws.operation"
RegionKey attribute.Key = "aws.region"
ServiceKey attribute.Key = "aws.service"
RequestIDKey attribute.Key = "aws.request_id"
)

func OperationAttr(operation string) attribute.KeyValue {
return OperationKey.String(operation)
}

func RegionAttr(region string) attribute.KeyValue {
return RegionKey.String(region)
}

func ServiceAttr(service string) attribute.KeyValue {
return ServiceKey.String(service)
}

func RequestIDAttr(requestID string) attribute.KeyValue {
return RequestIDKey.String(requestID)
}
116 changes: 116 additions & 0 deletions instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package otelaws

import (
"context"
"time"

v2Middleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
"github.com/aws/smithy-go/middleware"
smithyhttp "github.com/aws/smithy-go/transport/http"

"go.opentelemetry.io/contrib"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/semconv"
"go.opentelemetry.io/otel/trace"
)

const (
tracerName = "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws"
)

type spanTimestampKey struct{}

type otelMiddlewares struct {
tracer trace.Tracer
}

func (m otelMiddlewares) initializeMiddlewareBefore(stack *middleware.Stack) error {
return stack.Initialize.Add(middleware.InitializeMiddlewareFunc("OTelInitializeMiddlewareBefore", func(
ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) (
out middleware.InitializeOutput, metadata middleware.Metadata, err error) {

ctx = context.WithValue(ctx, spanTimestampKey{}, time.Now())
return next.HandleInitialize(ctx, in)
}),
middleware.Before)
}

func (m otelMiddlewares) initializeMiddlewareAfter(stack *middleware.Stack) error {
return stack.Initialize.Add(middleware.InitializeMiddlewareFunc("OTelInitializeMiddlewareAfter", func(
ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) (
out middleware.InitializeOutput, metadata middleware.Metadata, err error) {

serviceID := v2Middleware.GetServiceID(ctx)
opts := []trace.SpanOption{
trace.WithTimestamp(ctx.Value(spanTimestampKey{}).(time.Time)),
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(ServiceAttr(serviceID),
RegionAttr(v2Middleware.GetRegion(ctx)),
OperationAttr(v2Middleware.GetOperationName(ctx))),
}
ctx, span := m.tracer.Start(ctx, serviceID, opts...)
defer span.End()

out, metadata, err = next.HandleInitialize(ctx, in)
if err != nil {
span.RecordError(err)
}

return out, metadata, err
}),
middleware.After)
}

func (m otelMiddlewares) deserializeMiddleware(stack *middleware.Stack) error {
return stack.Deserialize.Add(middleware.DeserializeMiddlewareFunc("OTelDeserializeMiddleware", func(
ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) (
out middleware.DeserializeOutput, metadata middleware.Metadata, err error) {
out, metadata, err = next.HandleDeserialize(ctx, in)
resp, ok := out.RawResponse.(*smithyhttp.Response)
if !ok {
// No raw response to wrap with.
return out, metadata, err
}

span := trace.SpanFromContext(ctx)
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(resp.StatusCode))

requestID, ok := v2Middleware.GetRequestIDMetadata(metadata)
if ok {
span.SetAttributes(RequestIDAttr(requestID))
}

return out, metadata, err
}),
middleware.Before)
}

// AppendMiddlewares attaches OTel middlewares to the AWS Go SDK V2 for instrumentation.
// OTel middlewares can be appended to either all aws clients or a specific operation.
// Please see more details in https://aws.github.io/aws-sdk-go-v2/docs/middleware/
func AppendMiddlewares(apiOptions *[]func(*middleware.Stack) error, opts ...Option) {
cfg := config{
TracerProvider: otel.GetTracerProvider(),
}
for _, opt := range opts {
opt.Apply(&cfg)
}

m := otelMiddlewares{tracer: cfg.TracerProvider.Tracer(tracerName,
trace.WithInstrumentationVersion(contrib.SemVersion()))}
*apiOptions = append(*apiOptions, m.initializeMiddlewareBefore, m.initializeMiddlewareAfter, m.deserializeMiddleware)
}
165 changes: 165 additions & 0 deletions instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/aws_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package otelaws

import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/route53"
"github.com/aws/aws-sdk-go-v2/service/route53/types"
"github.com/stretchr/testify/assert"

"go.opentelemetry.io/otel/oteltest"
"go.opentelemetry.io/otel/trace"
)

func TestAppendMiddlewares(t *testing.T) {
cases := map[string]struct {
responseStatus int
responseBody []byte
expectedRegion string
expectedError string
expectedRequestID string
expectedStatusCode int
}{
"invalidChangeBatchError": {
responseStatus: 500,
responseBody: []byte(`<?xml version="1.0" encoding="UTF-8"?>
<InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<Messages>
<Message>Tried to create resource record set duplicate.example.com. type A, but it already exists</Message>
</Messages>
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
</InvalidChangeBatch>`),
expectedRegion: "us-east-1",
expectedError: "Error",
expectedRequestID: "b25f48e8-84fd-11e6-80d9-574e0c4664cb",
expectedStatusCode: 500,
},

"standardRestXMLError": {
responseStatus: 404,
responseBody: []byte(`<?xml version="1.0"?>
<ErrorResponse xmlns="http://route53.amazonaws.com/doc/2016-09-07/">
<Error>
<Type>Sender</Type>
<Code>MalformedXML</Code>
<Message>1 validation error detected: Value null at 'route53#ChangeSet' failed to satisfy constraint: Member must not be null</Message>
</Error>
<RequestId>1234567890A</RequestId>
</ErrorResponse>
`),
expectedRegion: "us-west-1",
expectedError: "Error",
expectedRequestID: "1234567890A",
expectedStatusCode: 404,
},

"Success response": {
responseStatus: 200,
responseBody: []byte(`<?xml version="1.0" encoding="UTF-8"?>
<ChangeResourceRecordSetsResponse>
<ChangeInfo>
<Comment>mockComment</Comment>
<Id>mockID</Id>
</ChangeInfo>
</ChangeResourceRecordSetsResponse>`),
expectedRegion: "us-west-2",
expectedStatusCode: 200,
},
}

for name, c := range cases {
server := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(c.responseStatus)
_, err := w.Write(c.responseBody)
if err != nil {
t.Fatal(err)
}
}))
defer server.Close()

t.Run(name, func(t *testing.T) {
sr := new(oteltest.SpanRecorder)
provider := oteltest.NewTracerProvider(oteltest.WithSpanRecorder(sr))

svc := route53.NewFromConfig(aws.Config{
Region: c.expectedRegion,
EndpointResolver: aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
return aws.Endpoint{
URL: server.URL,
SigningName: "route53",
}, nil
}),
Retryer: func() aws.Retryer {
return aws.NopRetryer{}
},
})
_, err := svc.ChangeResourceRecordSets(context.Background(), &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &types.ChangeBatch{
Changes: []types.Change{},
Comment: aws.String("mock"),
},
HostedZoneId: aws.String("zone"),
}, func(options *route53.Options) {
AppendMiddlewares(
&options.APIOptions, WithTracerProvider(provider))
})

spans := sr.Completed()
assert.Len(t, spans, 1)
span := spans[0]

if e, a := "Route 53", span.Name(); !strings.EqualFold(e, a) {
t.Errorf("expected span name to be %s, got %s", e, a)
}

if e, a := trace.SpanKindClient, span.SpanKind(); e != a {
t.Errorf("expected span kind to be %v, got %v", e, a)
}

if e, a := c.expectedError, span.StatusCode().String(); err != nil && !strings.EqualFold(e, a) {
t.Errorf("Span Error is missing.")
}

if e, a := c.expectedStatusCode, span.Attributes()["http.status_code"].AsInt64(); e != int(a) {
t.Errorf("expected status code to be %v, got %v", e, a)
}

if e, a := c.expectedRequestID, span.Attributes()["aws.request_id"].AsString(); !strings.EqualFold(e, a) {
t.Errorf("expected request id to be %s, got %s", e, a)
}

if e, a := "Route 53", span.Attributes()["aws.service"].AsString(); !strings.EqualFold(e, a) {
t.Errorf("expected service to be %s, got %s", e, a)
}

if e, a := c.expectedRegion, span.Attributes()["aws.region"].AsString(); !strings.EqualFold(e, a) {
t.Errorf("expected region to be %s, got %s", e, a)
}

if e, a := "ChangeResourceRecordSets", span.Attributes()["aws.operation"].AsString(); !strings.EqualFold(e, a) {
t.Errorf("expected operation to be %s, got %s", e, a)
}
})

}
}
44 changes: 44 additions & 0 deletions instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package otelaws

import (
"go.opentelemetry.io/otel/trace"
)

type config struct {
TracerProvider trace.TracerProvider
}

// Option applies an option value.
type Option interface {
Apply(*config)
}

// optionFunc provides a convenience wrapper for simple Options
// that can be represented as functions.
type optionFunc func(*config)

func (o optionFunc) Apply(c *config) {
o(c)
}

// WithTracerProvider specifies a tracer provider to use for creating a tracer.
// If none is specified, the global TracerProvider is used.
func WithTracerProvider(provider trace.TracerProvider) Option {
return optionFunc(func(cfg *config) {
cfg.TracerProvider = provider
})
}
Loading

0 comments on commit 9d0d9d9

Please sign in to comment.