diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9e4ed404b..5cf3f91db 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -89,6 +89,10 @@ updates: directory: "/instrumentation/aws_sdk" schedule: interval: weekly +- package-ecosystem: bundler + directory: "/instrumentation/aws_lambda" + schedule: + interval: weekly - package-ecosystem: bundler directory: "/instrumentation/active_record" schedule: diff --git a/.github/workflows/ci-instrumentation.yml b/.github/workflows/ci-instrumentation.yml index 728d0739e..9f8466a8a 100644 --- a/.github/workflows/ci-instrumentation.yml +++ b/.github/workflows/ci-instrumentation.yml @@ -29,6 +29,7 @@ jobs: - active_support - all - aws_sdk + - aws_lambda - base - concurrent_ruby - delayed_job @@ -91,6 +92,7 @@ jobs: [[ "${{ matrix.gem }}" == "active_record" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "active_support" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "aws_sdk" ]] && echo "skip=true" >> $GITHUB_OUTPUT + [[ "${{ matrix.gem }}" == "aws_lambda" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "delayed_job" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "graphql" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "http" ]] && echo "skip=true" >> $GITHUB_OUTPUT diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6d6a159dc..b66849230 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -16,6 +16,7 @@ "instrumentation/base": "0.22.3", "instrumentation/active_record": "0.7.0", "instrumentation/aws_sdk": "0.5.0", + "instrumentation/aws_lambda": "0.1.0", "instrumentation/lmdb": "0.22.1", "instrumentation/http": "0.23.2", "instrumentation/graphql": "0.27.0", diff --git a/.toys/.data/releases.yml b/.toys/.data/releases.yml index 563b9be80..db5722c22 100644 --- a/.toys/.data/releases.yml +++ b/.toys/.data/releases.yml @@ -100,6 +100,10 @@ gems: directory: instrumentation/aws_sdk version_constant: [OpenTelemetry, Instrumentation, AwsSdk, VERSION] + - name: opentelemetry-instrumentation-aws_lambda + directory: instrumentation/aws_lambda + version_constant: [OpenTelemetry, Instrumentation, AwsLambda, VERSION] + - name: opentelemetry-instrumentation-lmdb directory: instrumentation/lmdb version_constant: [OpenTelemetry, Instrumentation, LMDB, VERSION] diff --git a/instrumentation/aws_lambda/.rubocop.yml b/instrumentation/aws_lambda/.rubocop.yml new file mode 100644 index 000000000..1248a2f82 --- /dev/null +++ b/instrumentation/aws_lambda/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: ../../.rubocop.yml diff --git a/instrumentation/aws_lambda/.yardopts b/instrumentation/aws_lambda/.yardopts new file mode 100644 index 000000000..7bd7686dc --- /dev/null +++ b/instrumentation/aws_lambda/.yardopts @@ -0,0 +1,9 @@ +--no-private +--title=OpenTelemetry AWS Lambda Instrumentation +--markup=markdown +--main=README.md +./lib/opentelemetry/instrumentation/**/*.rb +./lib/opentelemetry/instrumentation.rb +- +README.md +CHANGELOG.md diff --git a/instrumentation/aws_lambda/CHANGELOG.md b/instrumentation/aws_lambda/CHANGELOG.md new file mode 100644 index 000000000..1fb233945 --- /dev/null +++ b/instrumentation/aws_lambda/CHANGELOG.md @@ -0,0 +1 @@ +# Release History: opentelemetry-instrumentation-aws_lambda diff --git a/instrumentation/aws_lambda/Gemfile b/instrumentation/aws_lambda/Gemfile new file mode 100644 index 000000000..a03fdb17e --- /dev/null +++ b/instrumentation/aws_lambda/Gemfile @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gemspec + +group :test do + gem 'opentelemetry-instrumentation-base', path: '../base' + gem 'webrick', '~> 1.7' +end diff --git a/instrumentation/aws_lambda/LICENSE b/instrumentation/aws_lambda/LICENSE new file mode 100644 index 000000000..1ef7dad2c --- /dev/null +++ b/instrumentation/aws_lambda/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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. diff --git a/instrumentation/aws_lambda/README.md b/instrumentation/aws_lambda/README.md new file mode 100644 index 000000000..90dc918c9 --- /dev/null +++ b/instrumentation/aws_lambda/README.md @@ -0,0 +1,58 @@ +# OpenTelemetry AWS-Lambda Instrumentation + +The OpenTelemetry `aws-lambda` gem is a community-maintained instrumentation for [AWS Lambda functions](https://docs.aws.amazon.com/lambda/latest/dg/ruby-handler.html). + +## How do I get started? + +Installation of the `opentelemetry-instrumentation-aws_lambda` gem is handled by the [OpenTelemetry Lambda Layer for Ruby](https://github.com/open-telemetry/opentelemetry-lambda/tree/main/ruby). + +We do not advise installing the `opentelemetry-instrumentation-aws_lambda` gem directly into your Ruby lambda. Instead, clone the [OpenTelemetry Lambda Layer for Ruby](https://github.com/open-telemetry/opentelemetry-lambda/tree/main/ruby) and build the layer locally. Then, save it in your AWS account. + +## Usage + +From the Lambda Layer side, create the wrapper. More information can be found at https://github.com/open-telemetry/opentelemetry-lambda/tree/main/ruby + +Below is an example of `ruby/src/layer/wrapper.rb`, where you can configure the layer to suit your needs before building it: +```ruby +require 'opentelemetry/sdk' +require 'opentelemetry/instrumentation/aws_lambda' +OpenTelemetry::SDK.configure do |c| + c.service_name = '' + c.use 'OpenTelemetry::Instrumentation::AwsLambda' +end + +def otel_wrapper(event:, context:) + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new() + otel_wrapper.call_wrapped(event: event, context: context) +end +``` + +## Example + +To run the example: + +1. `cd` to the examples directory and install gems + * `cd example` + * `bundle install` +2. Run the sample client script + * `ruby trace_demonstration.rb` + +This will run SNS publish command, printing OpenTelemetry traces to the console as it goes. + +## How can I get involved? + +The `opentelemetry-instrumentation-aws_lambda` gem source is [on github][repo-github], along with related gems including `opentelemetry-api` and `opentelemetry-sdk`. + +The OpenTelemetry Ruby gems are maintained by the OpenTelemetry-Ruby special interest group (SIG). You can get involved by joining us in [GitHub Discussions][discussions-url] or attending our weekly meeting. See the [meeting calendar][community-meetings] for dates and times. For more information on this and other language SIGs, see the OpenTelemetry [community page][ruby-sig]. + +## License + +Apache 2.0 license. See [LICENSE][license-github] for more information. + +[aws-sdk-home]: https://github.com/aws/aws-sdk-ruby +[bundler-home]: https://bundler.io +[repo-github]: https://github.com/open-telemetry/opentelemetry-ruby +[license-github]: https://github.com/open-telemetry/opentelemetry-ruby-contrib/blob/main/LICENSE +[ruby-sig]: https://github.com/open-telemetry/community#ruby-sig +[community-meetings]: https://github.com/open-telemetry/community#community-meetings +[discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions diff --git a/instrumentation/aws_lambda/Rakefile b/instrumentation/aws_lambda/Rakefile new file mode 100644 index 000000000..4b0e9b5a8 --- /dev/null +++ b/instrumentation/aws_lambda/Rakefile @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'yard' +require 'rubocop/rake_task' + +RuboCop::RakeTask.new + +Rake::TestTask.new :test do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/**/*_test.rb'] + t.warning = false +end + +YARD::Rake::YardocTask.new do |t| + t.stats_options = ['--list-undoc'] +end + +if RUBY_ENGINE == 'truffleruby' + task default: %i[test] +else + task default: %i[test rubocop yard] +end diff --git a/instrumentation/aws_lambda/example/Gemfile b/instrumentation/aws_lambda/example/Gemfile new file mode 100644 index 000000000..934ac342f --- /dev/null +++ b/instrumentation/aws_lambda/example/Gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'opentelemetry-instrumentation-aws_lambda', path: '../' +gem 'opentelemetry-sdk' diff --git a/instrumentation/aws_lambda/example/sample.rb b/instrumentation/aws_lambda/example/sample.rb new file mode 100644 index 000000000..7efc9561a --- /dev/null +++ b/instrumentation/aws_lambda/example/sample.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +def handler(event:, context:) + puts "Success" +end diff --git a/instrumentation/aws_lambda/example/trace_demonstration.rb b/instrumentation/aws_lambda/example/trace_demonstration.rb new file mode 100755 index 000000000..b135bad5b --- /dev/null +++ b/instrumentation/aws_lambda/example/trace_demonstration.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'rubygems' +require 'bundler/setup' +require_relative './sample' + +Bundler.require + +# Export traces to console by default +ENV['OTEL_TRACES_EXPORTER'] ||= 'console' +ENV['ORIG_HANDLER'] ||= 'sample.handler' + +OpenTelemetry::SDK.configure do |c| + c.use 'OpenTelemetry::Instrumentation::AwsLambda' +end + +class MockLambdaContext + attr_reader :aws_request_id, :invoked_function_arn, :function_name + + def initialize(aws_request_id:, invoked_function_arn:, function_name:) + @aws_request_id = aws_request_id + @invoked_function_arn = invoked_function_arn + @function_name = function_name + end +end + +# To accommendate the test case, handler class doesn't need to require the sample file if it's required here +# In lambda environment, the env will find the handler file. +module OpenTelemetry + module Instrumentation + module AwsLambda + class Handler + def resolve_original_handler + original_handler = ENV['ORIG_HANDLER'] || ENV['_HANDLER'] || '' + original_handler_parts = original_handler.split('.') + if original_handler_parts.size == 2 + handler_file, @handler_method = original_handler_parts + elsif original_handler_parts.size == 3 + handler_file, @handler_class, @handler_method = original_handler_parts + else + OpenTelemetry.logger.warn("aws-lambda instrumentation: Invalid handler #{original_handler}, must be of form FILENAME.METHOD or FILENAME.CLASS.METHOD.") + end + + # require handler_file #-> don't require file for this sample test + + original_handler + end + end + end + end +end + +def otel_wrapper(event:, context:) + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new() + otel_wrapper.call_wrapped(event: event, context: context) +end + +# sample event obtained from sample test +event = { + "body" => nil, + "headers" => { + "Accept" => "*/*", + "Host" => "127.0.0.1:3000", + "User-Agent" => "curl/8.1.2", + "X-Forwarded-Port" => 3000, + "X-Forwarded-Proto" => "http" + }, + "httpMethod" => "GET", + "isBase64Encoded" => false, + "multiValueHeaders" => {}, + "multiValueQueryStringParameters" => nil, + "path" => "/", + "pathParameters" => nil, + "queryStringParameters" => nil, + "requestContext" => { + "accountId" => 123456789012, + "apiId" => 1234567890, + "domainName" => "127.0.0.1:3000", + "extendedRequestId" => nil, + "httpMethod" => "GET", + "identity" => {}, + "path" => "/", + "protocol" => "HTTP/1.1", + "requestId" => "db7f8e7a-4cc5-4f6d-987b-713d0d9052c3", + "requestTime" => "08/Nov/2023:19:09:59 +0000", + "requestTimeEpoch" => 1699470599, + "resourceId" => "123456", + "resourcePath" => "/", + "stage" => "api" + }, + "resource" => "/", + "stageVariables" => nil, + "version" => "1.0" +} + +context = MockLambdaContext.new(aws_request_id: "aws_request_id",invoked_function_arn: "invoked_function_arn",function_name: "function") + +otel_wrapper(event: event, context: context) # you should see Success before the trace diff --git a/instrumentation/aws_lambda/lib/opentelemetry-instrumentation-aws_lambda.rb b/instrumentation/aws_lambda/lib/opentelemetry-instrumentation-aws_lambda.rb new file mode 100644 index 000000000..c034f140f --- /dev/null +++ b/instrumentation/aws_lambda/lib/opentelemetry-instrumentation-aws_lambda.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require_relative 'opentelemetry/instrumentation' diff --git a/instrumentation/aws_lambda/lib/opentelemetry/instrumentation.rb b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation.rb new file mode 100644 index 000000000..8dd0f5127 --- /dev/null +++ b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +# OpenTelemetry is an open source observability framework, providing a +# general-purpose API, SDK, and related tools required for the instrumentation +# of cloud-native software, frameworks, and libraries. +# +# The OpenTelemetry module provides global accessors for telemetry objects. +# See the documentation for the `opentelemetry-api` gem for details. +module OpenTelemetry + # "Instrumentation" are specified by + # https://github.com/open-telemetry/opentelemetry-specification/blob/784635d01d8690c8f5fcd1f55bdbc8a13cf2f4f2/specification/glossary.md#instrumentation-library + # + # Instrumentation should be able to handle the case when the library is not installed on a user's system. + module Instrumentation + end +end + +require_relative 'instrumentation/aws_lambda' diff --git a/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda.rb b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda.rb new file mode 100644 index 000000000..fb8d41885 --- /dev/null +++ b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry' +require 'opentelemetry-instrumentation-base' + +module OpenTelemetry + module Instrumentation + # Contains the OpenTelemetry instrumentation for the aws_lambda gem + module AwsLambda + end + end +end + +require_relative 'aws_lambda/instrumentation' +require_relative 'aws_lambda/version' diff --git a/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda/handler.rb b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda/handler.rb new file mode 100644 index 000000000..7c9a32c18 --- /dev/null +++ b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda/handler.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module AwsLambda + AWS_TRIGGERS = ['aws:sqs', 'aws:s3', 'aws:sns', 'aws:dynamodb'].freeze + + # Handler class that creates a span around the _HANDLER + class Handler + attr_reader :handler_method, :handler_class + + # anytime when the code in a Lambda function is updated or the functional configuration is changed, + # the next invocation results in a cold start; therefore these instance variables will be up-to-date + def initialize + @flush_timeout = ENV.fetch('OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT', '30000').to_i + @original_handler = ENV['ORIG_HANDLER'] || ENV['_HANDLER'] || '' + @handler_class = nil + @handler_method = nil + @handler_file = nil + + resolve_original_handler + end + + # Try to record and re-raise any exception from the wrapped function handler + # Instrumentation should never raise its own exception + def call_wrapped(event:, context:) + parent_context = extract_parent_context(event) + + span_kind = nil + span_kind = if event['Records'] && AWS_TRIGGERS.include?(event['Records'].dig(0, 'eventSource')) + :consumer + else + :server + end + + original_handler_error = nil + original_response = nil + OpenTelemetry::Context.with_current(parent_context) do + span_attributes = otel_attributes(event, context) + span = tracer.start_span( + @original_handler, + attributes: span_attributes, + kind: span_kind + ) + + begin + response = call_original_handler(event: event, context: context) + status_code = response['statusCode'] || response[:statusCode] if response.is_a?(Hash) + span.set_attribute(OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE, status_code) if status_code + rescue StandardError => e + original_handler_error = e + ensure + original_response = response + end + rescue StandardError => e + OpenTelemetry.logger.error("aws-lambda instrumentation #{e.class}: #{e.message}") + ensure + if original_handler_error + span&.record_exception(original_handler_error) + span&.status = OpenTelemetry::Trace::Status.error(original_handler_error.message) + end + span&.finish + OpenTelemetry.tracer_provider.force_flush(timeout: @flush_timeout) + end + + raise original_handler_error if original_handler_error + + original_response + end + + def instrumentation_config + AwsLambda::Instrumentation.instance.config + end + + def tracer + AwsLambda::Instrumentation.instance.tracer + end + + private + + # we don't expose error if our code cause issue that block user's code + def resolve_original_handler + original_handler_parts = @original_handler.split('.') + if original_handler_parts.size == 2 + @handler_file, @handler_method = original_handler_parts + elsif original_handler_parts.size == 3 + @handler_file, @handler_class, @handler_method = original_handler_parts + else + OpenTelemetry.logger.error("aws-lambda instrumentation: Invalid handler #{original_handler}, must be of form FILENAME.METHOD or FILENAME.CLASS.METHOD.") + end + + require @handler_file if @handler_file + end + + def call_original_handler(event:, context:) + if @handler_class + Kernel.const_get(@handler_class).send(@handler_method, event: event, context: context) + else + __send__(@handler_method, event: event, context: context) + end + end + + # Extract parent context from request headers + # Downcase Traceparent and Tracestate because TraceContext::TextMapPropagator's TRACEPARENT_KEY and TRACESTATE_KEY are all lowercase + # If any error occur, rescue and give empty context + def extract_parent_context(event) + headers = event['headers'] || {} + headers.transform_keys! do |key| + %w[Traceparent Tracestate].include?(key) ? key.downcase : key + end + + OpenTelemetry.propagation.extract( + headers, + getter: OpenTelemetry::Context::Propagation.text_map_getter + ) + rescue StandardError => e + OpenTelemetry.logger.error("aws-lambda instrumentation exception occurred while extracting the parent context: #{e.message}") + OpenTelemetry::Context.empty + end + + # lambda event version 1.0 and version 2.0 + # https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + def v1_proxy_attributes(event) + attributes = { + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => event['httpMethod'], + OpenTelemetry::SemanticConventions::Trace::HTTP_ROUTE => event['resource'], + OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => event['resource'] + } + attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET] += "?#{event['queryStringParameters']}" if event['queryStringParameters'] + + headers = event['headers'] + if headers + attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_USER_AGENT] = headers['User-Agent'] + attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_SCHEME] = headers['X-Forwarded-Proto'] + attributes[OpenTelemetry::SemanticConventions::Trace::NET_HOST_NAME] = headers['Host'] + end + attributes + end + + def v2_proxy_attributes(event) + request_context = event['requestContext'] + attributes = { + OpenTelemetry::SemanticConventions::Trace::NET_HOST_NAME => request_context['domainName'], + OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => request_context['http']['method'], + OpenTelemetry::SemanticConventions::Trace::HTTP_USER_AGENT => request_context['http']['userAgent'], + OpenTelemetry::SemanticConventions::Trace::HTTP_ROUTE => request_context['http']['path'], + OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET => request_context['http']['path'] + } + attributes[OpenTelemetry::SemanticConventions::Trace::HTTP_TARGET] += "?#{event['rawQueryString']}" if event['rawQueryString'] + attributes + end + + # fass.trigger set to http: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#api-gateway + # TODO: need to update Semantic Conventions for invocation_id, trigger and resource_id + def otel_attributes(event, context) + span_attributes = {} + span_attributes['faas.invocation_id'] = context.aws_request_id + span_attributes['cloud.resource_id'] = context.invoked_function_arn + span_attributes[OpenTelemetry::SemanticConventions::Trace::AWS_LAMBDA_INVOKED_ARN] = context.invoked_function_arn + span_attributes[OpenTelemetry::SemanticConventions::Resource::CLOUD_ACCOUNT_ID] = context.invoked_function_arn.split(':')[4] + + if event['requestContext'] + request_attributes = event['version'] == '2.0' ? v2_proxy_attributes(event) : v1_proxy_attributes(event) + request_attributes[OpenTelemetry::SemanticConventions::Trace::FAAS_TRIGGER] = 'http' + span_attributes.merge!(request_attributes) + end + + if event['Records'] + trigger_attributes = trigger_type_attributes(event) + span_attributes.merge!(trigger_attributes) + end + + span_attributes + rescue StandardError => e + OpenTelemetry.logger.error("aws-lambda instrumentation exception occurred while preparing span attributes: #{e.message}") + {} + end + + # sqs spec for lambda: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/faas/aws-lambda.md#sqs + # current there is no spec for 'aws:sns', 'aws:s3' and 'aws:dynamodb' + def trigger_type_attributes(event) + attributes = {} + case event['Records'].dig(0, 'eventSource') + when 'aws:sqs' + attributes[OpenTelemetry::SemanticConventions::Trace::FAAS_TRIGGER] = 'pubsub' + attributes[OpenTelemetry::SemanticConventions::Trace::MESSAGING_OPERATION] = 'process' + attributes[OpenTelemetry::SemanticConventions::Trace::MESSAGING_SYSTEM] = 'AmazonSQS' + end + attributes + end + end + end + end +end diff --git a/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda/instrumentation.rb b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda/instrumentation.rb new file mode 100644 index 000000000..62f25aee5 --- /dev/null +++ b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda/instrumentation.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module AwsLambda + # Instrumentation class that detects and installs the AwsLambda instrumentation + class Instrumentation < OpenTelemetry::Instrumentation::Base + install do |_config| + require_dependencies + end + + # determine if current environment is lambda by checking _HANLDER or ORIG_HANDLER + present do + (ENV.key?('_HANDLER') || ENV.key?('ORIG_HANDLER')) + end + + private + + def require_dependencies + require_relative 'handler' + end + end + end + end +end diff --git a/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda/version.rb b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda/version.rb new file mode 100644 index 000000000..6723022fd --- /dev/null +++ b/instrumentation/aws_lambda/lib/opentelemetry/instrumentation/aws_lambda/version.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module AwsLambda + VERSION = '0.1.0' + end + end +end diff --git a/instrumentation/aws_lambda/opentelemetry-instrumentation-aws_lambda.gemspec b/instrumentation/aws_lambda/opentelemetry-instrumentation-aws_lambda.gemspec new file mode 100644 index 000000000..972f1ee70 --- /dev/null +++ b/instrumentation/aws_lambda/opentelemetry-instrumentation-aws_lambda.gemspec @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'opentelemetry/instrumentation/aws_lambda/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-instrumentation-aws_lambda' + spec.version = OpenTelemetry::Instrumentation::AwsLambda::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'AWS Lambda instrumentation for the OpenTelemetry framework' + spec.description = 'AWS Lambda instrumentation for the OpenTelemetry framework' + spec.homepage = 'https://github.com/open-telemetry/opentelemetry-ruby-contrib' + spec.license = 'Apache-2.0' + + spec.files = Dir.glob('lib/**/*.rb') + + Dir.glob('*.md') + + ['LICENSE', '.yardopts'] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 3.0' + + spec.add_dependency 'opentelemetry-api', '~> 1.0' + spec.add_dependency 'opentelemetry-instrumentation-base', '~> 0.22.1' + + spec.add_development_dependency 'appraisal', '~> 2.5' + spec.add_development_dependency 'bundler', '~> 2.4' + spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'opentelemetry-sdk', '~> 1.1' + spec.add_development_dependency 'opentelemetry-test-helpers', '~> 0.3' + spec.add_development_dependency 'pry' + spec.add_development_dependency 'pry-byebug' unless RUBY_ENGINE == 'jruby' + spec.add_development_dependency 'rspec-mocks' + spec.add_development_dependency 'rubocop', '~> 1.56.1' + spec.add_development_dependency 'rubocop-performance', '~> 1.19.1' + spec.add_development_dependency 'simplecov', '~> 0.17.1' + spec.add_development_dependency 'webmock', '~> 3.19' + spec.add_development_dependency 'yard', '~> 0.9' + + if spec.respond_to?(:metadata) + spec.metadata['changelog_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}/file/CHANGELOG.md" + spec.metadata['source_code_uri'] = 'https://github.com/open-telemetry/opentelemetry-ruby-contrib/tree/main/instrumentation/aws_lambda' + spec.metadata['bug_tracker_uri'] = 'https://github.com/open-telemetry/opentelemetry-ruby-contrib/issues' + spec.metadata['documentation_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}" + end +end diff --git a/instrumentation/aws_lambda/test/opentelemetry/instrumentation_test.rb b/instrumentation/aws_lambda/test/opentelemetry/instrumentation_test.rb new file mode 100644 index 000000000..e737544f6 --- /dev/null +++ b/instrumentation/aws_lambda/test/opentelemetry/instrumentation_test.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Instrumentation::AwsLambda do + let(:instrumentation) { OpenTelemetry::Instrumentation::AwsLambda::Instrumentation.instance } + let(:exporter) { EXPORTER } + let(:event_v1) { EVENT_V1 } + let(:event_v2) { EVENT_V2 } + let(:event_record) { EVENT_RECORD } + let(:sqs_record) { SQS_RECORD } + let(:context) { CONTEXT } + let(:last_span) { exporter.finished_spans.last } + + it 'has #name' do + _(instrumentation.name).must_equal 'OpenTelemetry::Instrumentation::AwsLambda' + end + + it 'has #version' do + _(instrumentation.version).wont_be_nil + _(instrumentation.version).wont_be_empty + end + + describe '#compatible' do + it 'returns true for supported gem versions' do + _(instrumentation.compatible?).must_equal true + end + end + + describe '#install' do + it 'accepts argument' do + _(instrumentation.install({})).must_equal(true) + instrumentation.instance_variable_set(:@installed, false) + end + end + + describe 'validate_wrapper' do + it 'result should be span' do + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new + otel_wrapper.stub(:call_original_handler, {}) do + otel_wrapper.call_wrapped(event: event_v1, context: context) + _(last_span).must_be_kind_of(OpenTelemetry::SDK::Trace::SpanData) + end + end + + it 'validate_spans' do + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new + otel_wrapper.stub(:call_original_handler, {}) do + otel_wrapper.call_wrapped(event: event_v1, context: context) + + _(last_span.name).must_equal 'sample.test' + _(last_span.kind).must_equal :server + _(last_span.status.code).must_equal 1 + _(last_span.hex_parent_span_id).must_equal '0000000000000000' + + _(last_span.attributes['aws.lambda.invoked_arn']).must_equal 'arn:aws:lambda:location:id:function_name:function_name' + _(last_span.attributes['faas.invocation_id']).must_equal '41784178-4178-4178-4178-4178417855e' + _(last_span.attributes['faas.trigger']).must_equal 'http' + _(last_span.attributes['cloud.resource_id']).must_equal 'arn:aws:lambda:location:id:function_name:function_name' + _(last_span.attributes['cloud.account.id']).must_equal 'id' + _(last_span.attributes['http.method']).must_equal 'GET' + _(last_span.attributes['http.route']).must_equal '/' + _(last_span.attributes['http.target']).must_equal '/' + _(last_span.attributes['http.user_agent']).must_equal 'curl/8.1.2' + _(last_span.attributes['http.scheme']).must_equal 'http' + _(last_span.attributes['net.host.name']).must_equal '127.0.0.1:3000' + + _(last_span.instrumentation_scope).must_be_kind_of OpenTelemetry::SDK::InstrumentationScope + _(last_span.instrumentation_scope.name).must_equal 'OpenTelemetry::Instrumentation::AwsLambda' + _(last_span.instrumentation_scope.version).must_equal '0.1.0' + + _(last_span.hex_span_id.size).must_equal 16 + _(last_span.hex_trace_id.size).must_equal 32 + _(last_span.trace_flags.sampled?).must_equal true + _(last_span.tracestate.to_h.to_s).must_equal '{}' + end + end + + it 'validate_spans_with_parent_context' do + event_v1['headers']['Traceparent'] = '00-48b05d64abe4690867685635f72bdbac-ff40ea9699e62af2-01' + event_v1['headers']['Tracestate'] = 'otel=ff40ea9699e62af2-01' + + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new + otel_wrapper.stub(:call_original_handler, {}) do + otel_wrapper.call_wrapped(event: event_v1, context: context) + + _(last_span.name).must_equal 'sample.test' + _(last_span.kind).must_equal :server + + _(last_span.hex_parent_span_id).must_equal 'ff40ea9699e62af2' + _(last_span.hex_span_id.size).must_equal 16 + _(last_span.hex_trace_id.size).must_equal 32 + _(last_span.trace_flags.sampled?).must_equal true + _(last_span.tracestate.to_h.to_s).must_equal '{"otel"=>"ff40ea9699e62af2-01"}' + end + event_v1['headers'].delete('traceparent') + event_v1['headers'].delete('tracestate') + end + + it 'validate_spans_with_v2_events' do + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new + otel_wrapper.stub(:call_original_handler, {}) do + otel_wrapper.call_wrapped(event: event_v2, context: context) + + _(last_span.name).must_equal 'sample.test' + _(last_span.kind).must_equal :server + _(last_span.status.code).must_equal 1 + _(last_span.hex_parent_span_id).must_equal '0000000000000000' + + _(last_span.attributes['aws.lambda.invoked_arn']).must_equal 'arn:aws:lambda:location:id:function_name:function_name' + _(last_span.attributes['faas.invocation_id']).must_equal '41784178-4178-4178-4178-4178417855e' + _(last_span.attributes['faas.trigger']).must_equal 'http' + _(last_span.attributes['cloud.account.id']).must_equal 'id' + _(last_span.attributes['cloud.resource_id']).must_equal 'arn:aws:lambda:location:id:function_name:function_name' + _(last_span.attributes['net.host.name']).must_equal 'id.execute-api.us-east-1.amazonaws.com' + _(last_span.attributes['http.method']).must_equal 'POST' + _(last_span.attributes['http.user_agent']).must_equal 'agent' + _(last_span.attributes['http.route']).must_equal '/path/to/resource' + _(last_span.attributes['http.target']).must_equal '/path/to/resource?parameter1=value1¶meter1=value2¶meter2=value' + end + end + + it 'validate_spans_with_records_from_non_gateway_request' do + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new + otel_wrapper.stub(:call_original_handler, {}) do + otel_wrapper.call_wrapped(event: event_record, context: context) + + _(last_span.name).must_equal 'sample.test' + _(last_span.kind).must_equal :consumer + _(last_span.status.code).must_equal 1 + _(last_span.hex_parent_span_id).must_equal '0000000000000000' + + _(last_span.attributes['aws.lambda.invoked_arn']).must_equal 'arn:aws:lambda:location:id:function_name:function_name' + _(last_span.attributes['faas.invocation_id']).must_equal '41784178-4178-4178-4178-4178417855e' + _(last_span.attributes['cloud.resource_id']).must_equal 'arn:aws:lambda:location:id:function_name:function_name' + _(last_span.attributes['cloud.account.id']).must_equal 'id' + + assert_nil(last_span.attributes['faas.trigger']) + assert_nil(last_span.attributes['http.method']) + assert_nil(last_span.attributes['http.user_agent']) + assert_nil(last_span.attributes['http.route']) + assert_nil(last_span.attributes['http.target']) + assert_nil(last_span.attributes['net.host.name']) + end + end + + it 'validate_spans_with_records_from_sqs' do + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new + otel_wrapper.stub(:call_original_handler, {}) do + otel_wrapper.call_wrapped(event: sqs_record, context: context) + + _(last_span.name).must_equal 'sample.test' + _(last_span.kind).must_equal :consumer + _(last_span.status.code).must_equal 1 + _(last_span.hex_parent_span_id).must_equal '0000000000000000' + + _(last_span.attributes['aws.lambda.invoked_arn']).must_equal 'arn:aws:lambda:location:id:function_name:function_name' + _(last_span.attributes['faas.invocation_id']).must_equal '41784178-4178-4178-4178-4178417855e' + _(last_span.attributes['cloud.resource_id']).must_equal 'arn:aws:lambda:location:id:function_name:function_name' + _(last_span.attributes['cloud.account.id']).must_equal 'id' + _(last_span.attributes['faas.trigger']).must_equal 'pubsub' + _(last_span.attributes['messaging.operation']).must_equal 'process' + _(last_span.attributes['messaging.system']).must_equal 'AmazonSQS' + + assert_nil(last_span.attributes['http.method']) + assert_nil(last_span.attributes['http.user_agent']) + assert_nil(last_span.attributes['http.route']) + assert_nil(last_span.attributes['http.target']) + assert_nil(last_span.attributes['net.host.name']) + end + end + end + + describe 'validate_error_handling' do + it 'handle error if original handler cause issue' do + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new + otel_wrapper.stub(:call_original_handler, ->(**_args) { raise StandardError, 'Simulated Error' }) do + otel_wrapper.call_wrapped(event: event_v1, context: context) + rescue StandardError + _(last_span.name).must_equal 'sample.test' + _(last_span.kind).must_equal :server + + _(last_span.status.code).must_equal 2 + _(last_span.status.description).must_equal 'Simulated Error' + _(last_span.hex_parent_span_id).must_equal '0000000000000000' + + _(last_span.events[0].name).must_equal 'exception' + _(last_span.events[0].attributes['exception.type']).must_equal 'StandardError' + _(last_span.events[0].attributes['exception.message']).must_equal 'Simulated Error' + + _(last_span.hex_span_id.size).must_equal 16 + _(last_span.hex_trace_id.size).must_equal 32 + _(last_span.trace_flags.sampled?).must_equal true + _(last_span.tracestate.to_h.to_s).must_equal '{}' + end + end + + it 'if wrapped handler cause otel-related issue, wont break the entire lambda call' do + otel_wrapper = OpenTelemetry::Instrumentation::AwsLambda::Handler.new + otel_wrapper.stub(:call_wrapped, { 'test' => 'ok' }) do + otel_wrapper.stub(:call_original_handler, {}) do + OpenTelemetry::Context.stub(:with_current, lambda { |_context| + tracer.start_span('test_span', attributes: {}, kind: :server) + raise StandardError, 'OTEL Error' + }) do + response = otel_wrapper.call_wrapped(event: event_v1, context: context) + _(response['test']).must_equal 'ok' + end + end + end + end + end +end diff --git a/instrumentation/aws_lambda/test/test_helper.rb b/instrumentation/aws_lambda/test/test_helper.rb new file mode 100644 index 000000000..cf3cde320 --- /dev/null +++ b/instrumentation/aws_lambda/test/test_helper.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/setup' +Bundler.require(:default, :development, :test) + +require 'opentelemetry-instrumentation-aws_lambda' + +require 'minitest/autorun' +require 'rspec/mocks/minitest_integration' + +class MockLambdaContext + attr_reader :aws_request_id, :invoked_function_arn, :function_name + + def initialize(aws_request_id:, invoked_function_arn:, function_name:) + @aws_request_id = aws_request_id + @invoked_function_arn = invoked_function_arn + @function_name = function_name + end +end + +EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new +span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER) + +EVENT_V1 = { + 'body' => nil, + 'headers' => { + 'Accept' => '*/*', + 'Host' => '127.0.0.1:3000', + 'User-Agent' => 'curl/8.1.2', + 'X-Forwarded-Port' => 3000, + 'X-Forwarded-Proto' => 'http' + }, + 'httpMethod' => 'GET', + 'isBase64Encoded' => false, + 'multiValueHeaders' => {}, + 'multiValueQueryStringParameters' => nil, + 'path' => '/', + 'pathParameters' => nil, + 'queryStringParameters' => nil, + 'requestContext' => { + 'accountId' => 123_456_789_012, + 'apiId' => 1_234_567_890, + 'domainName' => '127.0.0.1:3000', + 'extendedRequestId' => nil, + 'httpMethod' => 'GET', + 'identity' => {}, + 'path' => '/', + 'protocol' => 'HTTP/1.1', + 'requestId' => 'db7f8e7a-4cc5-4f6d-987b-713d0d9052c3', + 'requestTime' => '08/Nov/2023:19:09:59 +0000', + 'requestTimeEpoch' => 1_699_470_599, + 'resourceId' => '123456', + 'resourcePath' => '/', + 'stage' => 'api' + }, + 'resource' => '/', + 'stageVariables' => nil, + 'version' => '1.0' +}.freeze + +EVENT_V2 = { + 'version' => '2.0', + 'routeKey' => '$default', + 'rawPath' => '/path/to/resource', + 'rawQueryString' => 'parameter1=value1¶meter1=value2¶meter2=value', + 'cookies' => %w[cookie1 cookie2], + 'headers' => { 'header1' => 'value1', 'Header2' => 'value1,value2' }, + 'queryStringParameters' => {}, + 'requestContext' => { + 'accountId' => '123456789012', + 'apiId' => 'api-id', + 'authentication' => { 'clientCert' => {} }, + 'authorizer' => {}, + 'domainName' => 'id.execute-api.us-east-1.amazonaws.com', + 'domainPrefix' => 'id', + 'http' => { + 'method' => 'POST', + 'path' => '/path/to/resource', + 'protocol' => 'HTTP/1.1', + 'sourceIp' => '192.168.0.1/32', + 'userAgent' => 'agent' + }, + 'requestId' => 'id', + 'routeKey' => '$default', + 'stage' => '$default', + 'time' => '12/Mar/2020:19:03:58 +0000', + 'timeEpoch' => 1_583_348_638_390 + }, + 'body' => 'eyJ0ZXN0IjoiYm9keSJ9', + 'pathParameters' => { 'parameter1' => 'value1' }, + 'isBase64Encoded' => true, + 'stageVariables' => { 'stageVariable1' => 'value1', 'stageVariable2' => 'value2' } +}.freeze + +EVENT_RECORD = { + 'Records' => + [ + { 'eventVersion' => '2.0', + 'eventSource' => 'aws:s3', + 'awsRegion' => 'us-east-1', + 'eventTime' => '1970-01-01T00:00:00.000Z', + 'eventName' => 'ObjectCreated:Put', + 'userIdentity' => { 'principalId' => 'EXAMPLE' }, + 'requestParameters' => { 'sourceIPAddress' => '127.0.0.1' }, + 'responseElements' => { + 'x-amz-request-id' => 'EXAMPLE123456789', + 'x-amz-id-2' => 'EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH' + }, + 's3' => { + 's3SchemaVersion' => '1.0', + 'configurationId' => 'testConfigRule', + 'bucket' => { + 'name' => 'mybucket', + 'ownerIdentity' => { + 'principalId' => 'EXAMPLE' + }, + 'arn' => 'arn:aws:s3:::mybucket' + }, + 'object' => { + 'key' => 'test/key', + 'size' => 1024, + 'eTag' => '0123456789abcdef0123456789abcdef', + 'sequencer' => '0A1B2C3D4E5F678901' + } + } } + ] +}.freeze + +SQS_RECORD = { + 'Records' => + [{ 'messageId' => '19dd0b57-b21e-4ac1-bd88-01bbb068cb78', + 'receiptHandle' => 'MessageReceiptHandle', + 'body' => 'Hello from SQS!', + 'attributes' => + { 'ApproximateReceiveCount' => '1', + 'SentTimestamp' => '1523232000000', + 'SenderId' => '123456789012', + 'ApproximateFirstReceiveTimestamp' => '1523232000001' }, + 'messageAttributes' => {}, + 'md5OfBody' => '7b270e59b47ff90a553787216d55d91d', + 'eventSource' => 'aws:sqs', + 'eventSourceARN' => 'arn:aws:sqs:us-east-1:123456789012:MyQueue', + 'awsRegion' => 'us-east-1' }] +}.freeze + +CONTEXT = MockLambdaContext.new(aws_request_id: '41784178-4178-4178-4178-4178417855e', + invoked_function_arn: 'arn:aws:lambda:location:id:function_name:function_name', + function_name: 'funcion') + +$LOAD_PATH.unshift("#{Dir.pwd}/example/") +ENV['ORIG_HANDLER'] = 'sample.test' +ENV['_HANDLER'] = 'sample.test' +OpenTelemetry::SDK.configure do |c| + c.error_handler = ->(exception:, message:) { raise(exception || message) } + c.logger = Logger.new($stderr, level: ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym) + c.use 'OpenTelemetry::Instrumentation::AwsLambda' + c.add_span_processor span_processor +end diff --git a/release-please-config.json b/release-please-config.json index 8686eb523..eba2f3786 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -81,6 +81,10 @@ "package-name": "opentelemetry-instrumentation-aws_sdk", "version-file": "lib/opentelemetry/instrumentation/aws_sdk/version.rb" }, + "instrumentation/aws_lambda": { + "package-name": "opentelemetry-instrumentation-aws_lambda", + "version-file": "lib/opentelemetry/instrumentation/aws_lambda/version.rb" + }, "instrumentation/lmdb": { "package-name": "opentelemetry-instrumentation-lmdb", "version-file": "lib/opentelemetry/instrumentation/lmdb/version.rb"