Skip to content

Commit 9d0d9d9

Browse files
wangzleiAneurysm9
andauthored
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 <[email protected]>
1 parent c332299 commit 9d0d9d9

File tree

13 files changed

+698
-0
lines changed

13 files changed

+698
-0
lines changed

.github/dependabot.yml

+18
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,24 @@ updates:
137137
schedule:
138138
interval: "weekly"
139139
day: "sunday"
140+
- package-ecosystem: "gomod"
141+
directory: "/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws"
142+
labels:
143+
- dependencies
144+
- go
145+
- "Skip Changelog"
146+
schedule:
147+
interval: "weekly"
148+
day: "sunday"
149+
- package-ecosystem: "gomod"
150+
directory: "/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws/example"
151+
labels:
152+
- dependencies
153+
- go
154+
- "Skip Changelog"
155+
schedule:
156+
interval: "weekly"
157+
day: "sunday"
140158
-
141159
package-ecosystem: "gomod"
142160
directory: "/instrumentation/github.com/bradfitz/gomemcache/memcache/otelmemcache"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package otelaws
16+
17+
import "go.opentelemetry.io/otel/attribute"
18+
19+
const (
20+
OperationKey attribute.Key = "aws.operation"
21+
RegionKey attribute.Key = "aws.region"
22+
ServiceKey attribute.Key = "aws.service"
23+
RequestIDKey attribute.Key = "aws.request_id"
24+
)
25+
26+
func OperationAttr(operation string) attribute.KeyValue {
27+
return OperationKey.String(operation)
28+
}
29+
30+
func RegionAttr(region string) attribute.KeyValue {
31+
return RegionKey.String(region)
32+
}
33+
34+
func ServiceAttr(service string) attribute.KeyValue {
35+
return ServiceKey.String(service)
36+
}
37+
38+
func RequestIDAttr(requestID string) attribute.KeyValue {
39+
return RequestIDKey.String(requestID)
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package otelaws
16+
17+
import (
18+
"context"
19+
"time"
20+
21+
v2Middleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
22+
"github.com/aws/smithy-go/middleware"
23+
smithyhttp "github.com/aws/smithy-go/transport/http"
24+
25+
"go.opentelemetry.io/contrib"
26+
"go.opentelemetry.io/otel"
27+
"go.opentelemetry.io/otel/semconv"
28+
"go.opentelemetry.io/otel/trace"
29+
)
30+
31+
const (
32+
tracerName = "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws"
33+
)
34+
35+
type spanTimestampKey struct{}
36+
37+
type otelMiddlewares struct {
38+
tracer trace.Tracer
39+
}
40+
41+
func (m otelMiddlewares) initializeMiddlewareBefore(stack *middleware.Stack) error {
42+
return stack.Initialize.Add(middleware.InitializeMiddlewareFunc("OTelInitializeMiddlewareBefore", func(
43+
ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) (
44+
out middleware.InitializeOutput, metadata middleware.Metadata, err error) {
45+
46+
ctx = context.WithValue(ctx, spanTimestampKey{}, time.Now())
47+
return next.HandleInitialize(ctx, in)
48+
}),
49+
middleware.Before)
50+
}
51+
52+
func (m otelMiddlewares) initializeMiddlewareAfter(stack *middleware.Stack) error {
53+
return stack.Initialize.Add(middleware.InitializeMiddlewareFunc("OTelInitializeMiddlewareAfter", func(
54+
ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) (
55+
out middleware.InitializeOutput, metadata middleware.Metadata, err error) {
56+
57+
serviceID := v2Middleware.GetServiceID(ctx)
58+
opts := []trace.SpanOption{
59+
trace.WithTimestamp(ctx.Value(spanTimestampKey{}).(time.Time)),
60+
trace.WithSpanKind(trace.SpanKindClient),
61+
trace.WithAttributes(ServiceAttr(serviceID),
62+
RegionAttr(v2Middleware.GetRegion(ctx)),
63+
OperationAttr(v2Middleware.GetOperationName(ctx))),
64+
}
65+
ctx, span := m.tracer.Start(ctx, serviceID, opts...)
66+
defer span.End()
67+
68+
out, metadata, err = next.HandleInitialize(ctx, in)
69+
if err != nil {
70+
span.RecordError(err)
71+
}
72+
73+
return out, metadata, err
74+
}),
75+
middleware.After)
76+
}
77+
78+
func (m otelMiddlewares) deserializeMiddleware(stack *middleware.Stack) error {
79+
return stack.Deserialize.Add(middleware.DeserializeMiddlewareFunc("OTelDeserializeMiddleware", func(
80+
ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) (
81+
out middleware.DeserializeOutput, metadata middleware.Metadata, err error) {
82+
out, metadata, err = next.HandleDeserialize(ctx, in)
83+
resp, ok := out.RawResponse.(*smithyhttp.Response)
84+
if !ok {
85+
// No raw response to wrap with.
86+
return out, metadata, err
87+
}
88+
89+
span := trace.SpanFromContext(ctx)
90+
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(resp.StatusCode))
91+
92+
requestID, ok := v2Middleware.GetRequestIDMetadata(metadata)
93+
if ok {
94+
span.SetAttributes(RequestIDAttr(requestID))
95+
}
96+
97+
return out, metadata, err
98+
}),
99+
middleware.Before)
100+
}
101+
102+
// AppendMiddlewares attaches OTel middlewares to the AWS Go SDK V2 for instrumentation.
103+
// OTel middlewares can be appended to either all aws clients or a specific operation.
104+
// Please see more details in https://aws.github.io/aws-sdk-go-v2/docs/middleware/
105+
func AppendMiddlewares(apiOptions *[]func(*middleware.Stack) error, opts ...Option) {
106+
cfg := config{
107+
TracerProvider: otel.GetTracerProvider(),
108+
}
109+
for _, opt := range opts {
110+
opt.Apply(&cfg)
111+
}
112+
113+
m := otelMiddlewares{tracer: cfg.TracerProvider.Tracer(tracerName,
114+
trace.WithInstrumentationVersion(contrib.SemVersion()))}
115+
*apiOptions = append(*apiOptions, m.initializeMiddlewareBefore, m.initializeMiddlewareAfter, m.deserializeMiddleware)
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package otelaws
16+
17+
import (
18+
"context"
19+
"net/http"
20+
"net/http/httptest"
21+
"strings"
22+
"testing"
23+
24+
"github.com/aws/aws-sdk-go-v2/aws"
25+
"github.com/aws/aws-sdk-go-v2/service/route53"
26+
"github.com/aws/aws-sdk-go-v2/service/route53/types"
27+
"github.com/stretchr/testify/assert"
28+
29+
"go.opentelemetry.io/otel/oteltest"
30+
"go.opentelemetry.io/otel/trace"
31+
)
32+
33+
func TestAppendMiddlewares(t *testing.T) {
34+
cases := map[string]struct {
35+
responseStatus int
36+
responseBody []byte
37+
expectedRegion string
38+
expectedError string
39+
expectedRequestID string
40+
expectedStatusCode int
41+
}{
42+
"invalidChangeBatchError": {
43+
responseStatus: 500,
44+
responseBody: []byte(`<?xml version="1.0" encoding="UTF-8"?>
45+
<InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
46+
<Messages>
47+
<Message>Tried to create resource record set duplicate.example.com. type A, but it already exists</Message>
48+
</Messages>
49+
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
50+
</InvalidChangeBatch>`),
51+
expectedRegion: "us-east-1",
52+
expectedError: "Error",
53+
expectedRequestID: "b25f48e8-84fd-11e6-80d9-574e0c4664cb",
54+
expectedStatusCode: 500,
55+
},
56+
57+
"standardRestXMLError": {
58+
responseStatus: 404,
59+
responseBody: []byte(`<?xml version="1.0"?>
60+
<ErrorResponse xmlns="http://route53.amazonaws.com/doc/2016-09-07/">
61+
<Error>
62+
<Type>Sender</Type>
63+
<Code>MalformedXML</Code>
64+
<Message>1 validation error detected: Value null at 'route53#ChangeSet' failed to satisfy constraint: Member must not be null</Message>
65+
</Error>
66+
<RequestId>1234567890A</RequestId>
67+
</ErrorResponse>
68+
`),
69+
expectedRegion: "us-west-1",
70+
expectedError: "Error",
71+
expectedRequestID: "1234567890A",
72+
expectedStatusCode: 404,
73+
},
74+
75+
"Success response": {
76+
responseStatus: 200,
77+
responseBody: []byte(`<?xml version="1.0" encoding="UTF-8"?>
78+
<ChangeResourceRecordSetsResponse>
79+
<ChangeInfo>
80+
<Comment>mockComment</Comment>
81+
<Id>mockID</Id>
82+
</ChangeInfo>
83+
</ChangeResourceRecordSetsResponse>`),
84+
expectedRegion: "us-west-2",
85+
expectedStatusCode: 200,
86+
},
87+
}
88+
89+
for name, c := range cases {
90+
server := httptest.NewServer(http.HandlerFunc(
91+
func(w http.ResponseWriter, r *http.Request) {
92+
w.WriteHeader(c.responseStatus)
93+
_, err := w.Write(c.responseBody)
94+
if err != nil {
95+
t.Fatal(err)
96+
}
97+
}))
98+
defer server.Close()
99+
100+
t.Run(name, func(t *testing.T) {
101+
sr := new(oteltest.SpanRecorder)
102+
provider := oteltest.NewTracerProvider(oteltest.WithSpanRecorder(sr))
103+
104+
svc := route53.NewFromConfig(aws.Config{
105+
Region: c.expectedRegion,
106+
EndpointResolver: aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
107+
return aws.Endpoint{
108+
URL: server.URL,
109+
SigningName: "route53",
110+
}, nil
111+
}),
112+
Retryer: func() aws.Retryer {
113+
return aws.NopRetryer{}
114+
},
115+
})
116+
_, err := svc.ChangeResourceRecordSets(context.Background(), &route53.ChangeResourceRecordSetsInput{
117+
ChangeBatch: &types.ChangeBatch{
118+
Changes: []types.Change{},
119+
Comment: aws.String("mock"),
120+
},
121+
HostedZoneId: aws.String("zone"),
122+
}, func(options *route53.Options) {
123+
AppendMiddlewares(
124+
&options.APIOptions, WithTracerProvider(provider))
125+
})
126+
127+
spans := sr.Completed()
128+
assert.Len(t, spans, 1)
129+
span := spans[0]
130+
131+
if e, a := "Route 53", span.Name(); !strings.EqualFold(e, a) {
132+
t.Errorf("expected span name to be %s, got %s", e, a)
133+
}
134+
135+
if e, a := trace.SpanKindClient, span.SpanKind(); e != a {
136+
t.Errorf("expected span kind to be %v, got %v", e, a)
137+
}
138+
139+
if e, a := c.expectedError, span.StatusCode().String(); err != nil && !strings.EqualFold(e, a) {
140+
t.Errorf("Span Error is missing.")
141+
}
142+
143+
if e, a := c.expectedStatusCode, span.Attributes()["http.status_code"].AsInt64(); e != int(a) {
144+
t.Errorf("expected status code to be %v, got %v", e, a)
145+
}
146+
147+
if e, a := c.expectedRequestID, span.Attributes()["aws.request_id"].AsString(); !strings.EqualFold(e, a) {
148+
t.Errorf("expected request id to be %s, got %s", e, a)
149+
}
150+
151+
if e, a := "Route 53", span.Attributes()["aws.service"].AsString(); !strings.EqualFold(e, a) {
152+
t.Errorf("expected service to be %s, got %s", e, a)
153+
}
154+
155+
if e, a := c.expectedRegion, span.Attributes()["aws.region"].AsString(); !strings.EqualFold(e, a) {
156+
t.Errorf("expected region to be %s, got %s", e, a)
157+
}
158+
159+
if e, a := "ChangeResourceRecordSets", span.Attributes()["aws.operation"].AsString(); !strings.EqualFold(e, a) {
160+
t.Errorf("expected operation to be %s, got %s", e, a)
161+
}
162+
})
163+
164+
}
165+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright The OpenTelemetry Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package otelaws
16+
17+
import (
18+
"go.opentelemetry.io/otel/trace"
19+
)
20+
21+
type config struct {
22+
TracerProvider trace.TracerProvider
23+
}
24+
25+
// Option applies an option value.
26+
type Option interface {
27+
Apply(*config)
28+
}
29+
30+
// optionFunc provides a convenience wrapper for simple Options
31+
// that can be represented as functions.
32+
type optionFunc func(*config)
33+
34+
func (o optionFunc) Apply(c *config) {
35+
o(c)
36+
}
37+
38+
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
39+
// If none is specified, the global TracerProvider is used.
40+
func WithTracerProvider(provider trace.TracerProvider) Option {
41+
return optionFunc(func(cfg *config) {
42+
cfg.TracerProvider = provider
43+
})
44+
}

0 commit comments

Comments
 (0)