forked from open-telemetry/opentelemetry-go-contrib
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
support aws sdk go for v2 instrumentation (#621)
* 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
Showing
13 changed files
with
698 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/attributes.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
116
instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/aws.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
165
instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/aws_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
44
instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/config.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} |
Oops, something went wrong.