diff --git a/.github/actions/test_gem/action.yml b/.github/actions/test_gem/action.yml index c26f0d703..4f6b15fe4 100644 --- a/.github/actions/test_gem/action.yml +++ b/.github/actions/test_gem/action.yml @@ -62,6 +62,14 @@ runs: fi fi + # Install ImageMagick for active_storage testing. + # Unfortunately, from ubuntu-24.04, ImageMagick is no longer pre-installed in Github Actions. + # See https://github.com/actions/runner-images/issues/10772 + - name: Install ImageMagick for active_storage testing + if: "${{ inputs.gem == 'opentelemetry-instrumentation-active_storage' }}" + shell: bash + run: sudo apt update && sudo apt install -y imagemagick + # Install ruby and bundle dependencies and cache! # ...but not for appraisals, sadly. - name: Install Ruby ${{ inputs.ruby }} with dependencies diff --git a/.github/workflows/ci-instrumentation.yml b/.github/workflows/ci-instrumentation.yml index cf938e48f..50f8e9708 100644 --- a/.github/workflows/ci-instrumentation.yml +++ b/.github/workflows/ci-instrumentation.yml @@ -27,6 +27,7 @@ jobs: - active_job - active_model_serializers - active_record + - active_storage - active_support - all - aws_sdk @@ -91,6 +92,7 @@ jobs: [[ "${{ matrix.gem }}" == "action_view" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "active_model_serializers" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "active_record" ]] && echo "skip=true" >> $GITHUB_OUTPUT + [[ "${{ matrix.gem }}" == "active_storage" ]] && 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 diff --git a/.toys/.data/releases.yml b/.toys/.data/releases.yml index a90d626e3..ccb62f516 100644 --- a/.toys/.data/releases.yml +++ b/.toys/.data/releases.yml @@ -30,6 +30,10 @@ commit_lint: # * changelog_path: Path to CHANGLEOG.md relative to the gem directory. # (Required only if it is not in the expected location.) gems: + - name: opentelemetry-instrumentation-active_storage + directory: instrumentation/active_storage + version_constant: [OpenTelemetry, Instrumentation, ActiveStorage, VERSION] + - name: opentelemetry-instrumentation-gruf directory: instrumentation/gruf version_constant: [OpenTelemetry, Instrumentation, Gruf, VERSION] diff --git a/instrumentation/active_storage/.rubocop.yml b/instrumentation/active_storage/.rubocop.yml new file mode 100644 index 000000000..1248a2f82 --- /dev/null +++ b/instrumentation/active_storage/.rubocop.yml @@ -0,0 +1 @@ +inherit_from: ../../.rubocop.yml diff --git a/instrumentation/active_storage/.yardopts b/instrumentation/active_storage/.yardopts new file mode 100644 index 000000000..1875c0c85 --- /dev/null +++ b/instrumentation/active_storage/.yardopts @@ -0,0 +1,9 @@ +--no-private +--title=OpenTelemetry Active Storage Instrumentation +--markup=markdown +--main=README.md +./lib/opentelemetry/instrumentation/**/*.rb +./lib/opentelemetry/instrumentation.rb +- +README.md +CHANGELOG.md diff --git a/instrumentation/active_storage/Appraisals b/instrumentation/active_storage/Appraisals new file mode 100644 index 000000000..38fcef500 --- /dev/null +++ b/instrumentation/active_storage/Appraisals @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +%w[6.1.0 7.0.0 7.1.0].each do |version| + appraise "activestorage-#{version}" do + gem 'sqlite3', '~> 1.4' + gem 'image_processing', '~> 1.2' + gem 'rails', "~> #{version}" + end +end + +if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.1.0') + %w[7.2.0].each do |version| + appraise "activestorage-#{version}" do + gem 'sqlite3', '~> 1.4' + gem 'image_processing', '~> 1.2' + gem 'rails', "~> #{version}" + end + end + + appraise 'activestorage-latest' do + gem 'sqlite3', '>= 2.1' + gem 'image_processing', '~> 1.2' + gem 'rails' + end +end diff --git a/instrumentation/active_storage/CHANGELOG.md b/instrumentation/active_storage/CHANGELOG.md new file mode 100644 index 000000000..6e60ce45e --- /dev/null +++ b/instrumentation/active_storage/CHANGELOG.md @@ -0,0 +1 @@ +# Release History: opentelemetry-instrumentation-active_storage diff --git a/instrumentation/active_storage/Gemfile b/instrumentation/active_storage/Gemfile new file mode 100644 index 000000000..2ededff74 --- /dev/null +++ b/instrumentation/active_storage/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/active_storage/LICENSE b/instrumentation/active_storage/LICENSE new file mode 100644 index 000000000..1ef7dad2c --- /dev/null +++ b/instrumentation/active_storage/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/active_storage/README.md b/instrumentation/active_storage/README.md new file mode 100644 index 000000000..f606e9989 --- /dev/null +++ b/instrumentation/active_storage/README.md @@ -0,0 +1,96 @@ +# OpenTelemetry ActiveStorage Instrumentation + +The ActiveStorage instrumentation is a community-maintained instrumentation for the ActiveStorage portion of the [Ruby on Rails][rails-home] web-application framework. + +## How do I get started? + +Install the gem using: + +```bash +# Install just the ActiveStorage instrumentation +gem install opentelemetry-instrumentation-active_storage +# Install the ActiveStorage 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-active_storage` 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 ActiveStorage instrumentation + c.use 'OpenTelemetry::Instrumentation::ActiveStorage' + # Use the ActiveStorage 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#active-storage) are recorded by this instrumentation: + +| Event Name | Creates Span? | Notes | +| - | - | - | +| `preview.active_storage` | :white_check_mark: | Creates an `internal` span | +| `transform.active_storage` | :white_check_mark: | Creates an `internal` span | +| `analyze.active_storage` | :white_check_mark: | Creates an `internal` span | +| `service_upload.active_storage` | :white_check_mark: | Creates an `internal` span | +| `service_streaming_download.active_storage` | :white_check_mark: | Creates an `internal` span | +| `service_download_chunk.active_storage` | :white_check_mark: | Creates an `internal` span | +| `service_download.active_storage` | :white_check_mark: | Creates an `internal` span | +| `service_delete.active_storage` | :white_check_mark: | Creates an `internal` span | +| `service_delete_prefixed.active_storage` | :white_check_mark: | Creates an `internal` span | +| `service_exist.active_storage` | :white_check_mark: | Creates an `internal` span | +| `service_url.active_storage` | :white_check_mark: | Creates an `internal` span | +| `service_update_metadata.active_storage` | :white_check_mark: | Creates an `internal` span | + +### Options + +ActiveStorage instrumentation doesn't expose secure tokens and urls by default, but if they are needed, simply use `:key` and `:url` option: + +```ruby +OpenTelemetry::SDK.configure do |c| + c.use 'OpenTelemetry::Instrumentation::ActiveStorage', { key: :include, url: :include } +end +``` + +## Semantic Conventions + +Internal spans are named using the name of the `ActiveSupport` event that was provided (e.g. `service_upload.active_storage`). + +Attributes attached to each event payload are prefixed with `active_storage.` (e.g. `active_storage.checksum`). + +## Examples + +Example usage can be seen in the `./example/trace_demonstration.rb` file [here](https://github.com/open-telemetry/opentelemetry-ruby-contrib/blob/main/instrumentation/active_storage/example/trace_demonstration.rb) + +## How can I get involved? + +The `opentelemetry-instrumentation-active_storage` 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 on our [GitHub Discussions][discussions-url], [Slack Channel][slack-channel] 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-active_storage` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information. + +[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 +[slack-channel]: https://cloud-native.slack.com/archives/C01NWKKMKMY +[discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions diff --git a/instrumentation/active_storage/Rakefile b/instrumentation/active_storage/Rakefile new file mode 100644 index 000000000..1a64ba842 --- /dev/null +++ b/instrumentation/active_storage/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/active_storage/example/trace_demonstration.rb b/instrumentation/active_storage/example/trace_demonstration.rb new file mode 100644 index 000000000..95feb1a36 --- /dev/null +++ b/instrumentation/active_storage/example/trace_demonstration.rb @@ -0,0 +1,97 @@ +# 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 'sqlite3' + gem 'opentelemetry-sdk' + gem 'opentelemetry-instrumentation-active_support', path: '../../active_support' + gem 'opentelemetry-instrumentation-active_storage', path: '../' +end + +require 'active_storage/engine' + +# 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.active_storage.service = :development + config.active_storage.service_configurations = { + development: { + service: 'Disk', + root: Dir.mktmpdir('active_storage_tests') + } + } + + # Override to avoid reading config/database.yml + def config.database_configuration + { + development: { + adapter: 'sqlite3', + database: ':memory:' + } + } + 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::ActiveStorage' + c.add_span_processor(span_processor) +end + +Rails.application.initialize! + +ActiveRecord::Base.logger = Logger.new(STDOUT) +ActiveRecord::Schema.define do + create_table :active_storage_blobs, force: true do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false + t.index [:key], unique: true + end + + create_table :active_storage_attachments, force: true do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false + t.references :blob, null: false + + t.datetime :created_at, null: false + t.index %i[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', unique: true + end +end + +ActiveStorage::Blob.create_and_upload!( + io: StringIO.new('test file content'), + filename: 'test.txt', + content_type: 'text/plain' +) + +# To run this example run the `ruby` command with this file +# Example: ruby trace_demonstration.rb diff --git a/instrumentation/active_storage/lib/opentelemetry-instrumentation-active_storage.rb b/instrumentation/active_storage/lib/opentelemetry-instrumentation-active_storage.rb new file mode 100644 index 000000000..c034f140f --- /dev/null +++ b/instrumentation/active_storage/lib/opentelemetry-instrumentation-active_storage.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/active_storage/lib/opentelemetry/instrumentation.rb b/instrumentation/active_storage/lib/opentelemetry/instrumentation.rb new file mode 100644 index 000000000..b736806ca --- /dev/null +++ b/instrumentation/active_storage/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/active_storage' diff --git a/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage.rb b/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage.rb new file mode 100644 index 000000000..a671fda42 --- /dev/null +++ b/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage.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 ActiveStorage gem + module ActiveStorage + end + end +end + +require_relative 'active_storage/instrumentation' +require_relative 'active_storage/version' diff --git a/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage/instrumentation.rb b/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage/instrumentation.rb new file mode 100644 index 000000000..9ac800af0 --- /dev/null +++ b/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage/instrumentation.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module ActiveStorage + # The {OpenTelemetry::Instrumentation::ActiveStorage::Instrumentation} class contains logic to detect and install the ActiveStorage instrumentation + # + # Installation and configuration of this instrumentation is done within the + # {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry/SDK#configure-instance_method OpenTelemetry::SDK#configure} + # block, calling {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use use()} + # or {https://www.rubydoc.info/gems/opentelemetry-sdk/OpenTelemetry%2FSDK%2FConfigurator:use_all use_all()}. + # + # ## Configuration keys and options + # + # ### `:disallowed_notification_payload_keys` + # + # Specifies an array of keys that should be excluded from the notification payload as span attributes. + # + # ### `:notification_payload_transform` + # + # - `proc` **default** `nil` + # + # Specifies custom proc used to extract span attributes form the notification payload. + # Use this to rename keys, extract nested values, or perform any other custom logic. + # + # ### `:key` + # + # - `symbol` **default** `:omit` + # + # Specifies whether to include secure token in the notification payload. Valid values are `:omit` and `:include`. + # + # ### `:url` + # + # - `symbol` **default** `:omit` + # + # Specifies whether to include url in the notification payload. Valid values are `:omit` and `:include`. + # + # @example An explicit default configuration + # OpenTelemetry::SDK.configure do |c| + # c.use_all({ + # 'OpenTelemetry::Instrumentation::ActionMailer' => { + # disallowed_notification_payload_keys: [], + # notification_payload_transform: nil, + # key: :omit, + # url: :omit, + # }, + # }) + # end + class Instrumentation < OpenTelemetry::Instrumentation::Base + MINIMUM_VERSION = Gem::Version.new('6.1.0') + + install do |_config| + resolve_key + resolve_url + resolve_payload_transform + require_dependencies + end + + present do + defined?(::ActiveStorage) + 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 :key, default: :omit, validate: %I[omit include] + option :url, default: :omit, validate: %I[omit include] + + private + + def gem_version + ::ActiveStorage.version + end + + def resolve_key + return unless _config[:key] == :omit + + _config[:disallowed_notification_payload_keys].append 'active_storage.key' + end + + def resolve_url + return unless _config[:url] == :omit + + _config[:disallowed_notification_payload_keys].append 'active_storage.url' + end + + def resolve_payload_transform + 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("ActiveStorage: transformed payload is #{user_payload.class} (require Hash)") + new_payload + end + end + end + _config[:notification_payload_transform] = transform_attributes + end + + def _config + ActiveStorage::Instrumentation.instance.config + end + + # add `active_storage.` prefix to each attribute + def transform_payload(payload) + payload.transform_keys { |k| "active_storage.#{k}" } + end + + def require_dependencies + require_relative 'railtie' + end + end + end + end +end diff --git a/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage/railtie.rb b/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage/railtie.rb new file mode 100644 index 000000000..0863ecbf7 --- /dev/null +++ b/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage/railtie.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module ActiveStorage + SUBSCRIPTIONS = %w[ + preview.active_storage + transform.active_storage + analyze.active_storage + service_upload.active_storage + service_streaming_download.active_storage + service_download_chunk.active_storage + service_download.active_storage + service_delete.active_storage + service_delete_prefixed.active_storage + service_exist.active_storage + service_url.active_storage + service_update_metadata.active_storage + ].freeze + + # This Railtie sets up subscriptions to relevant ActiveStorage notifications + class Railtie < ::Rails::Railtie + config.after_initialize do + ::OpenTelemetry::Instrumentation::ActiveSupport::Instrumentation.instance.install({}) + subscribe + end + + class << self + def subscribe + SUBSCRIPTIONS.each do |subscription_name| + ::OpenTelemetry::Instrumentation::ActiveSupport.subscribe( + ActiveStorage::Instrumentation.instance.tracer, + subscription_name, + config[:notification_payload_transform], + config[:disallowed_notification_payload_keys] + ) + end + end + + def unsubscribe + SUBSCRIPTIONS.each do |subscription_name| + ::ActiveSupport::Notifications.unsubscribe(subscription_name) + end + end + + def config + ActiveStorage::Instrumentation.instance.config + end + end + end + end + end +end diff --git a/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage/version.rb b/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage/version.rb new file mode 100644 index 000000000..7a85736de --- /dev/null +++ b/instrumentation/active_storage/lib/opentelemetry/instrumentation/active_storage/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 ActiveStorage + VERSION = '0.0.0' + end + end +end diff --git a/instrumentation/active_storage/opentelemetry-instrumentation-active_storage.gemspec b/instrumentation/active_storage/opentelemetry-instrumentation-active_storage.gemspec new file mode 100644 index 000000000..d539c28e4 --- /dev/null +++ b/instrumentation/active_storage/opentelemetry-instrumentation-active_storage.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/active_storage/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-instrumentation-active_storage' + spec.version = OpenTelemetry::Instrumentation::ActiveStorage::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'ActiveStorage instrumentation for the OpenTelemetry framework' + spec.description = 'ActiveStorage 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.4.0' + spec.add_dependency 'opentelemetry-instrumentation-active_support', '~> 0.7' + spec.add_dependency 'opentelemetry-instrumentation-base', '~> 0.22.6' + + 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.69.1' + spec.add_development_dependency 'rubocop-performance', '~> 1.23.0' + spec.add_development_dependency 'simplecov', '~> 0.17.1' + spec.add_development_dependency 'webmock', '~> 3.24.0' + 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/active_storage' + 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/active_storage/test/fixtures/sample.pdf b/instrumentation/active_storage/test/fixtures/sample.pdf new file mode 100644 index 000000000..c01805e89 Binary files /dev/null and b/instrumentation/active_storage/test/fixtures/sample.pdf differ diff --git a/instrumentation/active_storage/test/fixtures/sample.png b/instrumentation/active_storage/test/fixtures/sample.png new file mode 100644 index 000000000..293a1f43b Binary files /dev/null and b/instrumentation/active_storage/test/fixtures/sample.png differ diff --git a/instrumentation/active_storage/test/opentelemetry/instrumentation/active_storage/instrumentation_test.rb b/instrumentation/active_storage/test/opentelemetry/instrumentation/active_storage/instrumentation_test.rb new file mode 100644 index 000000000..e69d0df4e --- /dev/null +++ b/instrumentation/active_storage/test/opentelemetry/instrumentation/active_storage/instrumentation_test.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +require_relative '../../../../lib/opentelemetry/instrumentation/active_storage' + +describe OpenTelemetry::Instrumentation::ActiveStorage do + let(:instrumentation) { OpenTelemetry::Instrumentation::ActiveStorage::Instrumentation.instance } + let(:payload) do + { + checksum: 'BC3HWOZ8gHaD2PfLOgZP0w==', + service: 'S3' + } + end + + it 'has #name' do + _(instrumentation.name).must_equal 'OpenTelemetry::Instrumentation::ActiveStorage' + 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[:key]).must_equal :omit + _(instrumentation.config[:url]).must_equal :omit + end + end + + describe '#resolve_key' do + it 'with include' do + original_config = instrumentation.instance_variable_get(:@config) + modified_config = original_config.dup + modified_config[:key] = :include + modified_config[:disallowed_notification_payload_keys] = [] + instrumentation.instance_variable_set(:@config, modified_config) + + instrumentation.send(:resolve_key) + _(instrumentation.config[:disallowed_notification_payload_keys].size).must_equal 0 + + instrumentation.instance_variable_set(:@config, original_config) + end + end + + describe '#resolve_url' do + it 'with include' do + original_config = instrumentation.instance_variable_get(:@config) + modified_config = original_config.dup + modified_config[:url] = :include + modified_config[:disallowed_notification_payload_keys] = [] + instrumentation.instance_variable_set(:@config, modified_config) + + instrumentation.send(:resolve_url) + _(instrumentation.config[:disallowed_notification_payload_keys].size).must_equal 0 + + instrumentation.instance_variable_set(:@config, original_config) + end + end + + describe '#resolve_payload_transform' 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['active_storage.checksum'] = 'fake_checksum' } + instrumentation.instance_variable_set(:@config, modified_config) + + instrumentation.send(:resolve_payload_transform) + payload = { checksum: 'real_checksum' } + + tranformed_payload = instrumentation.config[:notification_payload_transform].call(payload) + + _(tranformed_payload['active_storage.checksum']).must_equal 'fake_checksum' + + instrumentation.instance_variable_set(:@config, original_config) + end + + it 'without user-defined payload' do + transformed_payload = instrumentation.config[:notification_payload_transform].call(payload) + + _(transformed_payload['active_storage.checksum']).must_equal 'BC3HWOZ8gHaD2PfLOgZP0w==' + _(transformed_payload['active_storage.service']).must_equal 'S3' + end + end + + describe '#transform_payload' do + it 'adds active_storage. prefix to payload' do + transformed_payload = instrumentation.send(:transform_payload, payload) + + _(transformed_payload['active_storage.checksum']).must_equal 'BC3HWOZ8gHaD2PfLOgZP0w==' + _(transformed_payload['active_storage.service']).must_equal 'S3' + end + end +end diff --git a/instrumentation/active_storage/test/opentelemetry/instrumentation/active_storage/subscription_test.rb b/instrumentation/active_storage/test/opentelemetry/instrumentation/active_storage/subscription_test.rb new file mode 100644 index 000000000..73724ddf2 --- /dev/null +++ b/instrumentation/active_storage/test/opentelemetry/instrumentation/active_storage/subscription_test.rb @@ -0,0 +1,360 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' +require 'opentelemetry-instrumentation-active_support' + +describe OpenTelemetry::Instrumentation::ActiveStorage do + let(:exporter) { EXPORTER } + let(:spans) { exporter.finished_spans } + let(:instrumentation) { OpenTelemetry::Instrumentation::ActiveStorage::Instrumentation.instance } + let(:key_png) { 'sample.png' } + let(:blob_png) do + ActiveStorage::Blob.stub(:generate_unique_secure_token, key_png) do + file = File.open("#{Dir.pwd}/test/fixtures/sample.png") + ActiveStorage::Blob.create_and_upload!( + io: file, + filename: 'sample.png', + content_type: 'image/png' + ) + end + end + let(:key_pdf) { 'sample.pdf' } + let(:blob_pdf) do + ActiveStorage::Blob.stub(:generate_unique_secure_token, key_pdf) do + file = File.open("#{Dir.pwd}/test/fixtures/sample.pdf") + ActiveStorage::Blob.create_and_upload!( + io: file, + filename: 'sample.pdf', + content_type: 'application/pdf' + ) + end + end + + before do + AppConfig.initialize_app + OpenTelemetry::Instrumentation::ActiveStorage::Railtie.unsubscribe + exporter.reset + end + + describe 'service_upload.active_storage' do + describe 'with default configuration' do + it 'generates a service_upload span' do + with_subscribe do + _(blob_png).wont_be_nil + end + + _(spans.length).must_equal(1) + span = spans.find { |s| s.name == 'service_upload.active_storage' } + + _(span).wont_be_nil + + _(span.attributes['active_storage.key']).must_be_nil + _(span.attributes['active_storage.checksum']).must_equal('9NwXsO4K/DCBz1BoXsIHiA==') + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + + describe 'with custom configuration' do + it 'with key: :include' do + with_configuration(key: :include, disallowed_notification_payload_keys: []) do + with_subscribe do + _(blob_png).wont_be_nil + end + end + + _(spans.length).must_equal(1) + span = spans.find { |s| s.name == 'service_upload.active_storage' } + + _(span).wont_be_nil + + _(span.attributes['active_storage.key']).must_equal(key_png) + _(span.attributes['active_storage.checksum']).must_equal('9NwXsO4K/DCBz1BoXsIHiA==') + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + end + + describe 'service_streaming_download.active_storage' do + describe 'with default configuration' do + it 'generates a service_streaming_download span' do + with_subscribe do + blob_png.download { |chunk| _(chunk).must_be(:present?) } + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_streaming_download.active_storage' } + + _(span).wont_be_nil + + _(span.attributes['active_storage.key']).must_be_nil + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + + describe 'with custom configuration' do + it 'with key: :include' do + with_configuration(key: :include, disallowed_notification_payload_keys: []) do + with_subscribe do + blob_png.download { |chunk| _(chunk).must_be(:present?) } + end + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_streaming_download.active_storage' } + + _(span).wont_be_nil + + _(span.attributes['active_storage.key']).must_equal(key_png) + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + end + + describe 'service_download_chunk.active_storage' do + describe 'with default configuration' do + it 'generates a service_download_chunk span' do + with_subscribe do + return unless blob_png.respond_to?(:download_chunk) + + blob_png.download_chunk(0..1024) + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_download_chunk.active_storage' } + + _(span).wont_be_nil + + _(span.attributes['active_storage.key']).must_be_nil + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + + describe 'with custom configuration' do + it 'with key: :include' do + with_configuration(key: :include, disallowed_notification_payload_keys: []) do + with_subscribe do + return unless blob_png.respond_to?(:download_chunk) + + blob_png.download_chunk(0..1024) + end + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_download_chunk.active_storage' } + + _(span).wont_be_nil + + _(span.attributes['active_storage.key']).must_equal(key_png) + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + end + + describe 'service_download.active_storage' do + describe 'with default configuration' do + it 'generates a service_download span' do + with_subscribe do + _(blob_png.download).must_be(:present?) + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_download.active_storage' } + + _(span).wont_be_nil + + _(span.attributes['active_storage.key']).must_be_nil + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + + describe 'with custom configuration' do + it 'with key: :include' do + with_configuration(key: :include, disallowed_notification_payload_keys: []) do + with_subscribe do + _(blob_png.download).must_be(:present?) + end + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_download.active_storage' } + + _(span).wont_be_nil + + _(span.attributes['active_storage.key']).must_equal(key_png) + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + end + + describe 'service_delete.active_storage' do + describe 'with default configuration' do + it 'generates a service_delete span' do + with_subscribe do + blob_pdf.delete + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_delete.active_storage' } + + _(span).wont_be_nil + _(span.attributes['active_storage.key']).must_be_nil + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + + describe 'with custom configuration' do + it 'with key: :include' do + with_configuration(key: :include, disallowed_notification_payload_keys: []) do + with_subscribe do + blob_pdf.delete + end + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_delete.active_storage' } + + _(span).wont_be_nil + _(span.attributes['active_storage.key']).must_equal(key_pdf) + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + end + + describe 'service_delete_prefixed.active_storage' do + it 'generates a service_delete_prefixed span' do + with_subscribe do + ActiveStorage::Blob.service.delete_prefixed('sample') + end + + _(spans.length).must_equal(1) + span = spans.find { |s| s.name == 'service_delete_prefixed.active_storage' } + + _(span).wont_be_nil + + _(span.attributes['active_storage.prefix']).must_equal('sample') + _(span.attributes['active_storage.service']).must_equal('Disk') + end + end + + describe 'service_exist.active_storage' do + describe 'with default configuration' do + it 'generates a service_exist span' do + with_subscribe do + _(ActiveStorage::Blob.service).wont_be(:exist?, 'key') + end + + _(spans.length).must_equal(1) + span = spans.find { |s| s.name == 'service_exist.active_storage' } + + _(span).wont_be_nil + _(span.attributes['active_storage.key']).must_be_nil + _(span.attributes['active_storage.service']).must_equal('Disk') + _(span.attributes['active_storage.exist']).must_equal(false) + end + end + + describe 'with custom configuration' do + it 'with key: :include' do + with_configuration(key: :include, disallowed_notification_payload_keys: []) do + with_subscribe do + _(ActiveStorage::Blob.service).wont_be(:exist?, 'key') + end + end + + _(spans.length).must_equal(1) + span = spans.find { |s| s.name == 'service_exist.active_storage' } + + _(span).wont_be_nil + _(span.attributes['active_storage.key']).must_equal('key') + _(span.attributes['active_storage.service']).must_equal('Disk') + _(span.attributes['active_storage.exist']).must_equal(false) + end + end + end + + describe 'service_url.active_storage' do + describe 'with default configuration' do + it 'generates a service_url span' do + with_subscribe do + _(blob_png.url).must_be(:present?) + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_url.active_storage' } + + _(span).wont_be_nil + _(span.attributes['active_storage.key']).must_be_nil + _(span.attributes['active_storage.service']).must_equal('Disk') + _(span.attributes['active_storage.url']).must_be_nil + end + end + + describe 'with custom configuration' do + it 'with key: :include' do + with_configuration(key: :include, url: :include, disallowed_notification_payload_keys: []) do + with_subscribe do + _(blob_png.url).must_be(:present?) + end + end + + _(spans.length).must_equal(2) + span = spans.find { |s| s.name == 'service_url.active_storage' } + + _(span).wont_be_nil + _(span.attributes['active_storage.key']).must_equal(key_png) + _(span.attributes['active_storage.service']).must_equal('Disk') + _(span.attributes['active_storage.url']).must_match(%r{^http://example\.com/rails/active_storage/disk/.*/sample.png}) + end + end + end + + describe 'preview.active_storage' do + it 'generates a preview span' do + with_subscribe do + _(blob_pdf.preview(resize_to_limit: [50, 50]).processed).must_be(:present?) + end + + _(spans.length >= 4).must_equal(true) + span = spans.find { |s| s.name == 'preview.active_storage' } + + _(span).wont_be_nil + end + end + + describe 'transform.active_storage' do + it 'generates a transform span' do + with_subscribe do + _(blob_png.variant(resize_to_limit: [50, 50]).processed).must_be(:present?) + end + + _(spans.length).must_equal(5) + span = spans.find { |s| s.name == 'transform.active_storage' } + + _(span).wont_be_nil + end + end + + # NOTE: The test for service_update_metadata.active_storage is skipped because this event is only for GCS service. + # https://github.com/rails/rails/blob/fa9cf269191c5077de1abdd1e3f934fbeaf2a5d0/guides/source/active_support_instrumentation.md?plain=1#L928 + + def with_configuration(values, &block) + original_config = instrumentation.instance_variable_get(:@config) + modified_config = original_config.merge(values) + instrumentation.instance_variable_set(:@config, modified_config) + + yield + ensure + instrumentation.instance_variable_set(:@config, original_config) + end + + def with_subscribe(&block) + OpenTelemetry::Instrumentation::ActiveStorage::Railtie.subscribe + yield + ensure + OpenTelemetry::Instrumentation::ActiveStorage::Railtie.unsubscribe + end +end diff --git a/instrumentation/active_storage/test/test_helper.rb b/instrumentation/active_storage/test/test_helper.rb new file mode 100644 index 000000000..4f2f48b57 --- /dev/null +++ b/instrumentation/active_storage/test/test_helper.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +ENV['RAILS_ENV'] = 'test' + +require 'bundler/setup' +Bundler.require(:default, :development, :test) + +require 'active_storage' +require 'minitest/autorun' +require 'test_helpers/app_config' + +# 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::ActiveStorage' + c.add_span_processor span_processor +end diff --git a/instrumentation/active_storage/test/test_helpers/app_config.rb b/instrumentation/active_storage/test/test_helpers/app_config.rb new file mode 100644 index 000000000..354731134 --- /dev/null +++ b/instrumentation/active_storage/test/test_helpers/app_config.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'active_storage/engine' + +class TestApp < Rails::Application + initializer :activestorage do + ActiveStorage::Engine.config.active_storage.service_configurations = { + test: { + service: 'Disk', + root: Dir.mktmpdir('active_storage_tests') + } + } + end + + # Override to avoid reading config/database.yml + def config.database_configuration + { + test: { + adapter: 'sqlite3', + database: ':memory:' + } + } + end +end + +require_relative 'test_previewer' + +module AppConfig + extend self + + def initialize_app + new_app = TestApp.new + new_app.config.secret_key_base = 'secret_key_base' + + # Ensure we don't see this Rails warning when testing + new_app.config.eager_load = false + new_app.config.active_support.to_time_preserves_timezone = :zone + + # Prevent tests from creating log/*.log + level = ENV.fetch('OTEL_LOG_LEVEL', 'fatal').to_sym + new_app.config.logger = Logger.new($stderr, level: level) + new_app.config.log_level = level + + new_app.config.filter_parameters = [:param_to_be_filtered] + + new_app.config.hosts << 'example.org' + + new_app.config.active_storage.service = :test + new_app.config.active_storage.previewers = [TestPreviewer] + + apply_rails_7_configs(new_app) if /^7|8\./.match?(Rails.version) + + new_app.initialize! + + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define do + create_table :active_storage_blobs, force: true do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false + t.index [:key], unique: true + end + + create_table :active_storage_attachments, force: true do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false + t.references :blob, null: false + + t.datetime :created_at, null: false + t.index %i[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', unique: true + end + end + + case Rails.version + when /^6\.1/ + ActiveStorage::Current.host = 'http://example.com' + when /^7|8\./ + ActiveStorage::Current.url_options = { host: 'http://example.com' } + end + + new_app + end + + private + + def apply_rails_7_configs(application) + # Required in Rails 7 + # Unfreeze values which may have been frozen on previous initializations. + ActiveSupport::Dependencies.autoload_paths = + ActiveSupport::Dependencies.autoload_paths.dup + ActiveSupport::Dependencies.autoload_once_paths = + ActiveSupport::Dependencies.autoload_once_paths.dup + + application.routes.draw do + scope ActiveStorage.routes_prefix do + get '/disk/:encoded_key/*filename' => 'active_storage/disk#show', as: :rails_disk_service + end + end + end +end diff --git a/instrumentation/active_storage/test/test_helpers/test_previewer.rb b/instrumentation/active_storage/test/test_helpers/test_previewer.rb new file mode 100644 index 000000000..5097ce01d --- /dev/null +++ b/instrumentation/active_storage/test/test_helpers/test_previewer.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +# TestPreviewer does not require dependencies like MuPDF and Poppler, so when testing, you can replace +# config.active_storage.previewers with this class to test the preview hook with minimal dependencies. +class TestPreviewer < ActiveStorage::Previewer + def self.accept?(blob) + blob.content_type.start_with?('application/pdf') + end + + def preview(**options) + download_blob_to_tempfile do |input| + draw_sample_image input do |_output| + file = File.open("#{Dir.pwd}/test/fixtures/sample.png") + yield io: file, filename: 'sample.png', content_type: 'image/png', **options + end + end + end + + private + + def draw_sample_image(file, &block) + draw 'echo', '"test previewer called"', &block + end +end diff --git a/instrumentation/all/Gemfile b/instrumentation/all/Gemfile index 30808d503..89d8bceeb 100644 --- a/instrumentation/all/Gemfile +++ b/instrumentation/all/Gemfile @@ -8,6 +8,7 @@ source 'https://rubygems.org' gemspec +gem 'opentelemetry-instrumentation-active_storage', path: '../active_storage' group :test do Dir.entries('../../helpers') .select { |entry| File.directory?(File.join('../../helpers', entry)) } diff --git a/instrumentation/all/lib/opentelemetry/instrumentation/all.rb b/instrumentation/all/lib/opentelemetry/instrumentation/all.rb index b53195678..dce4710c7 100644 --- a/instrumentation/all/lib/opentelemetry/instrumentation/all.rb +++ b/instrumentation/all/lib/opentelemetry/instrumentation/all.rb @@ -4,6 +4,7 @@ # # SPDX-License-Identifier: Apache-2.0 +require 'opentelemetry-instrumentation-active_storage' require 'opentelemetry-instrumentation-gruf' require 'opentelemetry-instrumentation-trilogy' require 'opentelemetry-instrumentation-active_support' diff --git a/instrumentation/all/opentelemetry-instrumentation-all.gemspec b/instrumentation/all/opentelemetry-instrumentation-all.gemspec index 2f60d7f6b..f953f47fa 100644 --- a/instrumentation/all/opentelemetry-instrumentation-all.gemspec +++ b/instrumentation/all/opentelemetry-instrumentation-all.gemspec @@ -26,6 +26,7 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.required_ruby_version = '>= 3.0' + spec.add_dependency 'opentelemetry-instrumentation-active_storage', '~> 0.0.0' spec.add_dependency 'opentelemetry-instrumentation-active_model_serializers', '~> 0.21.0' spec.add_dependency 'opentelemetry-instrumentation-aws_lambda', '~> 0.1.0' spec.add_dependency 'opentelemetry-instrumentation-aws_sdk', '~> 0.7.0'