From 76a52a17d54c899a25e29a485c6bfb6180c548e3 Mon Sep 17 00:00:00 2001 From: Garrett Egan <22334769+garrettwegan@users.noreply.github.com> Date: Thu, 22 Jul 2021 15:26:30 -0700 Subject: [PATCH] Implement Lambda instrumentation --- detectors/aws/lambda/detector.go | 68 +++ detectors/aws/lambda/detector_test.go | 44 ++ detectors/aws/lambda/go.mod | 11 + detectors/aws/lambda/go.sum | 24 + .../github.com/aws/otellambda/go.mod | 19 + .../github.com/aws/otellambda/go.sum | 145 ++++++ .../github.com/aws/otellambda/lambda.go | 351 ++++++++++++++ .../github.com/aws/otellambda/lambda_test.go | 436 ++++++++++++++++++ 8 files changed, 1098 insertions(+) create mode 100644 detectors/aws/lambda/detector.go create mode 100644 detectors/aws/lambda/detector_test.go create mode 100644 detectors/aws/lambda/go.mod create mode 100644 detectors/aws/lambda/go.sum create mode 100644 instrumentation/github.com/aws/otellambda/go.mod create mode 100644 instrumentation/github.com/aws/otellambda/go.sum create mode 100644 instrumentation/github.com/aws/otellambda/lambda.go create mode 100644 instrumentation/github.com/aws/otellambda/lambda_test.go diff --git a/detectors/aws/lambda/detector.go b/detectors/aws/lambda/detector.go new file mode 100644 index 00000000000..4ede2576c04 --- /dev/null +++ b/detectors/aws/lambda/detector.go @@ -0,0 +1,68 @@ +// 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 lambda + +import ( + "context" + "errors" + "os" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +const ( + lambdaFunctionNameEnvVar = "AWS_LAMBDA_FUNCTION_NAME" + awsRegionEnvVar = "AWS_REGION" + lambdaFunctionVersionEnvVar = "AWS_LAMBDA_FUNCTION_VERSION" +) + +var ( + empty = resource.Empty() + errNotOnLambda = errors.New("process is not on Lambda, cannot detect environment variables from Lambda") +) + +// resource detector collects resource information from Lambda environment +type resourceDetector struct{} + +// compile time assertion that resource detector implements the resource.Detector interface. +var _ resource.Detector = (*resourceDetector)(nil) + +// NewResourceDetector returns a resource detector that will detect AWS Lambda resources. +func NewResourceDetector() resource.Detector { + return &resourceDetector{} +} + +// Detect collects resources available when running on lambda +func (detector *resourceDetector) Detect(context.Context) (*resource.Resource, error) { + + // Lambda resources come from ENV + lambdaName := os.Getenv(lambdaFunctionNameEnvVar) + if len(lambdaName) == 0 { + return empty, errNotOnLambda + } + awsRegion := os.Getenv(awsRegionEnvVar) + functionVersion := os.Getenv(lambdaFunctionVersionEnvVar) + + attrs := []attribute.KeyValue{ + semconv.CloudProviderAWS, + semconv.CloudRegionKey.String(awsRegion), + semconv.FaaSNameKey.String(lambdaName), + semconv.FaaSVersionKey.String(functionVersion), + } + + return resource.NewWithAttributes(semconv.SchemaURL, attrs...), nil +} diff --git a/detectors/aws/lambda/detector_test.go b/detectors/aws/lambda/detector_test.go new file mode 100644 index 00000000000..05048f15b59 --- /dev/null +++ b/detectors/aws/lambda/detector_test.go @@ -0,0 +1,44 @@ +package lambda + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" +) + +// successfully return resource when process is running on Amazon Lambda environment +func TestDetectSuccess(t *testing.T) { + os.Clearenv() + _ = os.Setenv(lambdaFunctionNameEnvVar, "testFunction") + _ = os.Setenv(awsRegionEnvVar, "us-texas-1") + _ = os.Setenv(lambdaFunctionVersionEnvVar, "$LATEST") + + attributes := []attribute.KeyValue{ + semconv.CloudProviderAWS, + semconv.CloudRegionKey.String("us-texas-1"), + semconv.FaaSNameKey.String("testFunction"), + semconv.FaaSVersionKey.String("$LATEST"), + } + expectedResource := resource.NewWithAttributes(semconv.SchemaURL, attributes...) + detector := resourceDetector{} + res, err := detector.Detect(context.Background()) + + assert.Nil(t, err, "Detector unexpectedly returned error") + assert.Equal(t, expectedResource, res, "Resource returned is incorrect") +} + +// return empty resource when not running on lambda +func TestReturnsIfNoEnvVars(t *testing.T) { + os.Clearenv() + detector := resourceDetector{} + res, err := detector.Detect(context.Background()) + + assert.Equal(t, errNotOnLambda, err) + assert.Equal(t, 0, len(res.Attributes())) +} \ No newline at end of file diff --git a/detectors/aws/lambda/go.mod b/detectors/aws/lambda/go.mod new file mode 100644 index 00000000000..a1ba019067c --- /dev/null +++ b/detectors/aws/lambda/go.mod @@ -0,0 +1,11 @@ +module go.opentelemetry.io/contrib/detectors/aws/lambda + +go 1.16 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/stretchr/testify v1.7.0 + go.opentelemetry.io/otel v1.0.0-RC1 + go.opentelemetry.io/otel/sdk v1.0.0-RC1 + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect +) diff --git a/detectors/aws/lambda/go.sum b/detectors/aws/lambda/go.sum new file mode 100644 index 00000000000..1d2f0c1f7c1 --- /dev/null +++ b/detectors/aws/lambda/go.sum @@ -0,0 +1,24 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.0.0-RC1 h1:4CeoX93DNTWt8awGK9JmNXzF9j7TyOu9upscEdtcdXc= +go.opentelemetry.io/otel v1.0.0-RC1/go.mod h1:x9tRa9HK4hSSq7jf2TKbqFbtt58/TGk0f9XiEYISI1I= +go.opentelemetry.io/otel/oteltest v1.0.0-RC1 h1:G685iP3XiskCwk/z0eIabL55XUl2gk0cljhGk9sB0Yk= +go.opentelemetry.io/otel/oteltest v1.0.0-RC1/go.mod h1:+eoIG0gdEOaPNftuy1YScLr1Gb4mL/9lpDkZ0JjMRq4= +go.opentelemetry.io/otel/sdk v1.0.0-RC1 h1:Sy2VLOOg24bipyC29PhuMXYNJrLsxkie8hyI7kUlG9Q= +go.opentelemetry.io/otel/sdk v1.0.0-RC1/go.mod h1:kj6yPn7Pgt5ByRuwesbaWcRLA+V7BSDg3Hf8xRvsvf8= +go.opentelemetry.io/otel/trace v1.0.0-RC1 h1:jrjqKJZEibFrDz+umEASeU3LvdVyWKlnTh7XEfwrT58= +go.opentelemetry.io/otel/trace v1.0.0-RC1/go.mod h1:86UHmyHWFEtWjfWPSbu0+d0Pf9Q6e1U+3ViBOc+NXAg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/instrumentation/github.com/aws/otellambda/go.mod b/instrumentation/github.com/aws/otellambda/go.mod new file mode 100644 index 00000000000..34180b7a8d9 --- /dev/null +++ b/instrumentation/github.com/aws/otellambda/go.mod @@ -0,0 +1,19 @@ +module go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda + +go 1.16 + +replace ( + go.opentelemetry.io/contrib/detectors/aws/lambda => ../../../../../detectors/aws/lambda + go.opentelemetry.io/contrib/propagators/aws => ../../../../../propagators/aws +) + +require ( + github.com/aws/aws-lambda-go v1.24.0 + github.com/stretchr/testify v1.7.0 + go.opentelemetry.io/contrib/detectors/aws/lambda v0.21.0 + go.opentelemetry.io/contrib/propagators/aws v0.21.0 + go.opentelemetry.io/otel v1.0.0-RC1 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.0-RC1 + go.opentelemetry.io/otel/sdk v1.0.0-RC1 + go.opentelemetry.io/otel/trace v1.0.0-RC1 +) diff --git a/instrumentation/github.com/aws/otellambda/go.sum b/instrumentation/github.com/aws/otellambda/go.sum new file mode 100644 index 00000000000..10c48f9d998 --- /dev/null +++ b/instrumentation/github.com/aws/otellambda/go.sum @@ -0,0 +1,145 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/aws/aws-lambda-go v1.24.0 h1:bOMerM175hLqHLdF1Nonfv1NA20nTIatuC0HK8eMoYg= +github.com/aws/aws-lambda-go v1.24.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= +github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +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/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 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +go.opentelemetry.io/otel v1.0.0-RC1 h1:4CeoX93DNTWt8awGK9JmNXzF9j7TyOu9upscEdtcdXc= +go.opentelemetry.io/otel v1.0.0-RC1/go.mod h1:x9tRa9HK4hSSq7jf2TKbqFbtt58/TGk0f9XiEYISI1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.0-RC1 h1:GHKxjc4EDldz8ScMDpiNwX4BAub6wGFUUo5Axm2BimU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.0-RC1/go.mod h1:FliQjImlo7emZVjixV8nbDMAa4iAkcWTE9zzSEOiEPw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.0-RC1 h1:ZOQXuxKJ9evGspu3LvbZxx3KOOQvKAPBJVMOfGf1cOM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.0-RC1/go.mod h1:cDwRc2Jrh5Gku1peGK8p9rRuX/Uq2OtVmLicjlw2WYU= +go.opentelemetry.io/otel/oteltest v1.0.0-RC1 h1:G685iP3XiskCwk/z0eIabL55XUl2gk0cljhGk9sB0Yk= +go.opentelemetry.io/otel/oteltest v1.0.0-RC1/go.mod h1:+eoIG0gdEOaPNftuy1YScLr1Gb4mL/9lpDkZ0JjMRq4= +go.opentelemetry.io/otel/sdk v1.0.0-RC1 h1:Sy2VLOOg24bipyC29PhuMXYNJrLsxkie8hyI7kUlG9Q= +go.opentelemetry.io/otel/sdk v1.0.0-RC1/go.mod h1:kj6yPn7Pgt5ByRuwesbaWcRLA+V7BSDg3Hf8xRvsvf8= +go.opentelemetry.io/otel/trace v1.0.0-RC1 h1:jrjqKJZEibFrDz+umEASeU3LvdVyWKlnTh7XEfwrT58= +go.opentelemetry.io/otel/trace v1.0.0-RC1/go.mod h1:86UHmyHWFEtWjfWPSbu0+d0Pf9Q6e1U+3ViBOc+NXAg= +go.opentelemetry.io/proto/otlp v0.9.0 h1:C0g6TWmQYvjKRnljRULLWUVJGy8Uvu0NEL/5frY2/t4= +go.opentelemetry.io/proto/otlp v0.9.0/go.mod h1:1vKfU9rv61e9EVGthD1zNvUbiwPcimSsOPU9brfSHJg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/instrumentation/github.com/aws/otellambda/lambda.go b/instrumentation/github.com/aws/otellambda/lambda.go new file mode 100644 index 00000000000..1ef0271ebb0 --- /dev/null +++ b/instrumentation/github.com/aws/otellambda/lambda.go @@ -0,0 +1,351 @@ +package otellambda + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "reflect" + "runtime" + "strings" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-lambda-go/lambdacontext" + + lambdadetector "go.opentelemetry.io/contrib/detectors/aws/lambda" + "go.opentelemetry.io/contrib/propagators/aws/xray" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" +) + +const ( + tracerName = "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda" +) + +var tp *sdktrace.TracerProvider +var errorLogger = log.New(log.Writer(), "OTel Lambda Error: ", 0) + +func init() { + otel.SetTextMapPropagator(xray.Propagator{}) +} + +func initTracerProvider() { + ctx := context.Background() + + exp, err := otlptracegrpc.New(ctx, otlptracegrpc.WithInsecure()) + if err != nil { + errorLogger.Printf("failed to initialize exporter: %v\n", err) + return + } + + detector := lambdadetector.NewResourceDetector() + res, err := detector.Detect(ctx) + if err != nil { + errorLogger.Printf("failed to detect lambda resources: %v\n", err) + return + } + + tp = sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp), + sdktrace.WithIDGenerator(xray.NewIDGenerator()), + sdktrace.WithResource(res), + ) + + // Set the traceprovider + otel.SetTracerProvider(tp) +} + +func errorHandler(e error) func(context.Context, interface{}) (interface{}, error) { + return func(context.Context, interface{}) (interface{}, error) { + return nil, e + } +} + +// Ensure handler takes 0-2 values, with context +// as its first value if two arguments exist +func validateArguments(handler reflect.Type) (bool, error) { + handlerTakesContext := false + if handler.NumIn() > 2 { + return false, fmt.Errorf("handlers may not take more than two arguments, but handler takes %d", handler.NumIn()) + } else if handler.NumIn() > 0 { + contextType := reflect.TypeOf((*context.Context)(nil)).Elem() + argumentType := handler.In(0) + handlerTakesContext = argumentType.Implements(contextType) + if handler.NumIn() > 1 && !handlerTakesContext { + return false, fmt.Errorf("handler takes two arguments, but the first is not Context. got %s", argumentType.Kind()) + } + } + + return handlerTakesContext, nil +} + +// Ensure handler returns 0-2 values, with an error +// as its first value if any exist +func validateReturns(handler reflect.Type) error { + errorType := reflect.TypeOf((*error)(nil)).Elem() + + switch n := handler.NumOut(); { + case n > 2: + return fmt.Errorf("handler may not return more than two values") + case n > 1: + if !handler.Out(1).Implements(errorType) { + return fmt.Errorf("handler returns two values, but the second does not implement error") + } + case n == 1: + if !handler.Out(0).Implements(errorType) { + return fmt.Errorf("handler returns a single value, but it does not implement error") + } + } + + return nil +} + +// Wraps and calls customer lambda handler then unpacks response as necessary +func wrapperInternals(handlerFunc interface{}, event reflect.Value, ctx context.Context, takesContext bool) (interface{}, error) { + wrappedLambdaHandler := reflect.ValueOf(wrapper(handlerFunc)) + + argsWrapped := []reflect.Value{reflect.ValueOf(ctx), event, reflect.ValueOf(takesContext)} + response := wrappedLambdaHandler.Call(argsWrapped)[0].Interface().([]reflect.Value) + + // convert return values into (interface{}, error) + var err error + if len(response) > 0 { + if errVal, ok := response[len(response)-1].Interface().(error); ok { + err = errVal + } + } + var val interface{} + if len(response) > 1 { + val = response[0].Interface() + } + + return val, err +} + +// converts the given payload to the correct event type +func payloadToEvent(eventType reflect.Type, payload interface{}) (reflect.Value, error) { + event := reflect.New(eventType) + + // lambda SDK normally unmarshalls to customer event type, however + // with the wrapper the SDK unmarshalls to map[string]interface{} + // due to our use of reflection. Therefore we must convert this map + // to customer's desired event, we do so by simply re-marshalling then + // unmarshalling to the desired event type + remarshalledPayload, err := json.Marshal(payload) + if err != nil { + return reflect.Value{}, err + } + + if err := json.Unmarshal(remarshalledPayload, event.Interface()); err != nil { + return reflect.Value{}, err + } + return event, nil +} + +// LambdaHandlerWrapper Provides a lambda handler which wraps customer lambda handler with OTel Tracing +func LambdaHandlerWrapper(handlerFunc interface{}) interface{} { + if handlerFunc == nil { + return errorHandler(fmt.Errorf("handler is nil")) + } + handlerType := reflect.TypeOf(handlerFunc) + if handlerType.Kind() != reflect.Func { + return errorHandler(fmt.Errorf("handler kind %s is not %s", handlerType.Kind(), reflect.Func)) + } + + takesContext, err := validateArguments(handlerType) + if err != nil { + return errorHandler(err) + } + + if err := validateReturns(handlerType); err != nil { + return errorHandler(err) + } + + // note we will always take context to capture lambda context, + // regardless of whether customer takes context + if handlerType.NumIn() == 0 || handlerType.NumIn() == 1 && takesContext { + return func(ctx context.Context) (interface{}, error) { + var temp *interface{} + event := reflect.ValueOf(temp) + return wrapperInternals(handlerFunc, event, ctx, takesContext) + } + } else { // customer either takes both context and payload or just payload + return func(ctx context.Context, payload interface{}) (interface{}, error) { + event, err := payloadToEvent(handlerType.In(handlerType.NumIn()-1), payload) + if err != nil { + return nil, err + } + return wrapperInternals(handlerFunc, event.Elem(), ctx, takesContext) + } + } +} + +// basic implementation of TextMapCarrier +// which wraps the default map type +type mapCarrier map[string]string + +// Compile time check our mapCarrier implements propagation.TextMapCarrier +var _ propagation.TextMapCarrier = mapCarrier{} + +// Get returns the value associated with the passed key. +func (mc mapCarrier) Get(key string) string { + return mc[key] +} + +// Set stores the key-value pair. +func (mc mapCarrier) Set(key string, value string) { + mc[key] = value +} + +// Keys lists the keys stored in this carrier. +func (mc mapCarrier) Keys() []string { + keys := make([]string, len(mc)) + i := 0 + for k := range mc { + keys[i] = k + i++ + } + return keys +} + +// Adds OTel span surrounding customer handler call +func wrapper(handlerFunc interface{}) func(ctx context.Context, event interface{}, takesContext bool) []reflect.Value { + return func(ctx context.Context, event interface{}, takesContext bool) []reflect.Value { + + ctx, span := tracingBegin(ctx) + defer tracingEnd(ctx, span) + + handler := reflect.ValueOf(handlerFunc) + var args []reflect.Value + if takesContext { + args = append(args, reflect.ValueOf(ctx)) + } + if eventExists(event) { + args = append(args, reflect.ValueOf(event)) + } + + response := handler.Call(args) + + return response + } +} + +// Determine if an interface{} is nil or the +// if the reflect.Value of the event is nil +func eventExists(event interface{}) bool { + if event == nil { + return false + } + + // reflect.Value.isNil() can only be called on + // Values of certain Kinds. Unsupported Kinds + // will panic rather than return false + switch reflect.TypeOf(event).Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: + return !reflect.ValueOf(event).IsNil() + } + return true +} + +type wrappedHandler struct { + handler lambda.Handler +} + +// Compile time check our Handler implements lambda.Handler +var _ lambda.Handler = wrappedHandler{} + +// Invoke adds OTel span surrounding customer Handler invocation +func (h wrappedHandler) Invoke(ctx context.Context, payload []byte) ([]byte, error) { + + ctx, span := tracingBegin(ctx) + defer tracingEnd(ctx, span) + + response, err := h.handler.Invoke(ctx, payload) + if err != nil { + return nil, err + } + + return response, nil +} + +// HandlerWrapper Provides a Handler which wraps customer Handler with OTel Tracing +func HandlerWrapper(handler lambda.Handler) lambda.Handler { + return wrappedHandler{handler: handler} +} + +// Logic to start OTel Tracing +func tracingBegin(ctx context.Context) (context.Context, trace.Span) { + // Add trace id to context + xrayTraceId := os.Getenv("_X_AMZN_TRACE_ID") + mc := mapCarrier{} + mc.Set("X-Amzn-Trace-Id", xrayTraceId) + propagator := xray.Propagator{} + ctx = propagator.Extract(ctx, mc) + + // If tracer provider initialization failed we + // will attempt to initialize once per invocation + if tp == nil { + initTracerProvider() + } + + // if tracer provider successfully initializes then + // we add tracing, otherwise do customer business + // logic with no tracing + if tp != nil { + // Get a named tracer with package path as its name. + tracer := tp.Tracer(tracerName) + + var span trace.Span + spanName := os.Getenv("AWS_LAMBDA_FUNCTION_NAME") + + var attributes []attribute.KeyValue + lc, ok := lambdacontext.FromContext(ctx) + if !ok { + errorLogger.Println("failed to load lambda context from context, ensure tracing enabled in Lambda") + } + if lc != nil { + ctxRequestID := lc.AwsRequestID + attributes = append(attributes, attribute.KeyValue{Key: semconv.FaaSExecutionKey, Value: attribute.StringValue(ctxRequestID)}) + + // Resource attrs added as span attr due to static tp + // being created without meaningful context + ctxFunctionArn := lc.InvokedFunctionArn + attributes = append(attributes, attribute.KeyValue{Key: semconv.FaaSIDKey, Value: attribute.StringValue(ctxFunctionArn)}) + arnParts := strings.Split(ctxFunctionArn, ":") + if len(arnParts) >= 5 { + attributes = append(attributes, attribute.KeyValue{Key: semconv.CloudAccountIDKey, Value: attribute.StringValue(arnParts[4])}) + } + } + + ctx, span = tracer.Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindServer), trace.WithAttributes(attributes...)) + + return ctx, span + } + return ctx, nil +} + +// Logic to wrap up OTel Tracing +func tracingEnd(ctx context.Context, span trace.Span) { + if tp != nil { + // span will be valid if tp is not nil + span.End() + + // yield processor to attempt to attempt to ensure + // all spans have been consumed and are ready to be + // flushed - see https://github.com/open-telemetry/opentelemetry-go/issues/2080 + // to be removed upon resolution of above issue + runtime.Gosched() + + // force flush any tracing data since lambda may freeze + err := tp.ForceFlush(ctx) + if err != nil { + errorLogger.Println("failed to force a flush, lambda may freeze before instrumentation exported: ", err) + } + } +} diff --git a/instrumentation/github.com/aws/otellambda/lambda_test.go b/instrumentation/github.com/aws/otellambda/lambda_test.go new file mode 100644 index 00000000000..a1ac6939b19 --- /dev/null +++ b/instrumentation/github.com/aws/otellambda/lambda_test.go @@ -0,0 +1,436 @@ +package otellambda + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + "sync" + "testing" + "time" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-lambda-go/lambda/messages" + "github.com/aws/aws-lambda-go/lambdacontext" + "github.com/stretchr/testify/assert" + + lambdadetector "go.opentelemetry.io/contrib/detectors/aws/lambda" + "go.opentelemetry.io/contrib/propagators/aws/xray" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" +) + +var ( + mockLambdaContext = lambdacontext.LambdaContext{ + AwsRequestID: "123", + InvokedFunctionArn: "arn:partition:service:region:account-id:resource-type:resource-id", + Identity: lambdacontext.CognitoIdentity{ + CognitoIdentityID: "someId", + CognitoIdentityPoolID: "somePoolId", + }, + ClientContext: lambdacontext.ClientContext{}, + } + mockContext = xray.Propagator{}.Extract(lambdacontext.NewContext(context.TODO(), &mockLambdaContext), + mapCarrier{ + "X-Amzn-Trace-Id": "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1", + }) +) + +type mockIdGenerator struct { + sync.Mutex + traceCount int + spanCount int +} + +func (m *mockIdGenerator) NewIDs(_ context.Context) (trace.TraceID, trace.SpanID) { + m.Lock() + defer m.Unlock() + m.traceCount += 1 + m.spanCount += 1 + return [16]byte{byte(m.traceCount)}, [8]byte{byte(m.spanCount)} +} + +func (m *mockIdGenerator) NewSpanID(_ context.Context, _ trace.TraceID) trace.SpanID { + m.Lock() + defer m.Unlock() + m.spanCount += 1 + return [8]byte{byte(m.spanCount)} +} + +var _ sdktrace.IDGenerator = &mockIdGenerator{} + +type emptyHandler struct{} + +func (h emptyHandler) Invoke(_ context.Context, _ []byte) ([]byte, error) { + return nil, nil +} + +var _ lambda.Handler = emptyHandler{} + +func initMockTracerProvider() *tracetest.InMemoryExporter { + ctx := context.Background() + + exp := tracetest.NewInMemoryExporter() + + detector := lambdadetector.NewResourceDetector() + res, err := detector.Detect(ctx) + if err != nil { + errorLogger.Printf("failed to detect lambda resources: %v\n", err) + return nil + } + + tp = sdktrace.NewTracerProvider( + sdktrace.WithSyncer(exp), + sdktrace.WithIDGenerator(&mockIdGenerator{}), + sdktrace.WithResource(res), + ) + + // Set the traceprovider + otel.SetTracerProvider(tp) + + return exp +} + +func setEnvVars() { + _ = os.Setenv("AWS_LAMBDA_FUNCTION_NAME", "testFunction") + _ = os.Setenv("AWS_REGION", "us-texas-1") + _ = os.Setenv("AWS_LAMBDA_FUNCTION_VERSION", "$LATEST") + _ = os.Setenv("_X_AMZN_TRACE_ID", "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1") +} + +func TestLambdaHandlerSignatures(t *testing.T) { + setEnvVars() + + // for these tests we do not care about the tracing and + // so we will ignore it the in memory span exporter + _ = initMockTracerProvider() + + emptyPayload := "" + testCases := []struct { + name string + handler interface{} + expected error + args []reflect.Value + }{ + { + name: "nil handler", + expected: errors.New("handler is nil"), + handler: nil, + args: []reflect.Value{reflect.ValueOf(mockContext), reflect.ValueOf(emptyPayload)}, + }, + { + name: "handler is not a function", + expected: errors.New("handler kind struct is not func"), + handler: struct{}{}, + args: []reflect.Value{reflect.ValueOf(mockContext), reflect.ValueOf(emptyPayload)}, + }, + { + name: "handler declares too many arguments", + expected: errors.New("handlers may not take more than two arguments, but handler takes 3"), + handler: func(n context.Context, x string, y string) error { + return nil + }, + args: []reflect.Value{reflect.ValueOf(mockContext), reflect.ValueOf(emptyPayload)}, + }, + { + name: "two argument handler does not have context as first argument", + expected: errors.New("handler takes two arguments, but the first is not Context. got string"), + handler: func(a string, x context.Context) error { + return nil + }, + args: []reflect.Value{reflect.ValueOf(mockContext), reflect.ValueOf(emptyPayload)}, + }, + { + name: "handler returns too many values", + expected: errors.New("handler may not return more than two values"), + handler: func() (error, error, error) { + return nil, nil, nil + }, + args: []reflect.Value{reflect.ValueOf(mockContext), reflect.ValueOf(emptyPayload)}, + }, + { + name: "handler returning two values does not declare error as the second return value", + expected: errors.New("handler returns two values, but the second does not implement error"), + handler: func() (error, string) { + return nil, "hello" + }, + args: []reflect.Value{reflect.ValueOf(mockContext), reflect.ValueOf(emptyPayload)}, + }, + { + name: "handler returning a single value does not implement error", + expected: errors.New("handler returns a single value, but it does not implement error"), + handler: func() string { + return "hello" + }, + args: []reflect.Value{reflect.ValueOf(mockContext), reflect.ValueOf(emptyPayload)}, + }, + { + name: "no args or return value should not result in error", + expected: nil, + handler: func() { + }, + args: []reflect.Value{reflect.ValueOf(mockContext)}, // reminder - customer takes no args but wrapped handler always takes context from lambda + }, + } + for i, testCase := range testCases { + testCase := testCase + t.Run(fmt.Sprintf("testCase[%d] %s", i, testCase.name), func(t *testing.T) { + lambdaHandler := LambdaHandlerWrapper(testCase.handler) + handler := reflect.ValueOf(lambdaHandler) + resp := handler.Call(testCase.args) + assert.Equal(t, 2, len(resp)) + assert.Equal(t, testCase.expected, resp[1].Interface()) + }) + } +} + +type expected struct { + val interface{} + err error +} + +func TestHandlerInvokes(t *testing.T) { + setEnvVars() + + // for these tests we do not care about the tracing and + // so we will ignore it the in memory span exporter + _ = initMockTracerProvider() + + hello := func(s string) string { + return fmt.Sprintf("Hello %s!", s) + } + + testCases := []struct { + name string + input interface{} + expected expected + handler interface{} + }{ + { + name: "string input and return without context", + input: "Lambda", + expected: expected{`"Hello Lambda!"`, nil}, + handler: func(name string) (string, error) { + return hello(name), nil + }, + }, + { + name: "string input and return with context", + input: "Lambda", + expected: expected{`"Hello Lambda!"`, nil}, + handler: func(ctx context.Context, name string) (string, error) { + return hello(name), nil + }, + }, + { + name: "no input with response event and simple error", + input: nil, + expected: expected{"", errors.New("bad stuff")}, + handler: func() (interface{}, error) { + return nil, errors.New("bad stuff") + }, + }, + { + name: "input with response event and simple error", + input: "Lambda", + expected: expected{"", errors.New("bad stuff")}, + handler: func(e interface{}) (interface{}, error) { + return nil, errors.New("bad stuff") + }, + }, + { + name: "input and context with response event and simple error", + input: "Lambda", + expected: expected{"", errors.New("bad stuff")}, + handler: func(ctx context.Context, e interface{}) (interface{}, error) { + return nil, errors.New("bad stuff") + }, + }, + { + name: "input with response event and complex error", + input: "Lambda", + expected: expected{"", messages.InvokeResponse_Error{Message: "message", Type: "type"}}, + handler: func(e interface{}) (interface{}, error) { + return nil, messages.InvokeResponse_Error{Message: "message", Type: "type"} + }, + }, + { + name: "basic input struct serialization", + input: struct{ Custom int }{9001}, + expected: expected{`9001`, nil}, + handler: func(event struct{ Custom int }) (int, error) { + return event.Custom, nil + }, + }, + { + name: "basic output struct serialization", + input: 9001, + expected: expected{`{"Number":9001}`, nil}, + handler: func(event int) (struct{ Number int }, error) { + return struct{ Number int }{event}, nil + }, + }, + } + + // test invocation via a lambda handler + for i, testCase := range testCases { + testCase := testCase + t.Run(fmt.Sprintf("lambdaHandlerTestCase[%d] %s", i, testCase.name), func(t *testing.T) { + lambdaHandler := LambdaHandlerWrapper(testCase.handler) + handler := reflect.ValueOf(lambdaHandler) + handlerType := handler.Type() + + var args []reflect.Value + args = append(args, reflect.ValueOf(mockContext)) + if handlerType.NumIn() > 1 { + args = append(args, reflect.ValueOf(testCase.input)) + } + response := handler.Call(args) + assert.Equal(t, 2, len(response)) + if testCase.expected.err != nil { + assert.Equal(t, testCase.expected.err, response[handlerType.NumOut()-1].Interface()) + } else { + assert.Nil(t, response[handlerType.NumOut()-1].Interface()) + responseValMarshalled, _ := json.Marshal(response[0].Interface()) + assert.Equal(t, testCase.expected.val, string(responseValMarshalled)) + } + }) + } + + // test invocation via a Handler + for i, testCase := range testCases { + testCase := testCase + t.Run(fmt.Sprintf("handlerTestCase[%d] %s", i, testCase.name), func(t *testing.T) { + handler := HandlerWrapper(lambda.NewHandler(testCase.handler)) + inputPayload, _ := json.Marshal(testCase.input) + response, err := handler.Invoke(mockContext, inputPayload) + if testCase.expected.err != nil { + assert.Equal(t, testCase.expected.err, err) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.expected.val, string(response)) + } + }) + } +} + +var expectedTraceID, _ = trace.TraceIDFromHex("5759e988bd862e3fe1be46a994272793") +var expectedSpanStub = tracetest.SpanStub{ + Name: "testFunction", + SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: expectedTraceID, + SpanID: trace.SpanID{1}, + TraceFlags: 1, + TraceState: trace.TraceState{}, + Remote: false, + }), + Parent: trace.SpanContextFromContext(mockContext), + SpanKind: trace.SpanKindServer, + StartTime: time.Time{}, + EndTime: time.Time{}, + Attributes: []attribute.KeyValue{attribute.String("faas.execution", "123"), + attribute.String("faas.id", "arn:partition:service:region:account-id:resource-type:resource-id"), + attribute.String("cloud.account.id", "account-id")}, + Events: nil, + Links: nil, + Status: sdktrace.Status{}, + DroppedAttributes: 0, + DroppedEvents: 0, + DroppedLinks: 0, + ChildSpanCount: 0, + Resource: resource.NewWithAttributes(semconv.SchemaURL, + attribute.String("cloud.provider", "aws"), + attribute.String("cloud.region", "us-texas-1"), + attribute.String("faas.name", "testFunction"), + attribute.String("faas.version", "$LATEST")), + InstrumentationLibrary: instrumentation.Library{Name: "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda"}, +} + +func assertStubEqualsIgnoreTime(t *testing.T, expected tracetest.SpanStub, actual tracetest.SpanStub) { + assert.Equal(t, expected.Name, actual.Name) + assert.Equal(t, expected.SpanContext, actual.SpanContext) + assert.Equal(t, expected.Parent, actual.Parent) + assert.Equal(t, expected.SpanKind, actual.SpanKind) + assert.Equal(t, expected.Attributes, actual.Attributes) + assert.Equal(t, expected.Events, actual.Events) + assert.Equal(t, expected.Links, actual.Links) + assert.Equal(t, expected.Status, actual.Status) + assert.Equal(t, expected.DroppedAttributes, actual.DroppedAttributes) + assert.Equal(t, expected.DroppedEvents, actual.DroppedEvents) + assert.Equal(t, expected.DroppedLinks, actual.DroppedLinks) + assert.Equal(t, expected.ChildSpanCount, actual.ChildSpanCount) + assert.Equal(t, expected.Resource, actual.Resource) + assert.Equal(t, expected.InstrumentationLibrary, actual.InstrumentationLibrary) +} + +func TestLambdaHandlerWrapperTracing(t *testing.T) { + setEnvVars() + memExporter := initMockTracerProvider() + + customerHandler := func() (string, error) { + return "hello world", nil + } + + wrapped := LambdaHandlerWrapper(customerHandler) + wrappedCallable := reflect.ValueOf(wrapped) + resp := wrappedCallable.Call([]reflect.Value{reflect.ValueOf(mockContext)}) + assert.Len(t, resp, 2) + assert.Equal(t, "hello world", resp[0].Interface()) + assert.Nil(t, resp[1].Interface()) + + assert.Len(t, memExporter.GetSpans(), 1) + stub := memExporter.GetSpans()[0] + assertStubEqualsIgnoreTime(t, expectedSpanStub, stub) +} + +func TestHandlerWrapperTracing(t *testing.T) { + setEnvVars() + memExporter := initMockTracerProvider() + + wrapped := HandlerWrapper(emptyHandler{}) + _, err := wrapped.Invoke(mockContext, nil) + assert.NoError(t, err) + + assert.Len(t, memExporter.GetSpans(), 1) + stub := memExporter.GetSpans()[0] + assertStubEqualsIgnoreTime(t, expectedSpanStub, stub) +} + +func BenchmarkLambdaHandlerWrapper(b *testing.B) { + setEnvVars() + initMockTracerProvider() + + customerHandler := func(ctx context.Context, payload int) error { + return nil + } + wrapped := LambdaHandlerWrapper(customerHandler) + wrappedCallable := reflect.ValueOf(wrapped) + ctx := reflect.ValueOf(mockContext) + payload := reflect.ValueOf(0) + args := []reflect.Value{ctx, payload} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + wrappedCallable.Call(args) + } +} + +func BenchmarkHandlerWrapper(b *testing.B) { + setEnvVars() + initMockTracerProvider() + + wrapped := HandlerWrapper(emptyHandler{}) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = wrapped.Invoke(mockContext, []byte{0}) + } +}