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 589bd2d7f..c0fb678c1 100644 --- a/.github/workflows/ci-instrumentation.yml +++ b/.github/workflows/ci-instrumentation.yml @@ -21,6 +21,7 @@ jobs: fail-fast: false matrix: gem: + - action_mailer - action_pack - action_view - active_job @@ -29,6 +30,7 @@ jobs: - active_support - all - aws_sdk + - aws_lambda - base - concurrent_ruby - delayed_job @@ -92,6 +94,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..bda9206b0 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -8,6 +8,7 @@ "instrumentation/rdkafka": "0.4.2", "instrumentation/trilogy": "0.58.0", "instrumentation/active_support": "0.5.1", + "instrumentation/action_mailer": "0.1.0", "instrumentation/action_view": "0.7.0", "instrumentation/action_pack": "0.9.0", "instrumentation/active_job": "0.7.1", @@ -16,6 +17,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 ea0984d18..64c87a77f 100644 --- a/.toys/.data/releases.yml +++ b/.toys/.data/releases.yml @@ -71,6 +71,10 @@ gems: directory: instrumentation/active_support version_constant: [OpenTelemetry, Instrumentation, ActiveSupport, VERSION] + - name: opentelemetry-instrumentation-action_mailer + directory: instrumentation/action_mailer + version_constant: [OpenTelemetry, Instrumentation, ActionMailer, VERSION] + - name: opentelemetry-instrumentation-action_view directory: instrumentation/action_view version_constant: [OpenTelemetry, Instrumentation, ActionView, VERSION] @@ -104,6 +108,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/CONTRIBUTING.md b/CONTRIBUTING.md index a8a5fe37b..b885db0ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ conforms to the specification, but the interface and structure are flexible. It is preferable to have contributions follow the idioms of the language rather than conform to specific API names or argument patterns in the spec. -For a deeper discussion, see: https://github.com/open-telemetry/opentelemetry-specification/issues/165 +For a deeper discussion, see: ## Getting started @@ -40,6 +40,7 @@ git clone git@github.com:YOUR_GITHUB_NAME/opentelemetry-ruby-contrib.git ``` or + ```sh git clone https://github.com/YOUR_GITHUB_NAME/opentelemetry-ruby-contrib.git ``` @@ -66,10 +67,10 @@ _Setting up a running Ruby environment is outside the scope of this document._ This repository contains multiple Ruby gems: - * Various instrumentation gems located in subdirectories of `instrumentation` - * Various resource detector gems located in subdirectories of `resources` - * `opentelemetry-propagator-xray` located in the `propagator/xray` directory - * `opentelemetry-propagator-ottrace` located in the `propagator/ottrace` directory +* Various instrumentation gems located in subdirectories of `instrumentation` +* Various resource detector gems located in subdirectories of `resources` +* `opentelemetry-propagator-xray` located in the `propagator/xray` directory +* `opentelemetry-propagator-ottrace` located in the `propagator/ottrace` directory Each of these gems has its configuration and tests. @@ -88,9 +89,9 @@ configuration details. The services provided include: - * `app` - main container environment scoped to the `/app` directory. Used +* `app` - main container environment scoped to the `/app` directory. Used primarily to build and tag the `opentelemetry/opentelemetry-ruby-contrib:latest` image. - * `x-instrumentation-` - container environment scoped to a specific instrumentation library. See `docker-compose.yml` for available services. +* `x-instrumentation-` - container environment scoped to a specific instrumentation library. See `docker-compose.yml` for available services. To test using Docker: @@ -100,9 +101,9 @@ To test using Docker: * `docker-compose build` * This makes the image available locally 4. Install dependencies for the service you want to interact with - * `docker-compose run bundle install` + * `docker-compose run bundle install` 5. Run the tests - * `docker-compose run bundle exec rake test` + * `docker-compose run bundle exec rake test` ## Processing and visualizing traces locally @@ -155,8 +156,8 @@ to ensure that your code complies before opening a pull request. We also use Yard to generate class documentation automatically. Among other things, this means: - * Methods and arguments should include the appropriate type annotations - * You can use markdown formatting in your documentation comments +* Methods and arguments should include the appropriate type annotations +* You can use markdown formatting in your documentation comments You can generate the docs locally to see the results, by running: @@ -165,7 +166,11 @@ bundle install bundle exec rake yard ``` -## Create a Pull Request +## Instrumentation author's guide + +Please make sure that you review the [Instrumentation author's guide](.instrumentation/CONTRIBUTING.md) before submitting a new instrumentation. + +## Create a pull request You'll need to create a Pull Request once you've finished your work. The [Kubernetes GitHub Workflow][kube-github-workflow-pr] document has @@ -223,8 +228,8 @@ Releases are normally performed using GitHub Actions. 2. In the GitHub UI, go to the `Actions` tab, select the `Open release request` workflow, and run the workflow manually using the dropdown in the upper right. - * Releases must be run from the main branch. - * If you leave the `Gems to release` field, blank, and the script will + * Releases must be run from the main branch. + * If you leave the `Gems to release` field, blank, and the script will find all the gems that have had conventional-commit-tagged changes since their last release. Alternately, you can specify which gems to release by including their names, space-delimited, in this this field. You can @@ -247,9 +252,9 @@ Releases are normally performed using GitHub Actions. was opened; make sure the label is still there when you merge it. 6. The automated release script will run automatically, and will release the gem(s) once CI has completed. This includes: - * For each gem, it will create a release tag and a GitHub release. - * It will build and push the gems to rubygems. - * If the releases succeed, the script will update the release pull + * For each gem, it will create a release tag and a GitHub release. + * It will build and push the gems to rubygems. + * If the releases succeed, the script will update the release pull request with the results and change its label to `release: complete`. If something went wrong, the script will, if possible, report the error on the release pull request and change its label to `release: error`. @@ -268,20 +273,20 @@ review the release logs for the GitHub Actions workflows. There are four GitHub actions workflows related to releases. - * `Open release request` is the main release entrypoint, and is used to open - a release pull request. If something goes wrong with this process, the logs - will appear in the workflow run. - * `Force release` is generally used only to restart a failed release. - * `[release hook] Update open releases` is run on pushes to the main branch, - and pushes warnings to open release pull requests if you make modifications - before triggering the release (i.e. because you might need to update the - changelogs.) - * `[release hook] Process release` is the main release automation script and - is run when a pull request is closed. If it determines that a release pull - request was merged, it kicks off the release process for the affected gems. - It also updates the label on a closed release pull request. Finally, it - deletes release branches when they are no longer being used. If something - goes wrong with any of these processes, the logs will appear here. +* `Open release request` is the main release entrypoint, and is used to open + a release pull request. If something goes wrong with this process, the logs + will appear in the workflow run. +* `Force release` is generally used only to restart a failed release. +* `[release hook] Update open releases` is run on pushes to the main branch, + and pushes warnings to open release pull requests if you make modifications + before triggering the release (i.e. because you might need to update the + changelogs.) +* `[release hook] Process release` is the main release automation script and + is run when a pull request is closed. If it determines that a release pull + request was merged, it kicks off the release process for the affected gems. + It also updates the label on a closed release pull request. Finally, it + deletes release branches when they are no longer being used. If something + goes wrong with any of these processes, the logs will appear here. #### Restarting a release @@ -292,14 +297,14 @@ release, you can use the `Force release` workflow. GitHub UI. 2. In the GitHub UI, go to the `Actions` tab, select the `Force release` workflow, and run it manually. - * You must provide the gem name and version explicitly in the fields. - * The `Extra flags` field is useful for advanced cases. For example, if - the GitHub release tag is already created and the gem already pushed to - Rubygems, but the docs still need to be built, you can pass - `--only=docs` to perform only that one step. You can also force a - release even if the build is not green or the version/changelog checks - are failing, by passing `--skip-checks`. For more details, install the - `toys` gem and run `toys release perform --help` locally. + * You must provide the gem name and version explicitly in the fields. + * The `Extra flags` field is useful for advanced cases. For example, if + the GitHub release tag is already created and the gem already pushed to + Rubygems, but the docs still need to be built, you can pass + `--only=docs` to perform only that one step. You can also force a + release even if the build is not green or the version/changelog checks + are failing, by passing `--skip-checks`. For more details, install the + `toys` gem and run `toys release perform --help` locally. #### Running releases locally @@ -319,7 +324,7 @@ changed gems. To force-release, assuming the version and changelog are already modified: -``` +```console toys release perform --rubygems-api-key=$API_KEY $GEM_NAME $GEM_VERSION ``` @@ -337,14 +342,14 @@ not correspond exactly to the gem name. For releases to succeed, new gems MUST include the following: - * The above configuration entry. - * The `*.gemspec` file, with the name matching the gem name. - * A `version.rb` file in the standard location, or in a location listed in - the configuration. - * A `CHANGELOG.md` file. - * A `yard` rake task. +* The above configuration entry. +* The `*.gemspec` file, with the name matching the gem name. +* A `version.rb` file in the standard location, or in a location listed in + the configuration. +* A `CHANGELOG.md` file. +* A `yard` rake task. -## Dependabot Updates +## Dependabot updates This repository uses [Dependabot](https://dependabot.com/) to keep dependencies up to date, however there shared development dependencies are often scattered across multiple gems. Dependabot does not currently support the ability to group dependencies for gems in multiple subdirectories, so we use a custom script to bulk update dependencies across all gems. diff --git a/instrumentation/CONTRIBUTING.md b/instrumentation/CONTRIBUTING.md new file mode 100644 index 000000000..ff45dcbbb --- /dev/null +++ b/instrumentation/CONTRIBUTING.md @@ -0,0 +1,486 @@ +# Instrumentation author's guide + +This guide is for authors of OpenTelemetry Ruby instrumentation libraries. It provides guidance on how to contribute an instrumentation library. + +Please make sure to read and understand the [CONTRIBUTING](../CONTRIBUTING.md) guide before submitting any changes to instrumentation. + +## What we expect from you + +We are a community of volunteers with a shared goal of improving observability in Ruby applications. + +We welcome contributions from everyone. We want to make sure that you have a great experience contributing to this project, as well as a positive impact on the Ruby community. + +We have limited capacity to maintain instrumentation libraries, so we ask that you commit to maintaining the instrumentation library you contribute. + +In addition to the requirements to maintain at least [community member status](https://github.com/open-telemetry/community/blob/main/community-membership.md), contributing an instrumentation to this project requires the following: + +1. Responding to issues and pull requests +2. Performing timely code reviews and responding to issues +3. Addressing security vulnerabilities +4. Keeping the instrumentation library up to date with the latest: + * OpenTelemetry Ruby API and SDK changes + * Ruby language changes + * Instrumented library changes + +If you do not have the capacity to maintain the instrumentation library, please consider contributing to the OpenTelemetry Ruby project in other ways or consider creating a separate project for the instrumentation library. + +> :warning: Libraries that do not meet these requirements may be removed from the project at any time at the discretion of OpenTelemetry Ruby Contrib Maintainers. + +## Contributing a new instrumentation library + +Our long-term goal is to provide instrumentation for all popular Ruby libraries. Ideally, we would like to have first-party instrumentation for all libraries maintained by the gem's authors to ensure compatibility with upstream gems. However, in many cases this is not possible. + +For this reason, we welcome contributions of new instrumentation libraries that cannot be maintained by the original gem authors as first-party instrumentation. + +The following steps are required to contribute a new instrumentation library: + +1. Generate an instrumentation gem skeleton +2. Implement the instrumentation library, including comprehensive automated tests +3. Add the instrumentation library to the appropriate CI workflows +4. Include documentation for your instrumentation + * Document all instrumentation-specific configuration options in the `README.md` and `yardoc` class comments + * Document all semantic conventions used by the instrumentation in the `README.md` + * Provide executable examples in an `examples` directory +5. Submit a pull request + +## Generate the gem + +This repository contains a script to generate a new instrumentation library. + +The snippet below demonstrates how to generate a an instrumentation for the `werewolf` gem, starting from the repository root. + +```console + +$bash opentelemetry-ruby-contrib> bin/instrumentation_generator werewolf + +``` + +The output of the generator shows that it creates a new directory in the `instrumentation` directory using the name of the gem: + +``` console + +🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 + + WARNING: Your gem will *NOT* be tested until you add it to the CI workflows in `.github/workflows/ci.yml`!! + +🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 + + create instrumentation/werewolf/.rubocop.yml + create instrumentation/werewolf/.yardopts + create instrumentation/werewolf/Appraisals + create instrumentation/werewolf/CHANGELOG.md + create instrumentation/werewolf/Gemfile + create instrumentation/werewolf/LICENSE + create instrumentation/werewolf/opentelemetry-instrumentation-werewolf.gemspec + create instrumentation/werewolf/Rakefile + create instrumentation/werewolf/README.md + create instrumentation/werewolf/lib/opentelemetry-instrumentation-werewolf.rb + create instrumentation/werewolf/lib/opentelemetry/instrumentation.rb + create instrumentation/werewolf/lib/opentelemetry/instrumentation/werewolf.rb + create instrumentation/werewolf/lib/opentelemetry/instrumentation/werewolf/instrumentation.rb + create instrumentation/werewolf/lib/opentelemetry/instrumentation/werewolf/version.rb + create instrumentation/werewolf/test/test_helper.rb + create instrumentation/werewolf/test/opentelemetry/instrumentation/werewolf/instrumentation_test.rb + insert .toys/.data/releases.yml + insert instrumentation/all/Gemfile + insert instrumentation/all/opentelemetry-instrumentation-all.gemspec + insert instrumentation/all/lib/opentelemetry/instrumentation/all.rb + +``` + +## Implementation guidelines + +The original design and implementation of this project was heavily influenced by Datadog's `dd-trace-rb` project. You may refer to the [Datadog Porting Guide](datadog-porting-guide.md) as a reference for implementing instrumentations, however, the following guidelines are specific to OpenTelemetry Ruby: + +* Use `OpenTelemetry::Instrumentation::Base` +* Use the OpenTelemetry API +* Use first-party extension points +* Use Semantic Conventions +* Write comprehensive automated tests +* Understand performance characteristics + +### Use `OpenTelemetry::Instrumentation::Base` + +The entry point of your instrumentation should be implemented as a subclass of `OpenTelemetry::Instrumentation::Base`: + +* Implement an `install` block, where all of the integration work happens +* Implement a `present` block, which checks whether the library you are instrumenting was loaded +* Implement a `compatible` block and check for at least the minimum required library version + * OpenTelemetry Ruby Contrib generally supports only versions of gems that are within the maintenance window +* Add any custom configuration `options` you want to support + +The example below demonstrates how to implement the `Werewolf` instrumentation: + +```ruby + +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Werewolf + class Instrumentation < OpenTelemetry::Instrumentation::Base + MINIMUM_VERSION = Gem::Version.new('0.1.0') + + install do |_config| + require_relative 'handlers' + Handlers.subscribe + end + + option :transformations, default: :omit, validate: %i[omit include] + + present do + defined?(::Werewolf) && defined?(::ActiveSupport) + end + + compatible do + Gem::Version.new(::Wereworlf::VERSION) >= MINIMUM_VERSION + end + end + end + end +end + +``` + +* The `install` block lazily requires the instrumentation handlers, which subscribe to events published by the `Werewolf` event hooks. +* The `present` block checks if the `Werewolf` and `ActiveSupport` libraries are loaded, which it will use to subscribe to events and generate spans. It will skip the installation if those dependencies were not loaded before the instrumentation was initialized. +* The `compatible` block checks if the `Werewolf` library version is at least `0.1.0` and will skip installation if it is not. +* The `options` section allows you to define custom configuration options that can be passed to the instrumentation. In this example, the `transformations` option is defined with a default value of `:omit` and a validation rule that only allows `:omit` or `:include` values. + +### Use the OpenTelemetry API + +Instrumentations are intended to be portable and usable with vendor distributions of the SDK. For this reason, you must use the [OpenTelemetry API](https://github.com/open-telemetry/opentelemetry-ruby/tree/main/api) to create spans and add attributes, events, and links to spans and avoid using the [OpenTelemetry SDK](https://github.com/open-telemetry/opentelemetry-ruby/tree/main/sdk) directly. + +Each instrumentation _must_ use a named tracer. Instrumentations that inherit from `OpenTelemetry::Instrumentation::Base` will get a single helper method that will automatically provide your instrumentation with a named tracer under `OpenTelemetry::Instrumentation::${Gem Name}::Instrumentation.instance.tracer`. + +For example, the `Werewolf` module generated in the example above is available via `OpenTelemetry::Instrumentation::Werewolf::Instrumentation.instance.tracer`. You should reference this tracer in your code when creating spans like this: + +```ruby + + OpenTelemetry::Instrumentation::Werewolf::Instrumentation.instance.tracer.start_span('transform') do + # code to be traced + end + +``` + +> :warning: This tracer is not _upgradable_ before the SDK is initialized, therefore it is important that your instrumentation _always_ use stack local references of the tracer. + +### Use first-party extension points + +Whenever possible, use first-party extension points (hooks) to instrument libraries. This ensures that the instrumentation is compatible with the latest versions of the library and that the instrumentation is maintained by the library authors. [`ActiveSupport::Notifications`](https://guides.rubyonrails.org/active_support_instrumentation.html) and `Middleware` are good examples of first-party extension points used by our instrumentation libraries. + +Monkey patching is discouraged in OpenTelemetry Ruby because it is the most common source of bugs and incompatability with the libraries we instrument. If you must monkey patch, please ensure that the monkey patch is as isolated as possible and that it is clearly documented. + +### Use Semantic Conventions + +Use the [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/concepts/semantic-conventions/) to ensure the instrumentation is compatible with other OpenTelemetry libraries and that the data is useful in a distributed context. + +> :information_source: Privacy and security are important considerations when adding attributes to spans. Please ensure that you are not adding sensitive information to spans. If you are unsure, please ask for a review. + +When semantic conventions do not exist, use the [Elastic Common Schema](https://www.elastic.co/guide/en/ecs/current/index.html) and submit an Issue/PR with your attributes to the [Semantic Conventions repo](https://github.com/open-telemetry/semantic-conventions) to propose a new set of standard attributes. + +If the attribute is specific to your instrumentation, then consider namespacing it using the `instrumentation` prefix e.g. `werewolf.bite.damage` and calling it out in the instrumentation README. + +### Write comprehensive automated tests + +Code that is not tested will not be accepted by maintainers. We understand that providing 100% test coverage is not always possible but we still ask that you provide your best effort when writing automated tests. + +Most of the libraries instrument introduce changes outside of our control. For this reason, integration or state-based tests are preferred over interaction (mock) tests. + +When you do in fact run into cases where test doubles or API stubs are absolutely necessary, we recommend using the [`rspec-mocks`](https://github.com/rspec/rspec-mocks) and [`webmocks`](https://github.com/bblimke/webmock) gems. + +### Understand performance characteristics + +The OTel Specification describes expectations around the performance of SDKs, which you must review and apply to instrumentation: + +Instrumentation libraries should be as lightweight as possible and must: + +* Rely on `rubocop-performance` linters to catch performance issues +* Consider using [microbenchmarks](https://github.com/evanphx/benchmark-ips) and [profiling](https://ruby-prof.github.io/) to address any possible performance issues +* Provide minimal solutions and code paths + +#### Provide minimal solutions and code paths + +Instrumentation should have the minimal amount of code necessary to provide useful insights to our users. It may sound contrary to good engineering practices, but you must avoid adding lots of small methods, classes, and objects when instrumenting a library. + +Though often easier to maintain and reason about; small and well-factored code adds overhead to the library you're instrumenting, resulting in performance degradation due to unnecessary object allocations, method dispatching, and other performance overhead. + +It also contributes to building large backtraces, making it more difficult for our end users to understand the essential parts of exception reports. That will likely result in additional filtering logic in their application to avoid reporting unnecessary stack frames. + +In cases when code uses monkey patching, it runs the risk of _adding_ methods that conflict with the internal implementation of the library and may result in unexpected behavior and bugs. + +Avoid instrumenting _every_ method in a library and instead focus on the methods that provide the _most_ insights into what typically causes performance problems for applications, e.g. I/O and network calls. The use case for this type of low-level granularity falls under the purview of profiling. + +In the near future, [OTel Profiling](https://opentelemetry.io/blog/2024/profiling/) will provide users an even deeper understanding of what is happening in their applications at a more granular level. + +Here are some examples of performance fixes that reduced object allocations and method dispatching: + +* +* +* +* +* +* + +#### Avoid adding custom extensions + +Though your instrumentation may accept configurations options to customize the output, you should consider that the more options you add, the more complexity you will have to manage. + +You should _avoid_ adding options that allow custom code blocks (`type: :callable`) to be executed as part of the instrumentation. It is often difficult to predict error modes and the performance impact custom code will have on your instrumentation, which in turn will impact the service being instrumented. + +You should steer users towards post-processing as part of the [OTel Collector](https://opentelemetry.io/docs/collector/), which has a richer and more powerful toolset, and executes out of the application's critical code path. + +## Enable CI + +This project contains multiple CI workflows that execute tests and ensure the gems are installable. + +### Standalone instrumentation + +For standalone instrumentation that does not have any external service dependencies, add the gem to the `/.github/workflows/ci-instrumentation.yml` file under `jobs/instrumentation/strategy/matrix/gem`: + +``` yaml + +jobs: + instrumentation: + strategy: + fail-fast: false + matrix: + gem: + - action_pack + - action_view + - active_job + # ... + - werewolf + os: + - ubuntu-latest + +``` + +#### JRuby Compatibility + +If your gem is incompatible with `JRuby`, you can exclude it from the matrix by adding an entry to the `/.github/workflows/ci-instrumentation.yml` file under `jobs/instrumentation/steps/[name="JRuby Filter"]`: + +``` yaml + - name: "JRuby Filter" + id: jruby_skip + shell: bash + run: | + echo "skip=false" >> $GITHUB_OUTPUT + [[ "${{ matrix.gem }}" == "action_pack" ]] && echo "skip=true" >> $GITHUB_OUTPUT + # ... + [[ "${{ matrix.gem }}" == "werewolf" ]] && echo "skip=true" >> $GITHUB_OUTPUT + # This is essentially a bash script getting evaluated, so we need to return true or the whole job fails. + true +``` + +### External service instrumentations + +Adding jobs for instrumentation with external service dependencies may be a bit more difficult if the job does not already have a similar service configured. + +#### Using Existing Services + +CI is currently configured to support the following services: + +* kafka +* memcached +* mongodb +* mysql +* postgresql +* rabbitmq +* redis + +If your gem depends on one of those services, then great! The next step is to add the gem to matrix in the `/.github/workflows/ci-service-instrumentation.yml` file under `jobs/instrumentation_*/strategy/matrix/gem`: + +```yaml + + instrumentation_kafka: + strategy: + fail-fast: false + matrix: + gem: + - racecar + - rdkafka + - ruby_kafka + - werewolf + os: + - ubuntu-latest +``` + +#### Adding a New Service + +Assuming your external service is not supported, you may consider adding it as a new job in the `/.github/workflows/ci-service-instrumentation.yml` file, however we will accept new services on a case-by-case basis. + +Add the service container to `jobs/instrumentation_with_services/services` and add the gem to the matrix in `jobs/instrumentation_with_services/strategy/matrix/gem`: + +```yaml + + instrumentation_with_services: + strategy: + fail-fast: false + matrix: + gem: + - dalli + - mongo + - werewolf + os: + - ubuntu-latest + services: + # ... + my_service: + image: my_service:latest + # ... +``` + +> :information_source: Please refer to the official [GitHub Actions Documentation](https://docs.github.com/en/actions/using-containerized-services/about-service-containers) for more information on how to add a service container. + +If we determine the service container slows down the test suite significantly, it may make sense to copy the marix and steps stanzas from an existing instrumentation and update it to use the new service container as a dependency: + +```yaml + + instrumentation_silver: + strategy: + fail-fast: false + matrix: + gem: + - werewolf + os: + - ubuntu-latest + name: other / ${{ matrix.gem }} / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: "Test Ruby 3.3" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-instrumentation-${{ matrix.gem }}" + ruby: "3.3" + - name: "Test Ruby 3.2" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-instrumentation-${{ matrix.gem }}" + ruby: "3.2" + - name: "Test Ruby 3.1" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-instrumentation-${{ matrix.gem }}" + ruby: "3.1" + - name: "Test Ruby 3.0" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-instrumentation-${{ matrix.gem }}" + ruby: "3.0" + yard: true + rubocop: true + build: true + - name: "Test JRuby" + uses: ./.github/actions/test_gem + with: + gem: "opentelemetry-instrumentation-${{ matrix.gem }}" + ruby: "jruby-9.4.2.0" + services: + # ... + my_service: + image: my_service:latest + # ... +``` + +## Documentation + +### README and Yardoc + +The `instrumentation_generator` creates a `README.md` file for your instrumentation. Please ensure that the `README` is up-to-date and contains the following: + +1. The span names, events, and semantic attributes emitted by the instrumentation +2. The configuration options available +3. Any known limitations or caveats +4. The minimum supported gem version + +> :information_source: See the `ActiveJob` instrumentation [`README`](./active_job/README.md) for a comprehensive example. + +In addition to that, there should also be redundant `yardoc` comments in the entrypoint of your gem, i.e. the subclass `OpenTelemetry::Instrumentation::Base`. + +> :information_source: See the `Sidekiq::Instrumentation` [class description](./sidekiq/lib/opentelemetry/instrumentation/sidekiq/instrumentation.rb) for a comprehensive example. + +### Examples + +Executuable examples should be included in the `examples` directory that demonstrate how to use the instrumentation in a real-world scenario. + +We recommend using [Bundler's inline gemfile](https://bundler.io/guides/bundler_in_a_single_file_ruby_script.html) to run the examples. Here is an example from the `grape` instrumentation: + +```ruby + +# frozen_string_literal: true + +require 'bundler/inline' + +gemfile(true) do + source 'https://rubygems.org' + gem 'grape', '~> 1.2' + gem 'opentelemetry-sdk' + gem 'opentelemetry-instrumentation-rack' + gem 'opentelemetry-instrumentation-grape' +end + +# Export traces to console +ENV['OTEL_TRACES_EXPORTER'] ||= 'console' + +OpenTelemetry::SDK.configure do |c| + c.service_name = 'trace_demonstration' + c.use_all # this will only require instrumentation gems it finds that are installed by bundler. +end + +at_exit do + OpenTelemetry.tracer_provider.shutdown +end + +# A basic Grape API example +class ExampleAPI < Grape::API + format :json + + desc 'Return a greeting message' + get :hello do + { message: 'Hello, world!' } + end + + desc 'Return information about a user' + params do + requires :id, type: Integer, desc: 'User ID' + end + get 'users/:id' do + { id: params[:id], name: 'John Doe', email: 'johndoe@example.com' } + end +end + +# Set up fake Rack application +builder = Rack::Builder.app do + # Integration is automatic in web frameworks but plain Rack applications require this line. + # Enable it in your config.ru. + use *OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.middleware_args + run ExampleAPI +end +app = Rack::MockRequest.new(builder) + +app.get('/hello') +app.get('/users/1') + +``` + +## Submit a pull request + +You are encouraged to submit a `draft` pull request early in the development process to get feedback from the maintainers, run your tests via CI, and ensure that your changes are in line with the project's goals. + +The `CODEOWNERS` is used to notify instrumentation authors when a pull request is opened. Please add yourself to the `instrumentation` section of the `CODEOWNERS` file, e.g. + +```plaintext + +instrumentation/werewolf/ @lycanthrope @open-telemetry/ruby-contrib-maintainers @open-telemetry/ruby-contrib-approvers + +``` + +> :information_source: In order for you to receive a request to review PRs, you must be a member of the [`open-telemetry/community`](https://github.com/open-telemetry/community/blob/5db097b38ce930fb1ff3eb79a1625bae46136894/community-membership.md#community-membership). Please consider applying for membership if you are not already a member. + +The [CONTRIBUTING.md](../CONTRIBUTING.md) guide has the remaining steps to get your contribution reviewed, merged, and released. diff --git a/instrumentation/README.md b/instrumentation/README.md index a20b8b1f9..770e1a70d 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -10,7 +10,6 @@ Released instrumentations can be found at the [OpenTelemetry registry](https://o In-progress instrumentations can be found [here](https://github.com/open-telemetry/opentelemetry-ruby-contrib/issues?q=is%3Aopen+label%3Ainstrumentation+-label%3A%22help+wanted%22+). - ## How do I get started? ### Individual instrumentation libraries @@ -19,16 +18,20 @@ To get started with a single instrumentation library, for example `opentelemetry ### 1. Install the gem -``` +```console + gem install opentelemetry-instrumentation-rack + ``` ### 2. Configure OpenTelemetry to use the instrumentation -``` +```console + OpenTelemetry::SDK.configure do |c| c.use 'OpenTelemetry::Instrumentation::Rack' end + ``` Instrumentation-specific documentation can be found in each subdirectory's `README.md`. diff --git a/instrumentation/action_mailer/.rubocop.yml b/instrumentation/action_mailer/.rubocop.yml new file mode 100644 index 000000000..1248a2f82 --- /dev/null +++ b/instrumentation/action_mailer/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: ../../.rubocop.yml diff --git a/instrumentation/action_mailer/.yardopts b/instrumentation/action_mailer/.yardopts new file mode 100644 index 000000000..cd50652b7 --- /dev/null +++ b/instrumentation/action_mailer/.yardopts @@ -0,0 +1,9 @@ +--no-private +--title=OpenTelemetry Action Mailer Instrumentation +--markup=markdown +--main=README.md +./lib/opentelemetry/instrumentation/**/*.rb +./lib/opentelemetry/instrumentation.rb +- +README.md +CHANGELOG.md diff --git a/instrumentation/action_mailer/Appraisals b/instrumentation/action_mailer/Appraisals new file mode 100644 index 000000000..018ce75b2 --- /dev/null +++ b/instrumentation/action_mailer/Appraisals @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +appraise 'rails-6.1' do + gem 'rails', '~> 6.1.0' +end + +appraise 'rails-7.0' do + gem 'rails', '~> 7.0.0' +end + +appraise 'rails-7.1' do + gem 'rails', '~> 7.1.0' +end diff --git a/instrumentation/action_mailer/CHANGELOG.md b/instrumentation/action_mailer/CHANGELOG.md new file mode 100644 index 000000000..8f0fc70b8 --- /dev/null +++ b/instrumentation/action_mailer/CHANGELOG.md @@ -0,0 +1,5 @@ +# Release History: opentelemetry-instrumentation-action_mailer + +### v0.1.0 / 2024-05-13 + +Initial release. diff --git a/instrumentation/action_mailer/Gemfile b/instrumentation/action_mailer/Gemfile new file mode 100644 index 000000000..2ededff74 --- /dev/null +++ b/instrumentation/action_mailer/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 'opentelemetry-instrumentation-active_support', path: '../active_support' +end diff --git a/instrumentation/action_mailer/LICENSE b/instrumentation/action_mailer/LICENSE new file mode 100644 index 000000000..1ef7dad2c --- /dev/null +++ b/instrumentation/action_mailer/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/action_mailer/README.md b/instrumentation/action_mailer/README.md new file mode 100644 index 000000000..7744fc202 --- /dev/null +++ b/instrumentation/action_mailer/README.md @@ -0,0 +1,102 @@ +# OpenTelemetry ActionMailer Instrumentation + +The ActionMailer instrumentation is a community-maintained instrumentation for the ActionMailer portion of the [Ruby on Rails][rails-home] web-application framework. + +## How do I get started? + +Install the gem using: + +```bash +# Install just the ActionMailer instrumentation +gem install opentelemetry-instrumentation-action_mailer +# Install the ActionMailer instrumentation along with the rest of the Rails-related instrumentation +gem install opentelemetry-instrumentation-rails +``` + +Or, if you use [bundler][bundler-home], include `opentelemetry-instrumentation-action_mailer` in your `Gemfile`. + +## Usage + +To use the instrumentation, call `use` with the name of the instrumentation: + +```ruby +OpenTelemetry::SDK.configure do |c| + # Use only the ActionMailer instrumentation + c.use 'OpenTelemetry::Instrumentation::ActionMailer' + # Use the ActionMailer instrumentation along with the rest of the Rails-related instrumentation + c.use 'OpenTelemetry::Instrumentation::Rails' +end +``` + +Alternatively, you can also call `use_all` to install all the available instrumentation. + +```ruby +OpenTelemetry::SDK.configure do |c| + c.use_all +end +``` + +## Active Support Instrumentation + +This instrumentation relies entirely on `ActiveSupport::Notifications` and registers a custom Subscriber that listens to relevant events to report as spans. + +See the table below for details of what [Rails Framework Hook Events](https://guides.rubyonrails.org/active_support_instrumentation.html#action-mailer) are recorded by this instrumentation: + +| Event Name | Creates Span? | Notes | +| - | - | - | +| `deliver.action_mailer` | :white_check_mark: | Creates an span with kind `internal` and email content and status| +| `process.action_mailer` | :x: | Lack of useful info so ignored | + +### Options + +ActionMailer instrumentation doesn't expose email addresses by default, but if email addresses are needed, simply use `:email_address` option: +```ruby +OpenTelemetry::SDK.configure do |c| + c.use 'OpenTelemetry::Instrumentation::ActionMailer', { email_address: :include } +end +``` + +If only want to hide certain attributes from the notifications payload for email address: +```ruby +OpenTelemetry::SDK.configure do |c| + c.use 'OpenTelemetry::Instrumentation::ActionMailer', { email_address: :include, disallowed_notification_payload_keys: ['email.to.address'] } +end +``` + +## Semantic Conventions + +Internal spans are named using the name of the `ActiveSupport` event that was provided (e.g. `action_mailer deliver`). + +The following attributes from the notification payload for the `deliver.action_mailer` event are attached to `action_mailer deliver` spans: + +| Attribute Name | Type | Notes | +| - | - | - | +| `email.x_mailer` | String | Mailer class that is used to send mail | +| `email.message_id` | String | Set from Mail gem | +| `email.subject` | String | Mail subject | +| `email.to.address` | Array | Receiver for mail (omit by default, include when `email_address` set to `:include`) | +| `email.from.address` | Array | Sender for mail (omit by default, include when `email_address` set to `:include`) | +| `email.cc.address` | Array | mail CC (omit by default, include when `email_address` set to `:include`) | +| `email.bcc.address` | Array | mail BCC (omit by default, include when `email_address` set to `:include`) | +## Examples + +Example usage can be seen in the `./example/trace_request_demonstration.ru` file [here](https://github.com/open-telemetry/opentelemetry-ruby-contrib/blob/main/instrumentation/action_mailer/example/trace_request_demonstration.ru) + + +## How can I get involved? + +The `opentelemetry-instrumentation-action_mailer` 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 + +The `opentelemetry-instrumentation-action_mailer` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information. + +[rails-home]: https://github.com/rails/rails +[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/action_mailer/Rakefile b/instrumentation/action_mailer/Rakefile new file mode 100644 index 000000000..1a64ba842 --- /dev/null +++ b/instrumentation/action_mailer/Rakefile @@ -0,0 +1,28 @@ +# 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'] +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/action_mailer/example/app/views/test_mailer/welcome_email.html.erb b/instrumentation/action_mailer/example/app/views/test_mailer/welcome_email.html.erb new file mode 100644 index 000000000..1f25f75d9 --- /dev/null +++ b/instrumentation/action_mailer/example/app/views/test_mailer/welcome_email.html.erb @@ -0,0 +1,10 @@ + + + + Welcome to OpenTelemetry! + + +

