Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support aws sdk go for v2 instrumentation #621

Merged
merged 10 commits into from
Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
wangzlei marked this conversation as resolved.
Show resolved Hide resolved
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
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"
wangzlei marked this conversation as resolved.
Show resolved Hide resolved
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