Welcome!

+

Thank you for joining OpenTelemetry.

+ + diff --git a/instrumentation/action_mailer/example/tmp/local_secret.txt b/instrumentation/action_mailer/example/tmp/local_secret.txt new file mode 100644 index 000000000..277a74ad5 --- /dev/null +++ b/instrumentation/action_mailer/example/tmp/local_secret.txt @@ -0,0 +1 @@ +11007269270afc9d0da1bf5aebb7b00264473f1f36b82208c8f0d4375f516e03f41d91e2f81c1f6482b95540b07a7c4444d8cf329e8337dd397b14a59dd14a0e \ No newline at end of file diff --git a/instrumentation/action_mailer/example/trace_request_demonstration.ru b/instrumentation/action_mailer/example/trace_request_demonstration.ru new file mode 100644 index 000000000..2f6f6672c --- /dev/null +++ b/instrumentation/action_mailer/example/trace_request_demonstration.ru @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/inline' + +gemfile(true) do + source 'https://rubygems.org' + + gem 'rails' + gem 'opentelemetry-sdk' + gem 'stringio', '~> 3' + gem 'opentelemetry-instrumentation-active_support', path: '../../active_support' + gem 'opentelemetry-instrumentation-action_mailer', path: '../' +end + +require 'action_mailer/railtie' + +# TraceRequestApp is a minimal Rails application inspired by the Rails +# bug report template for action controller. +# The configuration is compatible with Rails 6.0 +class TraceRequestApp < Rails::Application + config.root = __dir__ + config.hosts << 'example.org' + credentials.secret_key_base = 'secret_key_base' + + config.eager_load = false + + config.logger = Logger.new($stdout) + Rails.logger = config.logger + + config.action_mailer.delivery_method = :test +end + +# A minimal test ApplicationMailer +class TestMailer < ActionMailer::Base + default from: 'no-reply@example.com' + + def welcome_email + mail(to: 'test_mailer@otel.org', subject: 'Welcome to OpenTelemetry!', cc: 'cc@example.com', bcc: 'bcc@example.com') + end +end + +# Simple setup for demonstration purposes, simple span processor should not be +# used in a production environment +span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new( + OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter.new +) + +OpenTelemetry::SDK.configure do |c| + c.use 'OpenTelemetry::Instrumentation::ActionMailer' + c.add_span_processor(span_processor) +end + +Rails.application.initialize! + +TestMailer.welcome_email.deliver_now + +# To run this example run the `ruby` command with this file +# Example: ruby trace_request_demonstration.ru +# Spans for the requests will appear in the console diff --git a/instrumentation/action_mailer/lib/opentelemetry-instrumentation-action_mailer.rb b/instrumentation/action_mailer/lib/opentelemetry-instrumentation-action_mailer.rb new file mode 100644 index 000000000..c034f140f --- /dev/null +++ b/instrumentation/action_mailer/lib/opentelemetry-instrumentation-action_mailer.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/action_mailer/lib/opentelemetry/instrumentation.rb b/instrumentation/action_mailer/lib/opentelemetry/instrumentation.rb new file mode 100644 index 000000000..2111a4c9e --- /dev/null +++ b/instrumentation/action_mailer/lib/opentelemetry/instrumentation.rb @@ -0,0 +1,19 @@ +# 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 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/action_mailer' diff --git a/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer.rb b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer.rb new file mode 100644 index 000000000..198b9197b --- /dev/null +++ b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer.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 ActionMailer gem + module ActionMailer + end + end +end + +require_relative 'action_mailer/instrumentation' +require_relative 'action_mailer/version' diff --git a/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/instrumentation.rb b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/instrumentation.rb new file mode 100644 index 000000000..2b33478aa --- /dev/null +++ b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/instrumentation.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module ActionMailer + # The Instrumentation class contains logic to detect and install the ActionMailer instrumentation + class Instrumentation < OpenTelemetry::Instrumentation::Base + MINIMUM_VERSION = Gem::Version.new('6.1.0') + EMAIL_ATTRIBUTE = %w[email.to.address email.from.address email.cc.address email.bcc.address].freeze + + install do |_config| + resolve_email_address + ecs_mail_convention + require_dependencies + end + + present do + defined?(::ActionMailer) + end + + compatible do + gem_version >= MINIMUM_VERSION + end + + option :disallowed_notification_payload_keys, default: [], validate: :array + option :notification_payload_transform, default: nil, validate: :callable + option :email_address, default: :omit, validate: %I[omit include] + + private + + def gem_version + ::ActionMailer.version + end + + def resolve_email_address + return unless _config[:email_address] == :omit + + _config[:disallowed_notification_payload_keys] += EMAIL_ATTRIBUTE + end + + def ecs_mail_convention + if _config[:notification_payload_transform].nil? + transform_attributes = ->(payload) { transform_payload(payload) } + else + original_callable = _config[:notification_payload_transform] + transform_attributes = lambda do |payload| + new_payload = transform_payload(payload) + user_payload = original_callable.call(new_payload) + if user_payload.instance_of?(Hash) + user_payload + else + OpenTelemetry.logger.error("ActionMailer: transformed payload is #{user_payload.class} (require Hash)") + new_payload + end + end + end + _config[:notification_payload_transform] = transform_attributes + end + + def _config + ActionMailer::Instrumentation.instance.config + end + + # email attribute key convention is obtained from: https://www.elastic.co/guide/en/ecs/8.11/ecs-email.html + def transform_payload(payload) + new_payload = {} + new_payload['email.message_id'] = payload[:message_id] if payload[:message_id] + new_payload['email.subject'] = payload[:subject] if payload[:subject] + new_payload['email.x_mailer'] = payload[:mailer] if payload[:mailer] + new_payload['email.to.address'] = payload[:to] if payload[:to] + new_payload['email.from.address'] = payload[:from] if payload[:from] + new_payload['email.cc.address'] = payload[:cc] if payload[:cc] + new_payload['email.bcc.address'] = payload[:bcc] if payload[:bcc] + new_payload['email.origination_timestamp'] = payload[:date] if payload[:date] + new_payload + end + + def require_dependencies + require_relative 'railtie' + end + end + end + end +end diff --git a/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/railtie.rb b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/railtie.rb new file mode 100644 index 000000000..0b036d0e9 --- /dev/null +++ b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/railtie.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module ActionMailer + SUBSCRIPTIONS = %w[ + deliver.action_mailer + ].freeze + + # This Railtie sets up subscriptions to relevant ActionMailer notifications + class Railtie < ::Rails::Railtie + config.after_initialize do + ::OpenTelemetry::Instrumentation::ActiveSupport::Instrumentation.instance.install({}) + + SUBSCRIPTIONS.each do |subscription_name| + config = ActionMailer::Instrumentation.instance.config + ::OpenTelemetry::Instrumentation::ActiveSupport.subscribe( + ActionMailer::Instrumentation.instance.tracer, + subscription_name, + config[:notification_payload_transform], + config[:disallowed_notification_payload_keys] + ) + end + end + end + end + end +end diff --git a/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/version.rb b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/version.rb new file mode 100644 index 000000000..20756b5f2 --- /dev/null +++ b/instrumentation/action_mailer/lib/opentelemetry/instrumentation/action_mailer/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 ActionMailer + VERSION = '0.1.0' + end + end +end diff --git a/instrumentation/action_mailer/opentelemetry-instrumentation-action_mailer.gemspec b/instrumentation/action_mailer/opentelemetry-instrumentation-action_mailer.gemspec new file mode 100644 index 000000000..227bf6e36 --- /dev/null +++ b/instrumentation/action_mailer/opentelemetry-instrumentation-action_mailer.gemspec @@ -0,0 +1,50 @@ +# 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/action_mailer/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-instrumentation-action_mailer' + spec.version = OpenTelemetry::Instrumentation::ActionMailer::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'ActionMailer instrumentation for the OpenTelemetry framework' + spec.description = 'ActionMailer 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-active_support', '~> 0.1' + 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 'rake', '~> 13.0' + spec.add_development_dependency 'rubocop', '~> 1.60.1' + spec.add_development_dependency 'rubocop-performance', '~> 1.20' + 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/action_mailer' + 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/action_mailer/test/opentelemetry/instrumentation/action_mailer/instrumentation_test.rb b/instrumentation/action_mailer/test/opentelemetry/instrumentation/action_mailer/instrumentation_test.rb new file mode 100644 index 000000000..24d7cc8c8 --- /dev/null +++ b/instrumentation/action_mailer/test/opentelemetry/instrumentation/action_mailer/instrumentation_test.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::Instrumentation::ActionMailer do + let(:instrumentation) { OpenTelemetry::Instrumentation::ActionMailer::Instrumentation.instance } + let(:payload) do + { + mailer: 'TestMailer', + message_id: '6638fab8d3cdb_f0b2c52420@8b5092010d2f.mail', + subject: 'Welcome to OpenTelemetry!', + to: ['test_mailer@otel.org'], + from: ['no-reply@example.com'], + bcc: ['bcc@example.com'], + cc: ['cc@example.com'], + perform_deliveries: true + } + end + + it 'has #name' do + _(instrumentation.name).must_equal 'OpenTelemetry::Instrumentation::ActionMailer' + end + + it 'has #version' do + _(instrumentation.version).wont_be_nil + _(instrumentation.version).wont_be_empty + end + + describe '#install' do + it 'accepts argument' do + _(instrumentation.install({})).must_equal(true) + instrumentation.instance_variable_set(:@installed, false) + end + end + + describe '#install with default options' do + it 'with default options' do + _(instrumentation.config[:disallowed_notification_payload_keys]).wont_be_empty + _(instrumentation.config[:email_address]).must_equal :omit + end + end + + describe '#resolve_email_address' do + it 'with include' do + original_config = instrumentation.instance_variable_get(:@config) + modified_config = original_config.dup + modified_config[:email_address] = :include + modified_config[:disallowed_notification_payload_keys] = [] + instrumentation.instance_variable_set(:@config, modified_config) + + instrumentation.send(:resolve_email_address) + _(instrumentation.config[:disallowed_notification_payload_keys].size).must_equal 0 + + instrumentation.instance_variable_set(:@config, original_config) + end + end + + describe '#transform_payload' do + it 'with simple payload' do + payload = { + mailer: 'TestMailer', + message_id: '6638fab8d3cdb_f0b2c52420@8b5092010d2f.mail', + subject: 'Welcome to OpenTelemetry!', + to: ['test_mailer@otel.org'], + from: ['no-reply@example.com'], + bcc: ['bcc@example.com'], + cc: ['cc@example.com'], + perform_deliveries: true + } + tranformed_payload = instrumentation.send(:transform_payload, payload) + + _(tranformed_payload['email.message_id']).must_equal '6638fab8d3cdb_f0b2c52420@8b5092010d2f.mail' + _(tranformed_payload['email.subject']).must_equal 'Welcome to OpenTelemetry!' + _(tranformed_payload['email.x_mailer']).must_equal 'TestMailer' + _(tranformed_payload['email.to.address'][0]).must_equal 'test_mailer@otel.org' + _(tranformed_payload['email.from.address'][0]).must_equal 'no-reply@example.com' + _(tranformed_payload['email.cc.address'][0]).must_equal 'cc@example.com' + _(tranformed_payload['email.bcc.address'][0]).must_equal 'bcc@example.com' + end + end + + describe '#ecs_mail_convention' do + it 'with user-defined payload' do + original_config = instrumentation.instance_variable_get(:@config) + modified_config = original_config.dup + + modified_config[:notification_payload_transform] = ->(payload) { payload['email.message_id'] = 'fake_message_id' } + instrumentation.instance_variable_set(:@config, modified_config) + + instrumentation.send(:ecs_mail_convention) + payload = { mailer: 'TestMailer' } + + tranformed_payload = instrumentation.config[:notification_payload_transform].call(payload) + + _(tranformed_payload['email.message_id']).must_equal 'fake_message_id' + + instrumentation.instance_variable_set(:@config, original_config) + end + + it 'without user-defined payload' do + tranformed_payload = instrumentation.config[:notification_payload_transform].call(payload) + + _(tranformed_payload['email.message_id']).must_equal '6638fab8d3cdb_f0b2c52420@8b5092010d2f.mail' + _(tranformed_payload['email.subject']).must_equal 'Welcome to OpenTelemetry!' + _(tranformed_payload['email.x_mailer']).must_equal 'TestMailer' + _(tranformed_payload['email.to.address'][0]).must_equal 'test_mailer@otel.org' + _(tranformed_payload['email.from.address'][0]).must_equal 'no-reply@example.com' + _(tranformed_payload['email.cc.address'][0]).must_equal 'cc@example.com' + _(tranformed_payload['email.bcc.address'][0]).must_equal 'bcc@example.com' + end + end +end diff --git a/instrumentation/action_mailer/test/test_helper.rb b/instrumentation/action_mailer/test/test_helper.rb new file mode 100644 index 000000000..b68d22b82 --- /dev/null +++ b/instrumentation/action_mailer/test/test_helper.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/setup' +Bundler.require(:default, :development, :test) + +require 'action_mailer' +require 'opentelemetry-instrumentation-action_mailer' +require 'minitest/autorun' +require 'webmock/minitest' + +# global opentelemetry-sdk setup: +EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new +span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER) + +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::ActionMailer' + c.add_span_processor span_processor +end 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..9e8d73ce0 --- /dev/null +++ b/instrumentation/aws_lambda/CHANGELOG.md @@ -0,0 +1,5 @@ +# Release History: opentelemetry-instrumentation-aws_lambda + +### v0.1.0 / 2024-05-11 + +Initial release. 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/instrumentation/datadog-porting-guide.md b/instrumentation/datadog-porting-guide.md index dc9c63c75..1ccadefd5 100644 --- a/instrumentation/datadog-porting-guide.md +++ b/instrumentation/datadog-porting-guide.md @@ -16,11 +16,15 @@ Aid developers who wish to port existing datadog (dd-trace-rb) instrumentation t * Add Gemfile, opentelemetry-instrumentation-#{name}.gemspec * Add runnable (docker) example (using its own Gemfile) -``` + +```console + $ docker-compose run ex-instrumentation-myinstrumentation bundle install $ docker-compose run ex-instrumentation-myinstrumentation bash-5.0$ ruby trace_demonstration.rb + ``` + * Rakefile * `tests/test_helper.rb` * Integrate rubocop (see https://github.com/open-telemetry/opentelemetry-ruby/pull/172#pullrequestreview-349183775) @@ -66,7 +70,7 @@ bash-5.0$ ruby trace_demonstration.rb ### Runtime performance considerations Watch for "low hanging fruit" performance improvements including: -* reduce object allocations - move to a constant, or cache values that would be generated repeatedly +* reduce object allocations - move to a constant, or cache values that would be generated repeatedly * look for "easy wins" vs. "complete redesigns" ## Testing diff --git a/instrumentation/grape/example/trace_demonstration.rb b/instrumentation/grape/example/trace_demonstration.rb index ca93f5eea..7f67a93d1 100644 --- a/instrumentation/grape/example/trace_demonstration.rb +++ b/instrumentation/grape/example/trace_demonstration.rb @@ -4,20 +4,22 @@ gemfile(true) do source 'https://rubygems.org' - gem 'opentelemetry-api' - gem 'opentelemetry-instrumentation-grape' + gem 'grape', '~> 1.2' gem 'opentelemetry-sdk' - gem 'grape' + gem 'opentelemetry-instrumentation-rack' + gem 'opentelemetry-instrumentation-grape' end -require 'opentelemetry-instrumentation-rack' - # Export traces to console ENV['OTEL_TRACES_EXPORTER'] ||= 'console' OpenTelemetry::SDK.configure do |c| c.service_name = 'trace_demonstration' - c.use_all # this will only require instrumentation gems it finds that are installed by bundler. + c.use_all # this will only require instrumentation gems it finds that are installed by bundler. +end + +at_exit do + OpenTelemetry.tracer_provider.shutdown end # A basic Grape API example @@ -30,9 +32,6 @@ class ExampleAPI < Grape::API end desc 'Return information about a user' - # Filters - before { sleep(0.01) } - after { sleep(0.01) } params do requires :id, type: Integer, desc: 'User ID' end @@ -45,7 +44,7 @@ class ExampleAPI < Grape::API builder = Rack::Builder.app do # Integration is automatic in web frameworks but plain Rack applications require this line. # Enable it in your config.ru. - use OpenTelemetry::Instrumentation::Rack::Middlewares::TracerMiddleware + use *OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.middleware_args run ExampleAPI end app = Rack::MockRequest.new(builder) diff --git a/instrumentation/pg/CHANGELOG.md b/instrumentation/pg/CHANGELOG.md index 45901ccbc..fe3d3eb37 100644 --- a/instrumentation/pg/CHANGELOG.md +++ b/instrumentation/pg/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History: opentelemetry-instrumentation-pg +### v0.27.3 / 2024-05-11 + +* ADDED: Support prepend SQL comment for PG instrumentation + ### v0.27.2 / 2024-04-30 * FIXED: Bundler conflict warnings diff --git a/instrumentation/pg/lib/opentelemetry/instrumentation/pg/patches/connection.rb b/instrumentation/pg/lib/opentelemetry/instrumentation/pg/patches/connection.rb index 49d0946d5..631610acf 100644 --- a/instrumentation/pg/lib/opentelemetry/instrumentation/pg/patches/connection.rb +++ b/instrumentation/pg/lib/opentelemetry/instrumentation/pg/patches/connection.rb @@ -112,7 +112,9 @@ def span_attrs(kind, *args) def extract_operation(sql) # From: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/9244a08a8d014afe26b82b91cf86e407c2599d73/plugins/node/opentelemetry-instrumentation-pg/src/utils.ts#L35 - sql.to_s.split[0].to_s.upcase + # Ignores prepend comment + comment_regex = %r{\A\/\*.*?\*\/}m + sql.to_s.sub(comment_regex, '').split[0].to_s.upcase end def span_name(operation) diff --git a/instrumentation/pg/lib/opentelemetry/instrumentation/pg/version.rb b/instrumentation/pg/lib/opentelemetry/instrumentation/pg/version.rb index bbd9e6c4c..105f4152e 100644 --- a/instrumentation/pg/lib/opentelemetry/instrumentation/pg/version.rb +++ b/instrumentation/pg/lib/opentelemetry/instrumentation/pg/version.rb @@ -7,7 +7,7 @@ module OpenTelemetry module Instrumentation module PG - VERSION = '0.27.2' + VERSION = '0.27.3' end end end diff --git a/instrumentation/pg/test/opentelemetry/instrumentation/pg/instrumentation_test.rb b/instrumentation/pg/test/opentelemetry/instrumentation/pg/instrumentation_test.rb index 332e90720..b7e97b9ac 100644 --- a/instrumentation/pg/test/opentelemetry/instrumentation/pg/instrumentation_test.rb +++ b/instrumentation/pg/test/opentelemetry/instrumentation/pg/instrumentation_test.rb @@ -177,6 +177,18 @@ end end + it 'ignores prepend comment to extract operation' do + client.query('/* comment */ SELECT 1') + + _(span.name).must_equal 'SELECT postgres' + _(span.attributes['db.system']).must_equal 'postgresql' + _(span.attributes['db.name']).must_equal 'postgres' + _(span.attributes['db.statement']).must_equal '/* comment */ SELECT 1' + _(span.attributes['db.operation']).must_equal 'SELECT' + _(span.attributes['net.peer.name']).must_equal host.to_s + _(span.attributes['net.peer.port']).must_equal port.to_i + end + it 'only caches 50 prepared statement names' do 51.times { |i| client.prepare("foo#{i}", "SELECT $1 AS foo#{i}") } client.exec_prepared('foo0', [1]) diff --git a/release-please-config.json b/release-please-config.json index 8686eb523..5a871d495 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -49,6 +49,10 @@ "package-name": "opentelemetry-instrumentation-active_support", "version-file": "lib/opentelemetry/instrumentation/active_support/version.rb" }, + "instrumentation/action_mailer": { + "package-name": "opentelemetry-instrumentation-action_mailer", + "version-file": "lib/opentelemetry/instrumentation/action_mailer/version.rb" + }, "instrumentation/action_view": { "package-name": "opentelemetry-instrumentation-action_view", "version-file": "lib/opentelemetry/instrumentation/action_view/version.rb" @@ -81,6 +85,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"