├── .changesets └── .gitkeep ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── chore.md └── workflows │ ├── agent_release.yml │ ├── ci.yml │ └── create_release_from_tag.yml ├── .gitignore ├── .gitmodules ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .yardopts ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── SUPPORT.md ├── appsignal.gemspec ├── benchmark.rake ├── bin └── appsignal ├── build_matrix.yml ├── ext ├── Rakefile ├── agent.rb ├── appsignal_extension.c ├── base.rb └── extconf.rb ├── gemfiles ├── capistrano2.gemfile ├── capistrano3.gemfile ├── dry-monitor.gemfile ├── grape.gemfile ├── hanami-2.0.gemfile ├── hanami-2.1.gemfile ├── hanami-2.2.gemfile ├── http5.gemfile ├── no_dependencies.gemfile ├── ownership.gemfile ├── padrino.gemfile ├── psych-3.gemfile ├── psych-4.gemfile ├── que-1.gemfile ├── que-2.gemfile ├── rails-6.0.gemfile ├── rails-6.1.gemfile ├── rails-7.0.gemfile ├── rails-7.1.gemfile ├── rails-7.2.gemfile ├── rails-8.0.gemfile ├── redis-4.gemfile ├── redis-5.gemfile ├── resque-2.gemfile ├── sequel.gemfile ├── sidekiq-7.gemfile ├── sidekiq-8.gemfile ├── sinatra.gemfile ├── webmachine1.gemfile └── webmachine2.gemfile ├── lib ├── appsignal.rb ├── appsignal │ ├── auth_check.rb │ ├── capistrano.rb │ ├── check_in.rb │ ├── check_in │ │ ├── cron.rb │ │ ├── event.rb │ │ └── scheduler.rb │ ├── cli.rb │ ├── cli │ │ ├── demo.rb │ │ ├── diagnose.rb │ │ ├── diagnose │ │ │ ├── paths.rb │ │ │ └── utils.rb │ │ ├── helpers.rb │ │ └── install.rb │ ├── config.rb │ ├── custom_marker.rb │ ├── demo.rb │ ├── environment.rb │ ├── event_formatter.rb │ ├── event_formatter │ │ ├── action_view │ │ │ └── render_formatter.rb │ │ ├── active_record │ │ │ ├── instantiation_formatter.rb │ │ │ └── sql_formatter.rb │ │ ├── elastic_search │ │ │ └── search_formatter.rb │ │ ├── faraday │ │ │ └── request_formatter.rb │ │ ├── mongo_ruby_driver │ │ │ └── query_formatter.rb │ │ ├── rom │ │ │ └── sql_formatter.rb │ │ ├── sequel │ │ │ └── sql_formatter.rb │ │ └── view_component │ │ │ └── render_formatter.rb │ ├── extension.rb │ ├── extension │ │ └── jruby.rb │ ├── garbage_collection.rb │ ├── helpers │ │ ├── instrumentation.rb │ │ └── metrics.rb │ ├── hooks.rb │ ├── hooks │ │ ├── action_cable.rb │ │ ├── action_mailer.rb │ │ ├── active_job.rb │ │ ├── active_support_notifications.rb │ │ ├── at_exit.rb │ │ ├── celluloid.rb │ │ ├── data_mapper.rb │ │ ├── delayed_job.rb │ │ ├── dry_monitor.rb │ │ ├── excon.rb │ │ ├── gvl.rb │ │ ├── http.rb │ │ ├── mongo_ruby_driver.rb │ │ ├── mri.rb │ │ ├── net_http.rb │ │ ├── ownership.rb │ │ ├── passenger.rb │ │ ├── puma.rb │ │ ├── que.rb │ │ ├── rake.rb │ │ ├── redis.rb │ │ ├── redis_client.rb │ │ ├── resque.rb │ │ ├── sequel.rb │ │ ├── shoryuken.rb │ │ ├── sidekiq.rb │ │ ├── unicorn.rb │ │ └── webmachine.rb │ ├── integrations │ │ ├── action_cable.rb │ │ ├── active_support_notifications.rb │ │ ├── capistrano │ │ │ ├── appsignal.cap │ │ │ └── capistrano_2_tasks.rb │ │ ├── data_mapper.rb │ │ ├── delayed_job_plugin.rb │ │ ├── dry_monitor.rb │ │ ├── excon.rb │ │ ├── http.rb │ │ ├── mongo_ruby_driver.rb │ │ ├── net_http.rb │ │ ├── object.rb │ │ ├── ownership.rb │ │ ├── puma.rb │ │ ├── que.rb │ │ ├── railtie.rb │ │ ├── rake.rb │ │ ├── redis.rb │ │ ├── redis_client.rb │ │ ├── resque.rb │ │ ├── shoryuken.rb │ │ ├── sidekiq.rb │ │ ├── unicorn.rb │ │ └── webmachine.rb │ ├── internal_errors.rb │ ├── loaders.rb │ ├── loaders │ │ ├── grape.rb │ │ ├── hanami.rb │ │ ├── padrino.rb │ │ └── sinatra.rb │ ├── logger.rb │ ├── marker.rb │ ├── probes.rb │ ├── probes │ │ ├── gvl.rb │ │ ├── helpers.rb │ │ ├── mri.rb │ │ └── sidekiq.rb │ ├── rack.rb │ ├── rack │ │ ├── abstract_middleware.rb │ │ ├── body_wrapper.rb │ │ ├── event_handler.rb │ │ ├── grape_middleware.rb │ │ ├── hanami_middleware.rb │ │ ├── instrumentation_middleware.rb │ │ ├── rails_instrumentation.rb │ │ └── sinatra_instrumentation.rb │ ├── sample_data.rb │ ├── span.rb │ ├── system.rb │ ├── transaction.rb │ ├── transmitter.rb │ ├── utils.rb │ ├── utils │ │ ├── data.rb │ │ ├── integration_logger.rb │ │ ├── integration_memory_logger.rb │ │ ├── json.rb │ │ ├── ndjson.rb │ │ ├── query_params_sanitizer.rb │ │ ├── rails_helper.rb │ │ ├── sample_data_sanitizer.rb │ │ └── stdout_and_logger_message.rb │ └── version.rb ├── puma │ └── plugin │ │ └── appsignal.rb └── sequel │ └── extensions │ └── appsignal_integration.rb ├── mono.yml ├── resources ├── appsignal.rb.erb ├── appsignal.yml.erb └── cacert.pem ├── script ├── bundler_wrapper └── install_deps └── spec ├── .rubocop.yml ├── integration ├── runner.rb ├── runners │ ├── log │ │ └── .gitkeep │ └── stop_with_trap.rb └── stop_spec.rb ├── lib ├── appsignal │ ├── auth_check_spec.rb │ ├── capistrano2_spec.rb │ ├── capistrano3_spec.rb │ ├── check_in │ │ ├── cron_spec.rb │ │ ├── event_spec.rb │ │ ├── heartbeat_spec.rb │ │ └── scheduler_spec.rb │ ├── cli │ │ ├── demo_spec.rb │ │ ├── diagnose │ │ │ ├── paths_spec.rb │ │ │ └── utils_spec.rb │ │ ├── diagnose_spec.rb │ │ ├── helpers_spec.rb │ │ └── install_spec.rb │ ├── cli_spec.rb │ ├── config_spec.rb │ ├── custom_marker_spec.rb │ ├── demo_spec.rb │ ├── environment_spec.rb │ ├── event_formatter │ │ ├── action_view │ │ │ └── render_formatter_spec.rb │ │ ├── active_record │ │ │ ├── instantiation_formatter_spec.rb │ │ │ └── sql_formatter_spec.rb │ │ ├── elastic_search │ │ │ └── search_formatter_spec.rb │ │ ├── faraday │ │ │ └── request_formatter_spec.rb │ │ ├── mongo_ruby_driver │ │ │ └── query_formatter_spec.rb │ │ ├── rom │ │ │ └── sql_formatter_spec.rb │ │ ├── sequel │ │ │ └── sql_formatter_spec.rb │ │ └── view_component │ │ │ └── render_formatter_spec.rb │ ├── event_formatter_spec.rb │ ├── extension │ │ └── jruby_spec.rb │ ├── extension_install_failure_spec.rb │ ├── extension_spec.rb │ ├── garbage_collection_spec.rb │ ├── hooks │ │ ├── action_cable_spec.rb │ │ ├── action_mailer_spec.rb │ │ ├── active_support_notifications │ │ │ ├── finish_with_state_shared_examples.rb │ │ │ ├── instrument_shared_examples.rb │ │ │ └── start_finish_shared_examples.rb │ │ ├── active_support_notifications_spec.rb │ │ ├── activejob_spec.rb │ │ ├── at_exit_spec.rb │ │ ├── celluloid_spec.rb │ │ ├── data_mapper_spec.rb │ │ ├── delayed_job_spec.rb │ │ ├── dry_monitor_spec.rb │ │ ├── excon_spec.rb │ │ ├── gvl_spec.rb │ │ ├── http_spec.rb │ │ ├── mongo_ruby_driver_spec.rb │ │ ├── mri_spec.rb │ │ ├── net_http_spec.rb │ │ ├── ownership_spec.rb │ │ ├── passenger_spec.rb │ │ ├── puma_spec.rb │ │ ├── que_spec.rb │ │ ├── rake_spec.rb │ │ ├── redis_client_spec.rb │ │ ├── redis_spec.rb │ │ ├── resque_spec.rb │ │ ├── sequel_spec.rb │ │ ├── shoryuken_spec.rb │ │ ├── sidekiq_spec.rb │ │ ├── unicorn_spec.rb │ │ └── webmachine_spec.rb │ ├── hooks_spec.rb │ ├── integrations │ │ ├── data_mapper_spec.rb │ │ ├── delayed_job_plugin_spec.rb │ │ ├── http_spec.rb │ │ ├── mongo_ruby_driver_spec.rb │ │ ├── net_http_spec.rb │ │ ├── object_spec.rb │ │ ├── ownership_spec.rb │ │ ├── puma_spec.rb │ │ ├── que_spec.rb │ │ ├── railtie_spec.rb │ │ ├── resque_spec.rb │ │ ├── shoryuken_spec.rb │ │ ├── sidekiq_spec.rb │ │ └── webmachine_spec.rb │ ├── loaders │ │ ├── grape_spec.rb │ │ ├── hanami_spec.rb │ │ ├── padrino_spec.rb │ │ └── sinatra_spec.rb │ ├── loaders_spec.rb │ ├── logger_spec.rb │ ├── marker_spec.rb │ ├── probes │ │ ├── gvl_spec.rb │ │ ├── mri_spec.rb │ │ └── sidekiq_spec.rb │ ├── probes_spec.rb │ ├── rack │ │ ├── abstract_middleware_spec.rb │ │ ├── body_wrapper_spec.rb │ │ ├── event_handler_spec.rb │ │ ├── grape_middleware_spec.rb │ │ ├── hanami_middleware_spec.rb │ │ ├── instrumentation_middleware_spec.rb │ │ ├── rails_instrumentation_spec.rb │ │ └── sinatra_instrumentation_spec.rb │ ├── rack_spec.rb │ ├── sample_data_spec.rb │ ├── span_spec.rb │ ├── system_spec.rb │ ├── transaction_spec.rb │ ├── transmitter_spec.rb │ └── utils │ │ ├── data_spec.rb │ │ ├── integration_logger_spec.rb │ │ ├── integration_memory_logger_spec.rb │ │ ├── json_spec.rb │ │ ├── query_params_sanitizer_spec.rb │ │ └── sample_data_sanitizer_spec.rb ├── appsignal_spec.rb └── puma │ └── appsignal_spec.rb ├── spec_helper.rb └── support ├── fixtures ├── generated_config.yml ├── projects │ ├── broken │ │ └── config │ │ │ └── appsignal.yml │ ├── valid │ │ ├── config │ │ │ └── appsignal.yml │ │ └── log │ │ │ └── .gitkeep │ ├── valid_with_rails_app │ │ ├── config │ │ │ ├── application.rb │ │ │ ├── appsignal.yml │ │ │ └── environment.rb │ │ └── log │ │ │ └── .gitkeep │ └── valid_with_rails_app_with_config_rb │ │ ├── config │ │ ├── application.rb │ │ ├── appsignal.rb │ │ └── environment.rb │ │ └── log │ │ └── .gitkeep └── uploaded_file.txt ├── hanami └── hanami_app.rb ├── helpers ├── action_mailer_helpers.rb ├── activejob_helpers.rb ├── api_request_helper.rb ├── cli_helpers.rb ├── config_helpers.rb ├── dependency_helper.rb ├── directory_helper.rb ├── env_helpers.rb ├── environment_metdata_helper.rb ├── example_exception.rb ├── example_standard_error.rb ├── loader_helper.rb ├── log_helpers.rb ├── rails_helper.rb ├── std_streams_helper.rb ├── system_helpers.rb ├── take_at_most_helper.rb ├── time_helpers.rb ├── transaction_helpers.rb └── wait_for_helper.rb ├── matchers ├── contains_log.rb ├── have_colorized_text.rb └── transaction.rb ├── mocks ├── appsignal_mock.rb ├── dummy_app.rb ├── fake_gc_profiler.rb ├── fake_gvl_tools.rb ├── hash_like.rb ├── mock_probe.rb └── puma_mock.rb ├── shared_examples └── instrument.rb ├── stubs ├── appsignal │ └── loaders │ │ └── loader_stub.rb ├── delayed_job.rb └── sidekiq │ └── api.rb └── testing.rb /.changesets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appsignal/appsignal-ruby/ef8021b965d486bf65931f261a9d0828ebb6b03d/.changesets/.gitkeep -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the project. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | If your bug is about how AppSignal works in your app specifically we recommend you contact us at support@appsignal.com with your bug report instead. 11 | 12 | --- 13 | 14 | ## Describe the bug 15 | 16 | A clear and concise description of what the bug is including the code used and the exception backtrace, if any. 17 | 18 | ## To Reproduce 19 | 20 | Steps to reproduce the behavior: 21 | 22 | - Using AppSignal for Ruby gem version 2.x.x 23 | - In my app using framework/library/gem ... version x.x.x 24 | - With this code: 25 | ``` 26 | Optional: Any additional AppSignal instrumentation that was added. 27 | ``` 28 | - In this code: 29 | ``` 30 | Your app code in which the bug occurs. 31 | ``` 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Chore 3 | about: Create an issue for a task that needs to be performed. 4 | title: '' 5 | labels: chore 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## TODO 11 | 12 | - [ ] Tasks that need to be performed 13 | - [ ] In order 14 | - [ ] To complete the chore 15 | -------------------------------------------------------------------------------- /.github/workflows/agent_release.yml: -------------------------------------------------------------------------------- 1 | name: "Update agent release files" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version" 8 | required: true 9 | type: string 10 | user: 11 | description: "User who triggered the release" 12 | required: true 13 | type: string 14 | files: 15 | description: "Files to update" 16 | required: true 17 | type: string 18 | 19 | jobs: 20 | update: 21 | name: "Update" 22 | uses: "appsignal/integrations-shared/.github/workflows/agent_release.yml@main" 23 | with: 24 | version: "${{inputs.version}}" 25 | user: "${{inputs.user}}" 26 | files: "${{inputs.files}}" 27 | secrets: 28 | PUBLISH_INTEGRATION_DEPLOY_KEY: "${{secrets.AGENT_UPDATE_DEPLOY_KEY}}" 29 | PUBLISH_GIT_SIGN_KEY: "${{secrets.PUBLISH_GIT_SIGN_KEY}}" 30 | PUBLISH_GIT_SIGN_PUBLIC_KEY: "${{secrets.PUBLISH_GIT_SIGN_PUBLIC_KEY}}" 31 | PUBLISH_AGENT_INTEGRATIONS_RELEASE_PAT: "${{secrets.PUBLISH_AGENT_INTEGRATIONS_RELEASE_PAT}}" 32 | -------------------------------------------------------------------------------- /.github/workflows/create_release_from_tag.yml: -------------------------------------------------------------------------------- 1 | name: "Create release from tag" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v**" 7 | 8 | permissions: 9 | contents: write 10 | actions: write 11 | 12 | jobs: 13 | release: 14 | name: "Create release" 15 | runs-on: ubuntu-latest 16 | env: 17 | PACKAGE_NAME: "Ruby gem" 18 | CHANGELOG_CATEGORY: "Ruby" 19 | CHANGELOG_LINK: "https://github.com/appsignal/appsignal-ruby/blob/main/CHANGELOG.md" 20 | steps: 21 | - name: Checkout repository at tag 22 | uses: actions/checkout@v4 23 | with: 24 | ref: "${{ github.ref }}" 25 | 26 | - name: Get tag name 27 | run: | 28 | echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 29 | 30 | - name: Get changelog contents from tag 31 | run: | 32 | # Use sed to remove everything after "-----BEGIN PGP SIGNATURE-----" if it's present 33 | # and also always remove the last line of the git show output 34 | git show --format=oneline --no-color --no-patch "${{ env.TAG_NAME }}" \ 35 | | sed '1,2d' \ 36 | | sed '$d' \ 37 | | sed '/-----BEGIN PGP SIGNATURE-----/,$d' \ 38 | > CHANGELOG_TEXT.txt 39 | 40 | echo "" >> CHANGELOG_TEXT.txt 41 | echo "" >> CHANGELOG_TEXT.txt 42 | 43 | TAG_NAME_FOR_LINK=$(echo "${{ env.TAG_NAME }}" | sed 's/^v//' | tr -d '.') 44 | echo "View the [$PACKAGE_NAME ${{ env.TAG_NAME }} changelog]($CHANGELOG_LINK#$TAG_NAME_FOR_LINK) for more information." >> CHANGELOG_TEXT.txt 45 | 46 | - name: Submit changelog entry 47 | run: | 48 | # Prepare JSON payload using jq to ensure proper escaping 49 | payload=$(jq -n \ 50 | --arg title "$PACKAGE_NAME ${{ env.TAG_NAME }}" \ 51 | --arg category "$CHANGELOG_CATEGORY" \ 52 | --arg version "$(echo "${{ env.TAG_NAME }}" | sed 's/^v//')" \ 53 | --arg changelog "$(cat CHANGELOG_TEXT.txt)" \ 54 | --arg assignee "${{ github.actor }}" \ 55 | '{ref: "main", inputs: {title: $title, category: $category, version: $version, changelog: $changelog, assignee: $assignee}}') 56 | 57 | curl -X POST \ 58 | -H "Authorization: token ${{ secrets.INTEGRATIONS_CHANGELOG_TOKEN }}" \ 59 | -H "Accept: application/vnd.github+json" \ 60 | --fail-with-body \ 61 | https://api.github.com/repos/appsignal/appsignal.com/actions/workflows/102125282/dispatches \ 62 | -d "$payload" 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbc 2 | *.sassc 3 | .sass-cache 4 | capybara-*.html 5 | *.log 6 | /doc 7 | /.bundle 8 | /vendor/bundle 9 | /tmp/* 10 | /db/*.sqlite3 11 | /public/system/* 12 | /coverage/ 13 | /spec/examples.txt 14 | /spec/tmp 15 | /spec/support/project_fixture/log/*.log 16 | /spec/integration/runners/tmp 17 | **.orig 18 | rerun.txt 19 | pickle-email-*.html 20 | *.gem 21 | Gemfile.lock 22 | gemfiles/*.lock 23 | .DS_Store 24 | .ruby-version 25 | bundle_and_spec_all_* 26 | ext/libappsignal.* 27 | ext/appsignal-agent 28 | ext/._appsignal-agent 29 | ext/appsignal.h 30 | ext/appsignal.version 31 | ext/Makefile 32 | ext/*.bundle 33 | ext/*.o 34 | ext/*.so 35 | ext/*.report 36 | ext/*.bundle.dSYM 37 | pkg/ 38 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/integration/diagnose"] 2 | path = spec/integration/diagnose 3 | url = git@github.com:appsignal/diagnose_tests.git 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --order defined 2 | --color 3 | --warnings 4 | --require spec_helper 5 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --protected 2 | --no-private 3 | --output-dir doc 4 | --markup markdown 5 | - 6 | CHANGELOG.md 7 | LICENSE 8 | README.md 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Please visit [docs.appsignal.com/appsignal/code-of-conduct.html](https://docs.appsignal.com/appsignal/code-of-conduct.html) for our Code of Conduct for this project. 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | gem "benchmark-ips" 8 | # Fix install issue for jruby on gem 3.1.8. 9 | # No java stub is published. 10 | gem "bigdecimal", "3.1.7" if RUBY_PLATFORM == "java" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2017 AppSignal 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | [Contact us][contact] and speak directly with the engineers working on 4 | AppSignal. They will help you get set up, tweak your code and make sure you get 5 | the most out of using AppSignal. You can also find our documentation at 6 | [docs.appsignal.com](https://docs.appsignal.com/). 7 | 8 | We do not recommend creating an issue on the Ruby gem's GitHub project if it 9 | concerns your private app data. During the support process we'll need the 10 | AppSignal logs and other resources that contain your app's private data, 11 | something for which the public GitHub issue tracker is not suited. 12 | 13 | Please contact us on our website [appsignal.com](https://appsignal.com/) or via 14 | email at [support@appsignal.com][contact]. 15 | 16 | [contact]: mailto:support@appsignal.com 17 | -------------------------------------------------------------------------------- /bin/appsignal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")) 5 | require "appsignal/cli" 6 | 7 | begin 8 | Appsignal::CLI.run 9 | rescue => e 10 | raise e if $DEBUG 11 | 12 | warn e.message 13 | warn e.backtrace.join("\n") 14 | exit 1 15 | end 16 | -------------------------------------------------------------------------------- /ext/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("base.rb", __dir__) 4 | 5 | def local_build? 6 | File.exist?(ext_path("appsignal-agent")) && 7 | ( 8 | File.exist?(ext_path("libappsignal.dylib")) || 9 | File.exist?(ext_path("libappsignal.so")) 10 | ) && 11 | File.exist?(ext_path("appsignal.h")) 12 | end 13 | 14 | task :default do 15 | fail_install_on_purpose_in_test! 16 | 17 | library_type = "dynamic" 18 | report["language"]["implementation"] = "jruby" 19 | report["language"]["implementation_version"] = JRUBY_VERSION 20 | report["build"]["library_type"] = library_type 21 | next unless check_architecture 22 | 23 | if local_build? 24 | report["build"]["source"] = "local" 25 | else 26 | archive = download_archive(library_type) 27 | next unless archive 28 | next unless verify_archive(archive, library_type) 29 | 30 | unarchive(archive) 31 | end 32 | 33 | # Have the extension loader raise the error if it encountes a problem 34 | ENV["_APPSIGNAL_EXTENSION_INSTALL"] = "true" 35 | # Load the extension to test if all functions can be "attached" with FFI 36 | require File.expand_path("../lib/appsignal/extension/jruby.rb", __dir__) 37 | 38 | successful_installation 39 | rescue StandardError, LoadError => e 40 | fail_installation_with_error(e) 41 | ensure 42 | create_dummy_makefile unless installation_succeeded? 43 | write_report 44 | end 45 | -------------------------------------------------------------------------------- /gemfiles/capistrano2.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "benchmark" 4 | gem "capistrano", "< 3.0" 5 | gem "net-ssh", "2.9.2" 6 | gem "ostruct" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/capistrano3.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'capistrano', '~> 3.0' 4 | gem 'i18n', '~> 1.2.0' 5 | gem 'net-ssh', '2.9.2' 6 | 7 | gemspec :path => '../' 8 | -------------------------------------------------------------------------------- /gemfiles/dry-monitor.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "dry-monitor", "~> 1.0.1" 4 | gem "ostruct" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/grape.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "grape" 4 | gem "ostruct" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/hanami-2.0.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "hanami", "~> 2.0.0" 4 | gem "hanami-controller", "~> 2.0.0" 5 | gem "hanami-router", "~> 2.0.0" 6 | gem "ostruct" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/hanami-2.1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "hanami", "~> 2.1.0" 4 | gem "hanami-controller", "~> 2.1.0" 5 | gem "hanami-router", "~> 2.1.0" 6 | gem "ostruct" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/hanami-2.2.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "hanami", "~> 2.2.0" 4 | gem "hanami-controller", "~> 2.2.0" 5 | gem "hanami-router", "~> 2.2.0" 6 | gem "ostruct" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/http5.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "http", "~> 5.0" 4 | gem "ostruct" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/no_dependencies.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby_version = Gem::Version.new(RUBY_VERSION) 4 | gem "rack", "~> 1.6" if ruby_version < Gem::Version.new("2.3.0") 5 | 6 | # Fix install issue for jruby on gem 3.1.8. 7 | # No java stub is published. 8 | gem "bigdecimal", "3.1.7" if RUBY_PLATFORM == "java" 9 | gem "ostruct" 10 | 11 | gemspec :path => "../" 12 | -------------------------------------------------------------------------------- /gemfiles/ownership.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "ownership" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/padrino.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "base64" # Ruby 3.4 requirement 4 | gem "ostruct" 5 | gem "padrino", "~> 0.15" 6 | gem "rack", "~> 2" 7 | gem "sinatra", "~> 2" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /gemfiles/psych-3.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "psych", "~> 3.3" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/psych-4.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "psych", "~> 4.0" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/que-1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "que", "~> 1.0" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/que-2.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "que", "~> 2.0" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/rails-6.0.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 6.0.0" 4 | gem "sidekiq" 5 | 6 | # Fix install issue for jruby on gem 3.1.8. 7 | # No java stub is published. 8 | gem "bigdecimal", "3.1.7" if RUBY_PLATFORM == "java" 9 | 10 | gemspec :path => "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails-6.1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "net-smtp", :require => false 4 | gem "rails", "~> 6.1.0" 5 | gem "sidekiq" 6 | 7 | # Fix install issue for jruby on gem 3.1.8. 8 | # No java stub is published. 9 | gem "bigdecimal", "3.1.7" if RUBY_PLATFORM == "java" 10 | 11 | gemspec :path => "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails-7.0.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "base64" # Ruby 3.4 requirement 4 | gem "drb" # Ruby 3.4 requirement 5 | gem "mutex_m" # Ruby 3.4 requirement 6 | gem "rails", "~> 7.0.1" 7 | gem "rake", "> 12.2" 8 | gem "sidekiq" 9 | 10 | # Fix install issue for jruby on gem 3.1.8. 11 | # No java stub is published. 12 | gem "benchmark" 13 | gem "bigdecimal", "3.1.7" if RUBY_PLATFORM == "java" 14 | gem "ostruct" 15 | 16 | gemspec :path => "../" 17 | -------------------------------------------------------------------------------- /gemfiles/rails-7.1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 7.1.0" 4 | gem "rake", "> 12.2" 5 | gem "sidekiq" 6 | 7 | if RUBY_PLATFORM == "java" 8 | # Fix install issue for jruby on gem 3.1.8. 9 | # No java stub is published. 10 | gem "bigdecimal", "3.1.7" 11 | # Fix default gem install issue 12 | gem "jar-dependencies", "0.4.1" 13 | end 14 | 15 | gem "ostruct" 16 | 17 | gemspec :path => "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails-7.2.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rails", "~> 7.2.0" 4 | gem "rake", "> 12.2" 5 | gem "sidekiq" 6 | 7 | if RUBY_PLATFORM == "java" 8 | # Fix install issue for jruby on gem 3.1.8. 9 | # No java stub is published. 10 | gem "bigdecimal", "3.1.7" 11 | # Fix default gem install issue 12 | gem "jar-dependencies", "0.4.1" 13 | end 14 | 15 | gem "ostruct" 16 | 17 | gemspec :path => "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails-8.0.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "rails", "~> 8.0.0" 5 | gem "rake" 6 | gem "sidekiq" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/redis-4.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "redis", "~> 4.0" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/redis-5.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "hiredis-client" 4 | gem "ostruct" 5 | gem "redis", "~> 5.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/resque-2.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'resque', "~> 2.0" 4 | gem 'sinatra' 5 | 6 | gemspec :path => '../' 7 | -------------------------------------------------------------------------------- /gemfiles/sequel.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "sequel" 4 | if RUBY_PLATFORM == "java" 5 | gem "jdbc-sqlite3" 6 | else 7 | gem "sqlite3" 8 | end 9 | gem "ostruct" 10 | 11 | gemspec :path => "../" 12 | -------------------------------------------------------------------------------- /gemfiles/sidekiq-7.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "rails" 5 | gem "rake" 6 | gem "sidekiq", "~> 7.0" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/sidekiq-8.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "rails" 5 | gem "rake" 6 | gem "sidekiq", "~> 8.0" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/sinatra.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "ostruct" 4 | gem "sinatra" 5 | 6 | gemspec :path => "../" 7 | -------------------------------------------------------------------------------- /gemfiles/webmachine1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "i18n", "~> 0.0" # Lock to pre 1.x version as it's not compatible 4 | gem "webmachine", "~> 1.6" 5 | gem "webrick" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/webmachine2.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "mutex_m" # Ruby 3.4 requirement 4 | gem "ostruct" 5 | gem "pstore" 6 | gem "webmachine", "~> 2.0" 7 | gem "webrick" 8 | 9 | gemspec :path => "../" 10 | -------------------------------------------------------------------------------- /lib/appsignal/auth_check.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | # Class used to perform a Push API validation / authentication check against 5 | # the AppSignal Push API. 6 | # 7 | # @example 8 | # config = Appsignal::Config.new(Dir.pwd, "production") 9 | # auth_check = Appsignal::AuthCheck.new(config) 10 | # # Valid push_api_key 11 | # auth_check.perform # => "200" 12 | # # Invalid push_api_key 13 | # auth_check.perform # => "401" 14 | # 15 | # @!attribute [r] config 16 | # @return [Appsignal::Config] config to use in the authentication request. 17 | # @api private 18 | class AuthCheck 19 | # Path used on the AppSignal Push API 20 | # https://push.appsignal.com/1/auth 21 | ACTION = "auth" 22 | 23 | attr_reader :config 24 | 25 | def initialize(config) 26 | @config = config 27 | end 28 | 29 | # Perform push api validation request and return response status code. 30 | # 31 | # @return [String] response status code. 32 | # @raise [StandardError] see {Appsignal::Transmitter#transmit}. 33 | def perform 34 | Appsignal::Transmitter.new(ACTION, config).transmit({}).code 35 | end 36 | 37 | # Perform push api validation request and return a descriptive response 38 | # tuple. 39 | # 40 | # @return [Array] response tuple. 41 | # - First value is the response status code. 42 | # - Second value is a description of the response and the exception error 43 | # message if an exception occurred. 44 | def perform_with_result 45 | status = perform 46 | result = 47 | case status 48 | when "200" 49 | "AppSignal has confirmed authorization!" 50 | when "401" 51 | "API key not valid with AppSignal..." 52 | else 53 | "Could not confirm authorization: " \ 54 | "#{status.nil? ? "nil" : status}" 55 | end 56 | [status, result] 57 | rescue => e 58 | result = "Something went wrong while trying to " \ 59 | "authenticate with AppSignal: #{e}" 60 | [nil, result] 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/appsignal/capistrano.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appsignal" 4 | require "capistrano/version" 5 | 6 | Appsignal::Environment.report_enabled("capistrano") 7 | 8 | if defined?(Capistrano::VERSION) && Gem::Version.new(Capistrano::VERSION) >= Gem::Version.new(3) 9 | # Capistrano 3+ 10 | load File.expand_path("integrations/capistrano/appsignal.cap", __dir__) 11 | else 12 | # Capistrano 2 13 | require "appsignal/integrations/capistrano/capistrano_2_tasks" 14 | end 15 | -------------------------------------------------------------------------------- /lib/appsignal/check_in/cron.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module CheckIn 5 | class Cron 6 | # @api private 7 | attr_reader :identifier, :digest 8 | 9 | def initialize(identifier:) 10 | @identifier = identifier 11 | @digest = SecureRandom.hex(8) 12 | end 13 | 14 | def start 15 | CheckIn.scheduler.schedule(event("start")) 16 | end 17 | 18 | def finish 19 | CheckIn.scheduler.schedule(event("finish")) 20 | end 21 | 22 | private 23 | 24 | def event(kind) 25 | Event.cron( 26 | :identifier => @identifier, 27 | :digest => @digest, 28 | :kind => kind 29 | ) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/appsignal/cli/diagnose/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class CLI 5 | class Diagnose 6 | class Utils 7 | def self.username_for_uid(uid) 8 | passwd_struct = Etc.getpwuid(uid) 9 | return unless passwd_struct 10 | 11 | passwd_struct.name 12 | rescue ArgumentError # rubocop:disable Lint/SuppressedException 13 | end 14 | 15 | def self.group_for_gid(gid) 16 | passwd_struct = Etc.getgrgid(gid) 17 | return unless passwd_struct 18 | 19 | passwd_struct.name 20 | rescue ArgumentError # rubocop:disable Lint/SuppressedException 21 | end 22 | 23 | def self.read_file_content(path, bytes_to_read) 24 | file_size = File.size(path) 25 | if bytes_to_read > file_size 26 | # When the file is smaller than the bytes_to_read 27 | # Read the whole file 28 | offset = 0 29 | length = file_size 30 | else 31 | # When the file is smaller than the bytes_to_read 32 | # Read the last X bytes_to_read 33 | length = bytes_to_read 34 | offset = file_size - bytes_to_read 35 | end 36 | 37 | File.binread(path, length, offset) 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/appsignal/cli/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appsignal/utils/rails_helper" 4 | 5 | module Appsignal 6 | class CLI 7 | module Helpers 8 | private 9 | 10 | COLOR_CODES = { 11 | :red => 31, 12 | :green => 32, 13 | :yellow => 33, 14 | :blue => 34, 15 | :pink => 35, 16 | :default => 0 17 | }.freeze 18 | 19 | def coloring=(value) 20 | @coloring = value 21 | end 22 | 23 | def coloring? 24 | return true unless defined?(@coloring) 25 | 26 | @coloring 27 | end 28 | 29 | def colorize(text, color) 30 | return text unless coloring? 31 | return text if Gem.win_platform? 32 | 33 | reset_color_code = COLOR_CODES.fetch(:default) 34 | color_code = COLOR_CODES.fetch(color, reset_color_code) 35 | 36 | "\e[#{color_code}m#{text}\e[#{reset_color_code}m" 37 | end 38 | 39 | def periods 40 | 3.times do 41 | print "." 42 | sleep 0.5 43 | end 44 | end 45 | 46 | def press_any_key 47 | puts 48 | print " Ready? Press any key:" 49 | stdin.getc 50 | puts 51 | puts 52 | end 53 | 54 | def ask_for_input 55 | value = stdin.gets 56 | value ? value.chomp : "" 57 | rescue Interrupt 58 | puts "\nExiting..." 59 | exit 1 60 | end 61 | 62 | def required_input(prompt) 63 | loop do 64 | print prompt 65 | value = ask_for_input 66 | return value unless value.empty? 67 | end 68 | end 69 | 70 | def yes_or_no(prompt, options = {}) 71 | loop do 72 | print prompt 73 | input = ask_for_input.strip 74 | input = options[:default] if input.empty? && options[:default] 75 | case input 76 | when "y", "Y", "yes" 77 | return true 78 | when "n", "N", "no" 79 | return false 80 | end 81 | end 82 | end 83 | 84 | def stdin 85 | $stdin 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/appsignal/custom_marker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | # Custom markers are used on AppSignal.com to indicate events in an 5 | # application, to give additional context on graph timelines. 6 | # 7 | # This helper class will send a request to the AppSignal public endpoint to 8 | # create a Custom marker for the application on AppSignal.com. 9 | # 10 | # @see https://docs.appsignal.com/api/public-endpoint/custom-markers.html 11 | # Public Endpoint API markers endpoint documentation 12 | # @see https://docs.appsignal.com/appsignal/terminology.html#markers 13 | # Terminology: Markers 14 | class CustomMarker 15 | # @param icon [String] icon to use for the marker, like an emoji. 16 | # @param message [String] name of the user that is creating the 17 | # marker. 18 | # @param created_at [Time/String] A Ruby time object or a valid ISO8601 19 | # timestamp. 20 | # @return [Boolean] 21 | def self.report( 22 | icon: nil, 23 | message: nil, 24 | created_at: nil 25 | ) 26 | new( 27 | { 28 | :icon => icon, 29 | :message => message, 30 | :created_at => created_at.respond_to?(:iso8601) ? created_at.iso8601 : created_at 31 | }.compact 32 | ).transmit 33 | end 34 | 35 | # @api private 36 | def initialize(marker_data) 37 | @marker_data = marker_data 38 | end 39 | 40 | # @api private 41 | def transmit 42 | unless Appsignal.config 43 | Appsignal.internal_logger.warn( 44 | "Did not transmit custom marker: no AppSignal config loaded" 45 | ) 46 | return false 47 | end 48 | 49 | transmitter = Transmitter.new( 50 | "#{Appsignal.config[:logging_endpoint]}/markers", 51 | Appsignal.config 52 | ) 53 | response = transmitter.transmit(@marker_data) 54 | 55 | if (200...300).include?(response.code.to_i) 56 | Appsignal.internal_logger.info("Transmitted custom marker") 57 | true 58 | else 59 | Appsignal.internal_logger.error( 60 | "Failed to transmit custom marker: #{response.code} status code" 61 | ) 62 | false 63 | end 64 | rescue => e 65 | Appsignal.internal_logger.error( 66 | "Failed to transmit custom marker: #{e.class}: #{e.message}\n" \ 67 | "#{e.backtrace}" 68 | ) 69 | false 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/appsignal/event_formatter/action_view/render_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class EventFormatter 5 | # @api private 6 | module ActionView 7 | class RenderFormatter 8 | BLANK = "" 9 | 10 | attr_reader :root_path 11 | 12 | def initialize 13 | @root_path = "#{Rails.root}/" 14 | end 15 | 16 | def format(payload) 17 | return nil unless payload[:identifier] 18 | 19 | [payload[:identifier].sub(root_path, BLANK), nil] 20 | end 21 | end 22 | end 23 | end 24 | end 25 | 26 | if defined?(Rails) 27 | Appsignal::EventFormatter.register( 28 | "render_partial.action_view", 29 | Appsignal::EventFormatter::ActionView::RenderFormatter 30 | ) 31 | Appsignal::EventFormatter.register( 32 | "render_template.action_view", 33 | Appsignal::EventFormatter::ActionView::RenderFormatter 34 | ) 35 | end 36 | -------------------------------------------------------------------------------- /lib/appsignal/event_formatter/active_record/instantiation_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class EventFormatter 5 | # @api private 6 | module ActiveRecord 7 | class InstantiationFormatter 8 | def format(payload) 9 | [payload[:class_name], nil] 10 | end 11 | end 12 | end 13 | end 14 | end 15 | 16 | Appsignal::EventFormatter.register( 17 | "instantiation.active_record", 18 | Appsignal::EventFormatter::ActiveRecord::InstantiationFormatter 19 | ) 20 | -------------------------------------------------------------------------------- /lib/appsignal/event_formatter/active_record/sql_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class EventFormatter 5 | # @api private 6 | module ActiveRecord 7 | class SqlFormatter 8 | def format(payload) 9 | [payload[:name], payload[:sql], SQL_BODY_FORMAT] 10 | end 11 | end 12 | end 13 | end 14 | end 15 | 16 | Appsignal::EventFormatter.register( 17 | "sql.active_record", 18 | Appsignal::EventFormatter::ActiveRecord::SqlFormatter 19 | ) 20 | -------------------------------------------------------------------------------- /lib/appsignal/event_formatter/elastic_search/search_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class EventFormatter 5 | # @api private 6 | module ElasticSearch 7 | class SearchFormatter 8 | def format(payload) 9 | [ 10 | "#{payload[:name]}: #{payload[:klass]}", 11 | sanitized_search(payload[:search]).inspect 12 | ] 13 | end 14 | 15 | def sanitized_search(search) 16 | return unless search.is_a?(Hash) 17 | 18 | {}.tap do |hsh| 19 | search.each do |key, val| 20 | hsh[key] = 21 | if [:index, :type].include?(key) 22 | val 23 | else 24 | Appsignal::Utils::QueryParamsSanitizer.sanitize(val) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | 34 | Appsignal::EventFormatter.register( 35 | "search.elasticsearch", 36 | Appsignal::EventFormatter::ElasticSearch::SearchFormatter 37 | ) 38 | -------------------------------------------------------------------------------- /lib/appsignal/event_formatter/faraday/request_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class EventFormatter 5 | # @api private 6 | module Faraday 7 | class RequestFormatter 8 | def format(payload) 9 | http_method = payload[:method].to_s.upcase 10 | uri = payload[:url] 11 | [ 12 | "#{http_method} #{uri.scheme}://#{uri.host}", 13 | "#{http_method} #{uri.scheme}://#{uri.host}#{uri.path}" 14 | ] 15 | end 16 | end 17 | end 18 | end 19 | end 20 | 21 | Appsignal::EventFormatter.register( 22 | "request.faraday", 23 | Appsignal::EventFormatter::Faraday::RequestFormatter 24 | ) 25 | -------------------------------------------------------------------------------- /lib/appsignal/event_formatter/rom/sql_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class EventFormatter 5 | # @api private 6 | module Rom 7 | class SqlFormatter 8 | def format(payload) 9 | ["query.#{payload[:name]}", payload[:query], SQL_BODY_FORMAT] 10 | end 11 | end 12 | end 13 | end 14 | end 15 | 16 | Appsignal::EventFormatter.register( 17 | "sql.dry", 18 | Appsignal::EventFormatter::Rom::SqlFormatter 19 | ) 20 | -------------------------------------------------------------------------------- /lib/appsignal/event_formatter/sequel/sql_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class EventFormatter 5 | # @api private 6 | module Sequel 7 | # Compatibility with the sequel-rails gem. 8 | # The sequel-rails gem adds its own ActiveSupport::Notifications events 9 | # that conflict with our own sequel instrumentor. Without this event 10 | # formatter the sequel-rails events are recorded without the SQL query 11 | # that's being executed. 12 | class SqlFormatter 13 | def format(payload) 14 | [payload[:name].to_s, payload[:sql], SQL_BODY_FORMAT] 15 | end 16 | end 17 | end 18 | end 19 | end 20 | 21 | Appsignal::EventFormatter.register( 22 | "sql.sequel", 23 | Appsignal::EventFormatter::Sequel::SqlFormatter 24 | ) 25 | -------------------------------------------------------------------------------- /lib/appsignal/event_formatter/view_component/render_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class EventFormatter 5 | # @api private 6 | module ViewComponent 7 | class RenderFormatter 8 | BLANK = "" 9 | 10 | attr_reader :root_path 11 | 12 | def initialize 13 | @root_path = "#{Rails.root}/" 14 | end 15 | 16 | def format(payload) 17 | [payload[:name], payload[:identifier].sub(@root_path, BLANK)] 18 | end 19 | end 20 | end 21 | end 22 | end 23 | 24 | if defined?(Rails) 25 | Appsignal::EventFormatter.register( 26 | "render.view_component", 27 | Appsignal::EventFormatter::ViewComponent::RenderFormatter 28 | ) 29 | Appsignal::EventFormatter.register( 30 | "!render.view_component", 31 | Appsignal::EventFormatter::ViewComponent::RenderFormatter 32 | ) 33 | end 34 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/action_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | class ActionMailerHook < Appsignal::Hooks::Hook 6 | register :action_mailer 7 | 8 | def dependencies_present? 9 | defined?(::ActionMailer) 10 | end 11 | 12 | def install 13 | ActiveSupport::Notifications 14 | .subscribe("process.action_mailer") do |_, _, _, _, payload| 15 | Appsignal.increment_counter( 16 | :action_mailer_process, 17 | 1, 18 | :mailer => payload[:mailer], :action => payload[:action] 19 | ) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/active_support_notifications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class ActiveSupportNotificationsHook < Appsignal::Hooks::Hook 7 | register :active_support_notifications 8 | 9 | def dependencies_present? 10 | defined?(::ActiveSupport::Notifications::Instrumenter) 11 | end 12 | 13 | def install 14 | ::ActiveSupport::Notifications.class_eval do 15 | def self.instrument(name, payload = {}) 16 | # Don't check the notifier if any subscriber is listening: 17 | # AppSignal is listening 18 | instrumenter.instrument(name, payload) do 19 | yield payload if block_given? 20 | end 21 | end 22 | end 23 | 24 | require "appsignal/integrations/active_support_notifications" 25 | parent_integration_module = Appsignal::Integrations::ActiveSupportNotificationsIntegration 26 | 27 | if defined?(::ActiveSupport::Notifications::Fanout::Handle) 28 | install_module( 29 | parent_integration_module::StartFinishHandlerIntegration, 30 | ::ActiveSupport::Notifications::Fanout::Handle 31 | ) 32 | else 33 | instrumenter = ::ActiveSupport::Notifications::Instrumenter 34 | 35 | if instrumenter.method_defined?(:start) && instrumenter.method_defined?(:finish) 36 | install_module(parent_integration_module::StartFinishIntegration, instrumenter) 37 | else 38 | install_module(parent_integration_module::InstrumentIntegration, instrumenter) 39 | end 40 | 41 | return unless instrumenter.method_defined?(:finish_with_state) 42 | 43 | install_module(parent_integration_module::FinishStateIntegration, instrumenter) 44 | end 45 | end 46 | 47 | def install_module(mod, instrumenter) 48 | instrumenter.send(:prepend, mod) 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/at_exit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class AtExit < Appsignal::Hooks::Hook 7 | register :at_exit 8 | 9 | def dependencies_present? 10 | true 11 | end 12 | 13 | def install 14 | Kernel.at_exit(&AtExitCallback.method(:call)) 15 | end 16 | 17 | # Stop AppSignal before the app exists. 18 | # 19 | # This is the default behavior and can be customized with the 20 | # `enable_at_exit_hook` option. 21 | # 22 | # When the `enable_at_exit_reporter` option is set to `true` (the 23 | # default), it will report any unhandled errors that will crash the Ruby 24 | # process. 25 | # 26 | # If this error was previously reported by any of our instrumentation, 27 | # the error will not also be reported here. This way we don't report an 28 | # error from a Rake task or instrumented script twice. 29 | class AtExitCallback 30 | def self.call 31 | report_error = false 32 | return unless Appsignal.config&.[](:enable_at_exit_reporter) 33 | 34 | error = $! # rubocop:disable Style/SpecialGlobalVars 35 | return unless error 36 | return if ignored_error?(error) 37 | return if Appsignal::Transaction.last_errors.include?(error) 38 | 39 | report_error = true 40 | 41 | Appsignal.report_error(error) do |transaction| 42 | transaction.set_namespace("unhandled") 43 | end 44 | ensure 45 | at_exit_hook = Appsignal.config&.[](:enable_at_exit_hook) 46 | if at_exit_hook == "always" || (at_exit_hook == "on_error" && report_error) 47 | Appsignal.stop("at_exit") 48 | end 49 | end 50 | 51 | IGNORED_ERRORS = [ 52 | # Normal exits from the application we do not need to report 53 | SystemExit, 54 | SignalException 55 | ].freeze 56 | 57 | def self.ignored_error?(error) 58 | IGNORED_ERRORS.include?(error.class) 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/celluloid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class CelluloidHook < Appsignal::Hooks::Hook 7 | register :celluloid 8 | 9 | def dependencies_present? 10 | defined?(::Celluloid) 11 | end 12 | 13 | def install 14 | # Some versions of Celluloid have race conditions while exiting 15 | # that can result in a dead lock. We stop appsignal before shutting 16 | # down Celluloid so we're sure our thread does not aggravate this situation. 17 | # This way we also make sure any outstanding transactions get flushed. 18 | 19 | Celluloid.singleton_class.send(:prepend, Module.new do 20 | def shutdown 21 | Appsignal.stop("celluloid") 22 | super 23 | end 24 | end) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/data_mapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class DataMapperHook < Appsignal::Hooks::Hook 7 | register :data_mapper 8 | 9 | def dependencies_present? 10 | defined?(::DataMapper) && 11 | defined?(::DataObjects::Connection) 12 | end 13 | 14 | def install 15 | require "appsignal/integrations/data_mapper" 16 | ::DataObjects::Connection.include Appsignal::Hooks::DataMapperLogListener 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/delayed_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class DelayedJobHook < Appsignal::Hooks::Hook 7 | register :delayed_job 8 | 9 | def dependencies_present? 10 | defined?(::Delayed::Plugin) 11 | end 12 | 13 | def install 14 | # The DJ plugin is a subclass of Delayed::Plugin, so we can only 15 | # require this code if we're actually installing. 16 | require "appsignal/integrations/delayed_job_plugin" 17 | ::Delayed::Worker.plugins << Appsignal::Integrations::DelayedJobPlugin 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/dry_monitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class DryMonitorHook < Appsignal::Hooks::Hook 7 | register :dry_monitor 8 | 9 | def dependencies_present? 10 | defined?(::Dry::Monitor::Notifications) 11 | end 12 | 13 | def install 14 | require "appsignal/integrations/dry_monitor" 15 | 16 | ::Dry::Monitor::Notifications.prepend(Appsignal::Integrations::DryMonitorIntegration) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/excon.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class ExconHook < Appsignal::Hooks::Hook 7 | register :excon 8 | 9 | def dependencies_present? 10 | Appsignal.config && defined?(::Excon) 11 | end 12 | 13 | def install 14 | require "appsignal/integrations/excon" 15 | ::Excon.defaults[:instrumentor] = Appsignal::Integrations::ExconIntegration 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/gvl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class GvlHook < Appsignal::Hooks::Hook 7 | register :gvl 8 | 9 | def dependencies_present? 10 | return false if Appsignal::System.jruby? 11 | 12 | require "gvltools" 13 | Appsignal.config && Appsignal::Probes::GvlProbe.dependencies_present? 14 | rescue LoadError 15 | false 16 | end 17 | 18 | def install 19 | Appsignal::Probes.register :gvl, Appsignal::Probes::GvlProbe 20 | ::GVLTools::GlobalTimer.enable if Appsignal.config[:enable_gvl_global_timer] 21 | ::GVLTools::WaitingThreads.enable if Appsignal.config[:enable_gvl_waiting_threads] 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class HttpHook < Appsignal::Hooks::Hook 7 | register :http_rb 8 | 9 | def dependencies_present? 10 | defined?(HTTP::Client) && Appsignal.config && Appsignal.config[:instrument_http_rb] 11 | end 12 | 13 | def install 14 | require "appsignal/integrations/http" 15 | HTTP::Client.prepend Appsignal::Integrations::HttpIntegration 16 | 17 | Appsignal::Environment.report_enabled("http_rb") 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/mongo_ruby_driver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class MongoRubyDriverHook < Appsignal::Hooks::Hook 7 | register :mongo_ruby_driver 8 | 9 | def dependencies_present? 10 | defined?(::Mongo::Monitoring::Global) 11 | end 12 | 13 | def install 14 | require "appsignal/integrations/mongo_ruby_driver" 15 | 16 | Mongo::Monitoring::Global.subscribe( 17 | Mongo::Monitoring::COMMAND, 18 | Appsignal::Hooks::MongoMonitorSubscriber.new 19 | ) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/mri.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class MriHook < Appsignal::Hooks::Hook 7 | register :mri 8 | 9 | def dependencies_present? 10 | defined?(::RubyVM) 11 | end 12 | 13 | def install 14 | Appsignal::Probes.register :mri, Appsignal::Probes::MriProbe 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/net_http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "net/http" 4 | 5 | module Appsignal 6 | class Hooks 7 | # @api private 8 | class NetHttpHook < Appsignal::Hooks::Hook 9 | register :net_http 10 | 11 | def dependencies_present? 12 | Appsignal.config && Appsignal.config[:instrument_net_http] 13 | end 14 | 15 | def install 16 | require "appsignal/integrations/net_http" 17 | Net::HTTP.prepend Appsignal::Integrations::NetHttpIntegration 18 | 19 | Appsignal::Environment.report_enabled("net_http") 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/ownership.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class OwnershipHook < Appsignal::Hooks::Hook 7 | register :ownership 8 | 9 | def dependencies_present? 10 | defined?(::Ownership) && 11 | Gem::Version.new(::Ownership::VERSION) >= Gem::Version.new("0.2.0") && 12 | Appsignal.config && 13 | Appsignal.config[:instrument_ownership] 14 | end 15 | 16 | def install 17 | require "appsignal/integrations/ownership" 18 | 19 | # If a transaction is created in a code context that has an owner, 20 | # set the namespace of the transaction to the owner. 21 | Appsignal::Transaction.after_create << 22 | Appsignal::Integrations::OwnershipIntegrationHelper.method(:after_create) 23 | 24 | # If an error was reported in a code context that has an owner, 25 | # set the namespace of the transaction to the owner. 26 | # In some circumstances, this will be more accurate than the last owner 27 | # that was set for the transaction, which is what would otherwise be 28 | # reported. 29 | Appsignal::Transaction.before_complete << 30 | Appsignal::Integrations::OwnershipIntegrationHelper.method(:before_complete) 31 | 32 | # If an owner is set in a code context that has an active transaction, 33 | # set the namespace of the transaction to the owner. 34 | unless ::Ownership.singleton_class.included_modules.include?( 35 | Appsignal::Integrations::OwnershipIntegration 36 | ) 37 | ::Ownership.singleton_class.prepend Appsignal::Integrations::OwnershipIntegration 38 | end 39 | 40 | Appsignal::Environment.report_enabled("ownership") 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/passenger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class PassengerHook < Appsignal::Hooks::Hook 7 | register :passenger 8 | 9 | def dependencies_present? 10 | defined?(::PhusionPassenger) 11 | end 12 | 13 | def install 14 | ::PhusionPassenger.on_event(:starting_worker_process) do |_forked| 15 | Appsignal.forked 16 | end 17 | 18 | ::PhusionPassenger.on_event(:stopping_worker_process) do 19 | Appsignal.stop("passenger") 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class PumaHook < Appsignal::Hooks::Hook 7 | register :puma 8 | 9 | def dependencies_present? 10 | defined?(::Puma) && 11 | Gem::Version.new(Puma::Const::VERSION) >= Gem::Version.new("3.0.0") 12 | end 13 | 14 | def install 15 | require "appsignal/integrations/puma" 16 | ::Puma::Server.prepend(Appsignal::Integrations::PumaServer) 17 | 18 | return unless defined?(::Puma::Cluster) 19 | 20 | # For clustered mode with multiple workers 21 | ::Puma::Cluster.send(:prepend, Module.new do 22 | def stop_workers 23 | Appsignal.stop("puma cluster") 24 | super 25 | end 26 | end) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/que.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class QueHook < Appsignal::Hooks::Hook 7 | register :que 8 | 9 | def dependencies_present? 10 | defined?(::Que::Job) 11 | end 12 | 13 | def install 14 | require "appsignal/integrations/que" 15 | ::Que::Job.prepend Appsignal::Integrations::QuePlugin 16 | 17 | ::Que.error_notifier = proc do |error, _job| 18 | Appsignal::Transaction.current.set_error(error) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/rake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class RakeHook < Appsignal::Hooks::Hook 7 | register :rake 8 | 9 | def dependencies_present? 10 | defined?(::Rake::Task) 11 | end 12 | 13 | def install 14 | require "appsignal/integrations/rake" 15 | ::Rake::Task.prepend Appsignal::Integrations::RakeIntegration 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/redis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class RedisHook < Appsignal::Hooks::Hook 7 | register :redis 8 | 9 | def dependencies_present? 10 | defined?(::Redis) && 11 | !defined?(::RedisClient) && 12 | Appsignal.config && 13 | Appsignal.config[:instrument_redis] 14 | end 15 | 16 | def install 17 | require "appsignal/integrations/redis" 18 | ::Redis::Client.prepend Appsignal::Integrations::RedisIntegration 19 | 20 | Appsignal::Environment.report_enabled("redis") 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/redis_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class RedisClientHook < Appsignal::Hooks::Hook 7 | register :redis_client 8 | 9 | def dependencies_present? 10 | defined?(::RedisClient) && 11 | Gem::Version.new(::RedisClient::VERSION) >= Gem::Version.new("0.14.0") && 12 | Appsignal.config && 13 | Appsignal.config[:instrument_redis] 14 | end 15 | 16 | def install 17 | require "appsignal/integrations/redis_client" 18 | ::RedisClient::RubyConnection.prepend Appsignal::Integrations::RedisClientIntegration 19 | Appsignal::Environment.report_enabled("redis") 20 | 21 | return unless defined?(::RedisClient::HiredisConnection) 22 | 23 | ::RedisClient::HiredisConnection.prepend Appsignal::Integrations::RedisClientIntegration 24 | Appsignal::Environment.report_enabled("hiredis") 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/resque.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class ResqueHook < Appsignal::Hooks::Hook 7 | register :resque 8 | 9 | def dependencies_present? 10 | defined?(::Resque) 11 | end 12 | 13 | def install 14 | require "appsignal/integrations/resque" 15 | Resque::Job.prepend Appsignal::Integrations::ResqueIntegration 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/sequel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | module SequelLogExtension 7 | # Add query instrumentation 8 | def log_yield(sql, args = nil) 9 | Appsignal.instrument( 10 | "sql.sequel", 11 | nil, 12 | sql, 13 | Appsignal::EventFormatter::SQL_BODY_FORMAT 14 | ) do 15 | super 16 | end 17 | end 18 | end 19 | 20 | module SequelLogConnectionExtension 21 | # Add query instrumentation 22 | def log_connection_yield(sql, conn, args = nil) 23 | Appsignal.instrument( 24 | "sql.sequel", 25 | nil, 26 | sql, 27 | Appsignal::EventFormatter::SQL_BODY_FORMAT 28 | ) do 29 | super 30 | end 31 | end 32 | end 33 | 34 | class SequelHook < Appsignal::Hooks::Hook 35 | register :sequel 36 | 37 | def dependencies_present? 38 | defined?(::Sequel::Database) && 39 | Appsignal.config && 40 | Appsignal.config[:instrument_sequel] 41 | end 42 | 43 | def install 44 | # Register the extension... 45 | if (::Sequel::MAJOR >= 4 && ::Sequel::MINOR >= 35) || ::Sequel::MAJOR >= 5 46 | ::Sequel::Database.register_extension( 47 | :appsignal_integration, 48 | Appsignal::Hooks::SequelLogConnectionExtension 49 | ) 50 | else 51 | ::Sequel::Database.register_extension( 52 | :appsignal_integration, 53 | Appsignal::Hooks::SequelLogExtension 54 | ) 55 | end 56 | 57 | # ... and automatically add it to future instances. 58 | ::Sequel::Database.extension(:appsignal_integration) 59 | 60 | Appsignal::Environment.report_enabled("sequel") 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/shoryuken.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class ShoryukenHook < Appsignal::Hooks::Hook 7 | register :shoryuken 8 | 9 | def dependencies_present? 10 | defined?(::Shoryuken) 11 | end 12 | 13 | def install 14 | require "appsignal/integrations/shoryuken" 15 | 16 | ::Shoryuken.configure_server do |config| 17 | config.server_middleware do |chain| 18 | chain.add Appsignal::Integrations::ShoryukenMiddleware 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/sidekiq.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | class SidekiqHook < Appsignal::Hooks::Hook 6 | register :sidekiq 7 | 8 | def self.version_5_1_or_higher? 9 | @version_5_1_or_higher ||= 10 | if dependencies_present? 11 | Gem::Version.new(::Sidekiq::VERSION) >= Gem::Version.new("5.1.0") 12 | else 13 | false 14 | end 15 | end 16 | 17 | def self.dependencies_present? 18 | defined?(::Sidekiq) 19 | end 20 | 21 | def dependencies_present? 22 | self.class.dependencies_present? 23 | end 24 | 25 | def install 26 | require "appsignal/integrations/sidekiq" 27 | Appsignal::Probes.register :sidekiq, Appsignal::Probes::SidekiqProbe 28 | 29 | ::Sidekiq.configure_server do |config| 30 | config.error_handlers << 31 | Appsignal::Integrations::SidekiqErrorHandler.new 32 | if config.respond_to? :death_handlers 33 | config.death_handlers << 34 | Appsignal::Integrations::SidekiqDeathHandler.new 35 | end 36 | 37 | config.server_middleware do |chain| 38 | if chain.respond_to? :prepend 39 | chain.prepend Appsignal::Integrations::SidekiqMiddleware 40 | else 41 | chain.add Appsignal::Integrations::SidekiqMiddleware 42 | end 43 | end 44 | end 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/unicorn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class UnicornHook < Appsignal::Hooks::Hook 7 | register :unicorn 8 | 9 | def dependencies_present? 10 | defined?(::Unicorn::HttpServer) && 11 | defined?(::Unicorn::Worker) 12 | end 13 | 14 | def install 15 | require "appsignal/integrations/unicorn" 16 | ::Unicorn::HttpServer.prepend Appsignal::Integrations::UnicornIntegration::Server 17 | ::Unicorn::Worker.prepend Appsignal::Integrations::UnicornIntegration::Worker 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/appsignal/hooks/webmachine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class WebmachineHook < Appsignal::Hooks::Hook 7 | register :webmachine 8 | 9 | def dependencies_present? 10 | defined?(::Webmachine) 11 | end 12 | 13 | def install 14 | require "appsignal/integrations/webmachine" 15 | ::Webmachine::Decision::FSM.prepend Appsignal::Integrations::WebmachineIntegration 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/action_cable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module ActionCableIntegration 7 | def perform_action(*args, &block) 8 | # The request is only the original websocket request 9 | env = connection.env 10 | request = ActionDispatch::Request.new(env) 11 | request_id = request.request_id || SecureRandom.uuid 12 | env[Appsignal::Hooks::ActionCableHook::REQUEST_ID] ||= request_id 13 | 14 | transaction = Appsignal::Transaction.create(Appsignal::Transaction::ACTION_CABLE) 15 | 16 | begin 17 | super 18 | rescue Exception => exception # rubocop:disable Lint/RescueException 19 | transaction.set_error(exception) 20 | raise exception 21 | ensure 22 | transaction.set_action_if_nil("#{self.class}##{args.first["action"]}") 23 | transaction.add_params_if_nil(args.first) 24 | transaction.add_session_data { request.session.to_h if request.respond_to? :session } 25 | transaction.set_metadata("path", request.path) 26 | transaction.set_metadata("method", "websocket") 27 | transaction.add_tags(:request_id => request_id) if request_id 28 | Appsignal::Transaction.complete_current! 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/capistrano/appsignal.cap: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Capistrano 3 integration 4 | namespace :appsignal do 5 | task :deploy do 6 | appsignal_env = fetch(:appsignal_env, 7 | fetch(:stage, fetch(:rails_env, fetch(:rack_env, "production")))) 8 | user = fetch(:appsignal_user, ENV["USER"] || ENV.fetch("USERNAME", nil)) 9 | revision = fetch(:appsignal_revision, fetch(:current_revision)) 10 | 11 | Appsignal._load_config!(appsignal_env) do |config| 12 | config&.merge_dsl_options(fetch(:appsignal_config, {})) 13 | end 14 | Appsignal._start_logger 15 | 16 | if Appsignal.config&.active? 17 | marker_data = { 18 | :revision => revision, 19 | :user => user 20 | } 21 | 22 | marker = Appsignal::Marker.new(marker_data, Appsignal.config) 23 | # {#dry_run?} helper was added in Capistrano 3.5.0 24 | # https://github.com/capistrano/capistrano/commit/38d8d6d2c8485f1b5643857465b16ff01da57aff 25 | if respond_to?(:dry_run?) && dry_run? 26 | puts "Dry run: AppSignal deploy marker not actually sent." 27 | else 28 | marker.transmit 29 | end 30 | else 31 | puts "Not notifying of deploy, config is not active for environment: #{appsignal_env}" 32 | end 33 | end 34 | end 35 | 36 | after "deploy:finished", "appsignal:deploy" 37 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/capistrano/capistrano_2_tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | class Capistrano 7 | def self.tasks(config) 8 | config.load do 9 | after "deploy", "appsignal:deploy" 10 | after "deploy:migrations", "appsignal:deploy" 11 | 12 | namespace :appsignal do 13 | task :deploy do 14 | appsignal_env = fetch(:appsignal_env, 15 | fetch(:stage, fetch(:rails_env, fetch(:rack_env, "production")))) 16 | user = fetch(:appsignal_user, ENV["USER"] || ENV.fetch("USERNAME", nil)) 17 | revision = fetch(:appsignal_revision, fetch(:current_revision)) 18 | 19 | Appsignal._load_config!(appsignal_env) do |conf| 20 | conf&.merge_dsl_options(fetch(:appsignal_config, {})) 21 | end 22 | Appsignal._start_logger 23 | 24 | if Appsignal.config&.active? 25 | marker_data = { 26 | :revision => revision, 27 | :user => user 28 | } 29 | 30 | marker = Marker.new(marker_data, Appsignal.config) 31 | if config.dry_run 32 | puts "Dry run: AppSignal deploy marker not actually sent." 33 | else 34 | marker.transmit 35 | end 36 | else 37 | puts "Not notifying of deploy, config is not active for " \ 38 | "environment: #{appsignal_env}" 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | 48 | if ::Capistrano::Configuration.instance 49 | Appsignal::Integrations::Capistrano.tasks(::Capistrano::Configuration.instance) 50 | end 51 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/data_mapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | module DataMapperLogListener 7 | SQL_CLASSES = [ 8 | "DataObjects::SqlServer::Connection", 9 | "DataObjects::Sqlite3::Connection", 10 | "DataObjects::Mysql::Connection", 11 | "DataObjects::Postgres::Connection" 12 | ].freeze 13 | 14 | def log(message) 15 | # If scheme is SQL-like, try to sanitize it, otherwise clear the body 16 | if SQL_CLASSES.include?(self.class.to_s) 17 | body_content = message.query 18 | body_format = Appsignal::EventFormatter::SQL_BODY_FORMAT 19 | else 20 | body_content = "" 21 | body_format = Appsignal::EventFormatter::DEFAULT 22 | end 23 | 24 | # Record event 25 | Appsignal::Transaction.current.record_event( 26 | "query.data_mapper", 27 | "DataMapper Query", 28 | body_content, 29 | message.duration, 30 | body_format 31 | ) 32 | super 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/dry_monitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module DryMonitorIntegration 7 | def instrument(event_id, payload = {}, &block) 8 | Appsignal::Transaction.current.start_event 9 | 10 | super 11 | ensure 12 | title, body, body_format = Appsignal::EventFormatter.format("#{event_id}.dry", payload) 13 | 14 | Appsignal::Transaction.current.finish_event( 15 | title || event_id.to_s, 16 | title, 17 | body, 18 | body_format 19 | ) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/excon.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module ExconIntegration 7 | def self.instrument(name, data, &block) 8 | namespace, *event = name.split(".") 9 | rails_name = [event, namespace].flatten.join(".") 10 | 11 | title = 12 | if rails_name == "response.excon" 13 | data[:host] 14 | else 15 | "#{data[:method].to_s.upcase} #{data[:scheme]}://#{data[:host]}" 16 | end 17 | Appsignal.instrument(rails_name, title, &block) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module HttpIntegration 7 | def request(verb, uri, opts = {}) 8 | uri_module = defined?(HTTP::URI) ? HTTP::URI : URI 9 | parsed_request_uri = uri.is_a?(URI) ? uri : uri_module.parse(uri.to_s) 10 | request_uri = "#{parsed_request_uri.scheme}://#{parsed_request_uri.host}" 11 | 12 | Appsignal.instrument("request.http_rb", "#{verb.upcase} #{request_uri}") do 13 | super 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/mongo_ruby_driver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | class Hooks 5 | # @api private 6 | class MongoMonitorSubscriber 7 | # Called by Mongo::Monitor when query starts 8 | def started(event) 9 | return unless Appsignal::Transaction.current? 10 | 11 | transaction = Appsignal::Transaction.current 12 | return if transaction.paused? 13 | 14 | # Format the command 15 | command = Appsignal::EventFormatter::MongoRubyDriver::QueryFormatter 16 | .format(event.command_name, event.command) 17 | 18 | # Store the query on the transaction, we need it when the event finishes 19 | store = transaction.store("mongo_driver") 20 | store[event.request_id] = command 21 | 22 | # Start this event 23 | transaction.start_event 24 | end 25 | 26 | # Called by Mongo::Monitor when query succeeds 27 | def succeeded(event) 28 | # Finish the event as succeeded 29 | finish("SUCCEEDED", event) 30 | end 31 | 32 | # Called by Mongo::Monitor when query fails 33 | def failed(event) 34 | # Finish the event as failed 35 | finish("FAILED", event) 36 | end 37 | 38 | # Finishes the event in the AppSignal extension 39 | def finish(result, event) 40 | return unless Appsignal::Transaction.current? 41 | 42 | transaction = Appsignal::Transaction.current 43 | return if transaction.paused? 44 | 45 | # Get the query from the transaction store 46 | store = transaction.store("mongo_driver") 47 | command = store.delete(event.request_id) || {} 48 | 49 | # Finish the event in the extension. 50 | transaction.finish_event( 51 | "query.mongodb", 52 | "#{event.command_name} | #{event.database_name} | #{result}", 53 | Appsignal::Utils::Data.generate(command), 54 | Appsignal::EventFormatter::DEFAULT 55 | ) 56 | 57 | # Send global query metrics 58 | Appsignal.add_distribution_value( 59 | "mongodb_query_duration", 60 | event.duration, 61 | :database => event.database_name 62 | ) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/net_http.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module NetHttpIntegration 7 | def request(request, body = nil, &block) 8 | Appsignal.instrument( 9 | "request.net_http", 10 | "#{request.method} #{use_ssl? ? "https" : "http"}://#{request["host"] || address}" 11 | ) do 12 | super 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/object.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Appsignal::Environment.report_enabled("object_instrumentation") if defined?(Appsignal) 4 | 5 | class Object 6 | # @see https://docs.appsignal.com/ruby/instrumentation/method-instrumentation.html 7 | # Method instrumentation documentation. 8 | def self.appsignal_instrument_class_method(method_name, options = {}) 9 | singleton_class.send \ 10 | :alias_method, "appsignal_uninstrumented_#{method_name}", method_name 11 | singleton_class.send(:define_method, method_name) do |*args, &block| 12 | name = options.fetch(:name) do 13 | "#{method_name}.class_method.#{appsignal_reverse_class_name}.other" 14 | end 15 | Appsignal.instrument name do 16 | send "appsignal_uninstrumented_#{method_name}", *args, &block 17 | end 18 | end 19 | 20 | if singleton_class.respond_to?(:ruby2_keywords, true) # rubocop:disable Style/GuardClause 21 | singleton_class.send(:ruby2_keywords, method_name) 22 | end 23 | end 24 | 25 | # @see https://docs.appsignal.com/ruby/instrumentation/method-instrumentation.html 26 | # Method instrumentation documentation. 27 | def self.appsignal_instrument_method(method_name, options = {}) 28 | alias_method "appsignal_uninstrumented_#{method_name}", method_name 29 | define_method method_name do |*args, &block| 30 | name = options.fetch(:name) do 31 | "#{method_name}.#{appsignal_reverse_class_name}.other" 32 | end 33 | Appsignal.instrument name do 34 | send "appsignal_uninstrumented_#{method_name}", *args, &block 35 | end 36 | end 37 | ruby2_keywords method_name if respond_to?(:ruby2_keywords, true) 38 | end 39 | 40 | # @api private 41 | def self.appsignal_reverse_class_name 42 | return "AnonymousClass" unless name 43 | 44 | name.split("::").reverse.join(".") 45 | end 46 | 47 | # @api private 48 | def appsignal_reverse_class_name 49 | self.class.appsignal_reverse_class_name 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/ownership.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module OwnershipIntegration 7 | # Implement the `around_change` logic by monkey-patching the reader, 8 | # instead of by using the `around_change=` writer. This allows customers 9 | # to use the `around_change=` writer in their own code without 10 | # accidentally overriding AppSignal's instrumentation. 11 | def around_change 12 | proc do |owner, block| 13 | OwnershipIntegrationHelper.set(Appsignal::Transaction.current, owner) 14 | 15 | original = super 16 | 17 | if original 18 | original.call(owner, block) 19 | else 20 | block.call 21 | end 22 | end 23 | end 24 | end 25 | 26 | module OwnershipIntegrationHelper 27 | class << self 28 | def set(transaction, owner) 29 | return if owner.nil? 30 | 31 | transaction.add_tags(:owner => owner) 32 | transaction.set_namespace(owner) if set_namespace? 33 | end 34 | 35 | def after_create(transaction) 36 | set(transaction, ::Ownership.owner) 37 | end 38 | 39 | def before_complete(transaction, error) 40 | set(transaction, error.owner) if error.respond_to?(:owner) 41 | end 42 | 43 | private 44 | 45 | def set_namespace? 46 | Appsignal.config && Appsignal.config[:ownership_set_namespace] 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module PumaServer 7 | def lowlevel_error(error, env, response_status = 500) 8 | response = 9 | if method(:lowlevel_error).super_method.arity.abs == 3 # Puma >= 5 10 | super 11 | else # Puma <= 4 12 | super(error, env) 13 | end 14 | 15 | unless PumaServerHelper.ignored_error?(error) 16 | Appsignal.report_error(error) do |transaction| 17 | Appsignal::Rack::ApplyRackRequest 18 | .new(::Rack::Request.new(env)) 19 | .apply_to(transaction) 20 | transaction.add_tags( 21 | :reported_by => :puma_lowlevel_error, 22 | :response_status => response_status 23 | ) 24 | end 25 | end 26 | 27 | response 28 | end 29 | end 30 | 31 | module PumaServerHelper 32 | IGNORED_ERRORS = [ 33 | # Ignore internal Puma Client IO errors 34 | # https://github.com/puma/puma/blob/9ee922d28e1fffd02c1d5480a9e13376f92f46a3/lib/puma/server.rb#L536-L544 35 | "Puma::MiniSSL::SSLError", 36 | "Puma::HttpParserError", 37 | "Puma::HttpParserError501" 38 | ].freeze 39 | 40 | def self.ignored_error?(error) 41 | IGNORED_ERRORS.include?(error.class.to_s) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/que.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module QuePlugin 7 | def _run(*args) 8 | transaction = 9 | Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB) 10 | 11 | begin 12 | Appsignal.instrument("perform_job.que") { super } 13 | rescue Exception => error # rubocop:disable Lint/RescueException 14 | transaction.set_error(error) 15 | raise error 16 | ensure 17 | local_attrs = respond_to?(:que_attrs) ? que_attrs : attrs 18 | transaction.set_action_if_nil("#{local_attrs[:job_class]}#run") 19 | transaction.add_params_if_nil do 20 | { 21 | :arguments => local_attrs[:args] 22 | }.tap do |hash| 23 | hash[:keyword_arguments] = local_attrs[:kwargs] if local_attrs.key?(:kwargs) 24 | end 25 | end 26 | transaction.add_tags( 27 | "id" => local_attrs[:job_id] || local_attrs[:id], 28 | "queue" => local_attrs[:queue], 29 | "run_at" => local_attrs[:run_at].to_s, 30 | "priority" => local_attrs[:priority], 31 | "attempts" => local_attrs[:error_count].to_i 32 | ) 33 | Appsignal::Transaction.complete_current! 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/rake.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module RakeIntegration 7 | IGNORED_ERRORS = [ 8 | # Normal exits from the application we do not need to report 9 | SystemExit, 10 | SignalException 11 | ].freeze 12 | 13 | def self.ignored_error?(error) 14 | IGNORED_ERRORS.include?(error.class) 15 | end 16 | 17 | def execute(*args) 18 | transaction = 19 | if Appsignal.config[:enable_rake_performance_instrumentation] 20 | Appsignal::Integrations::RakeIntegrationHelper.register_at_exit_hook 21 | _appsignal_create_transaction 22 | end 23 | 24 | Appsignal.instrument "task.rake" do 25 | super 26 | end 27 | rescue Exception => error # rubocop:disable Lint/RescueException 28 | Appsignal::Integrations::RakeIntegrationHelper.register_at_exit_hook 29 | unless RakeIntegration.ignored_error?(error) 30 | transaction ||= _appsignal_create_transaction 31 | transaction.set_error(error) 32 | end 33 | raise error 34 | ensure 35 | if transaction 36 | # Format given arguments and cast to hash if possible 37 | params, _ = args 38 | params = params.to_hash if params.respond_to?(:to_hash) 39 | transaction.set_action(name) 40 | transaction.add_params_if_nil(params) 41 | transaction.complete 42 | end 43 | end 44 | 45 | private 46 | 47 | def _appsignal_create_transaction 48 | Appsignal::Transaction.create("rake") 49 | end 50 | end 51 | 52 | # @api private 53 | module RakeIntegrationHelper 54 | # Register an `at_exit` hook when a task is executed. This will stop 55 | # AppSignal when _all_ tasks are executed and Rake exits. 56 | def self.register_at_exit_hook 57 | return if @register_at_exit_hook 58 | 59 | Kernel.at_exit(&method(:at_exit_hook)) 60 | 61 | @register_at_exit_hook = true 62 | end 63 | 64 | # The at_exit hook itself 65 | def self.at_exit_hook 66 | Appsignal.stop("rake") 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/redis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module RedisIntegration 7 | def write(command) 8 | sanitized_command = 9 | if command[0] == :eval 10 | "#{command[1]}#{" ?" * (command.size - 3)}" 11 | else 12 | "#{command[0]}#{" ?" * (command.size - 1)}" 13 | end 14 | 15 | Appsignal.instrument "query.redis", id, sanitized_command do 16 | super 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/redis_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module RedisClientIntegration 7 | def write(command) 8 | sanitized_command = 9 | if command[0] == :eval 10 | "#{command[1]}#{" ?" * (command.size - 3)}" 11 | else 12 | "#{command[0]}#{" ?" * (command.size - 1)}" 13 | end 14 | 15 | Appsignal.instrument "query.redis", @config.id, sanitized_command do 16 | super 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/resque.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module ResqueIntegration 7 | def perform 8 | transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB) 9 | 10 | Appsignal.instrument "perform.resque" do 11 | super 12 | end 13 | rescue Exception => exception # rubocop:disable Lint/RescueException 14 | transaction.set_error(exception) 15 | raise exception 16 | ensure 17 | if transaction 18 | transaction.set_action_if_nil("#{payload["class"]}#perform") 19 | transaction.add_params_if_nil { ResqueHelpers.arguments(payload) } 20 | transaction.add_tags("queue" => queue) 21 | 22 | Appsignal::Transaction.complete_current! 23 | end 24 | Appsignal.stop("resque") 25 | end 26 | end 27 | 28 | # @api private 29 | class ResqueHelpers 30 | def self.arguments(payload) 31 | case payload["class"] 32 | when "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper" 33 | nil # Set in the ActiveJob integration 34 | else 35 | payload["args"] 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/unicorn.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module UnicornIntegration 7 | # Make sure that appsignal is started and the last transaction 8 | # in a worker gets flushed. 9 | # 10 | # We'd love to be able to hook this into Unicorn in a less 11 | # intrusive way, but this is the best we can do given the 12 | # options we have. 13 | 14 | module Server 15 | def worker_loop(worker) 16 | Appsignal.forked 17 | super 18 | end 19 | end 20 | 21 | module Worker 22 | def close 23 | Appsignal.stop("unicorn") 24 | super 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/appsignal/integrations/webmachine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Integrations 5 | # @api private 6 | module WebmachineIntegration 7 | def run 8 | has_parent_transaction = Appsignal::Transaction.current? 9 | transaction = 10 | if has_parent_transaction 11 | Appsignal::Transaction.current 12 | else 13 | Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST) 14 | end 15 | transaction.add_params_if_nil { request.query } 16 | transaction.add_headers_if_nil { request.headers if request.respond_to?(:headers) } 17 | 18 | Appsignal.instrument("process_action.webmachine") do 19 | super 20 | end 21 | ensure 22 | transaction.set_action_if_nil("#{resource.class.name}##{request.method}") 23 | 24 | Appsignal::Transaction.complete_current! unless has_parent_transaction 25 | end 26 | 27 | private 28 | 29 | def handle_exceptions 30 | super do 31 | yield 32 | rescue Exception => e # rubocop:disable Lint/RescueException 33 | Appsignal.set_error(e) 34 | raise e 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/appsignal/internal_errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | # @api private 5 | class InternalError < StandardError; end 6 | 7 | # @api private 8 | class NotStartedError < InternalError 9 | MESSAGE = <<~MESSAGE 10 | The AppSignal Ruby gem was not started! 11 | 12 | This error was raised by calling `Appsignal.check_if_started!` 13 | MESSAGE 14 | 15 | def message 16 | MESSAGE 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/appsignal/loaders.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | # @api private 5 | module Loaders 6 | class << self 7 | def loaders 8 | @loaders ||= {} 9 | end 10 | 11 | def instances 12 | @instances ||= {} 13 | end 14 | 15 | def register(name, klass) 16 | loaders[name.to_sym] = klass 17 | end 18 | 19 | def registered?(name) 20 | loaders.key?(name) 21 | end 22 | 23 | def unregister(name) 24 | loaders.delete(name) 25 | end 26 | 27 | def load(name_str) 28 | name = name_str.to_sym 29 | 30 | unless registered?(name) 31 | require_loader(name) 32 | unless registered?(name) 33 | Appsignal.internal_logger 34 | .warn("No loader found with the name '#{name}'.") 35 | return 36 | end 37 | end 38 | 39 | Appsignal.internal_logger.debug("Loading '#{name}' loader") 40 | 41 | begin 42 | loader_klass = loaders[name] 43 | loader = loader_klass.new 44 | instances[name] = loader 45 | loader.on_load if loader.respond_to?(:on_load) 46 | rescue => e 47 | Appsignal.internal_logger.error( 48 | "An error occurred while loading the '#{name}' loader: " \ 49 | "#{e.class}: #{e.message}\n#{e.backtrace}" 50 | ) 51 | end 52 | end 53 | 54 | def start 55 | instances.each do |name, instance| 56 | Appsignal.internal_logger.debug("Starting '#{name}' loader") 57 | begin 58 | instance.on_start if instance.respond_to?(:on_start) 59 | rescue => e 60 | Appsignal.internal_logger.error( 61 | "An error occurred while starting the '#{name}' loader: " \ 62 | "#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}" 63 | ) 64 | end 65 | end 66 | end 67 | 68 | private 69 | 70 | def require_loader(name) 71 | require "appsignal/loaders/#{name}" 72 | rescue LoadError 73 | nil 74 | end 75 | end 76 | 77 | class Loader 78 | class << self 79 | attr_reader :loader_name 80 | 81 | def register(name) 82 | @loader_name = name 83 | Loaders.register(name, self) 84 | end 85 | end 86 | 87 | def register_config_defaults(options) 88 | Appsignal::Config.add_loader_defaults(self.class.loader_name, **options) 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/appsignal/loaders/grape.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Loaders 5 | class GrapeLoader < Loader 6 | register :grape 7 | 8 | def on_load 9 | require "appsignal/rack/grape_middleware" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/appsignal/loaders/hanami.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Loaders 5 | class HanamiLoader < Loader 6 | register :hanami 7 | 8 | def on_load 9 | hanami_app_config = ::Hanami.app.config 10 | register_config_defaults( 11 | :root_path => hanami_app_config.root.to_s, 12 | :env => hanami_app_config.env, 13 | :ignore_errors => [ 14 | "Hanami::Router::NotAllowedError", 15 | "Hanami::Router::NotFoundError" 16 | ] 17 | ) 18 | end 19 | 20 | def on_start 21 | require "appsignal/rack/hanami_middleware" 22 | 23 | hanami_app_config = ::Hanami.app.config 24 | hanami_app_config.middleware.use( 25 | ::Rack::Events, 26 | [Appsignal::Rack::EventHandler.new] 27 | ) 28 | hanami_app_config.middleware.use(Appsignal::Rack::HanamiMiddleware) 29 | 30 | return unless Gem::Version.new(Hanami::VERSION) < Gem::Version.new("2.2.0") 31 | 32 | ::Hanami::Action.prepend Appsignal::Loaders::HanamiLoader::HanamiIntegration 33 | end 34 | 35 | # Legacy instrumentation to set the action name in Hanami apps older than Hanami 2.2 36 | module HanamiIntegration 37 | def call(env) 38 | super 39 | ensure 40 | transaction = env[::Appsignal::Rack::APPSIGNAL_TRANSACTION] 41 | 42 | transaction&.set_action_if_nil(self.class.name) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/appsignal/loaders/sinatra.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Loaders 5 | class SinatraLoader < Loader 6 | register :sinatra 7 | 8 | def on_load 9 | app_settings = ::Sinatra::Application.settings 10 | register_config_defaults( 11 | :root_path => app_settings.root, 12 | :env => app_settings.environment 13 | ) 14 | end 15 | 16 | def on_start 17 | require "appsignal/rack/sinatra_instrumentation" 18 | 19 | ::Sinatra::Base.use(::Rack::Events, [Appsignal::Rack::EventHandler.new]) 20 | ::Sinatra::Base.use(Appsignal::Rack::SinatraBaseInstrumentation) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/appsignal/marker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | # Deploy markers are used on AppSignal.com to indicate changes in an 5 | # application, "Deploy markers" indicate a deploy of an application. 6 | # 7 | # Incidents for exceptions and performance issues will be closed and 8 | # reopened if they occur again in the new deploy. 9 | # 10 | # This class will help send a request to the AppSignal Push API to create a 11 | # Deploy marker for the application on AppSignal.com. 12 | # 13 | # @!attribute [r] marker_data 14 | # @return [Hash] marker data to send. 15 | # 16 | # @!attribute [r] config 17 | # @return [Appsignal::Config] config to use in the authentication request. 18 | # Set config does not override data set in {#marker_data}. 19 | # 20 | # @see Appsignal::CLI::NotifyOfDeploy 21 | # @see https://docs.appsignal.com/appsignal/terminology.html#markers 22 | # Terminology: Deploy marker 23 | # @api private 24 | class Marker 25 | # Path used on the AppSignal Push API 26 | # https://push.appsignal.com/1/markers 27 | ACTION = "markers" 28 | 29 | attr_reader :marker_data, :config 30 | 31 | # @param marker_data [Hash] see {#marker_data} 32 | # @option marker_data :environment [String] environment to load 33 | # configuration for. 34 | # @option marker_data :name [String] name of the application. 35 | # @option marker_data :user [String] name of the user that is creating the 36 | # marker. 37 | # @option marker_data :revision [String] the revision that has been 38 | # deployed. E.g. a git commit SHA. 39 | # @param config [Appsignal::Config] 40 | def initialize(marker_data, config) 41 | @marker_data = marker_data 42 | @config = config 43 | end 44 | 45 | # Send a request to create the marker. 46 | # 47 | # Prints output to STDOUT. 48 | # 49 | # @return [void] 50 | def transmit 51 | transmitter = Transmitter.new(ACTION, config) 52 | puts "Notifying AppSignal of '#{config.env}' deploy with " \ 53 | "revision: #{marker_data[:revision]}, user: #{marker_data[:user]}" 54 | 55 | response = transmitter.transmit(marker_data) 56 | raise "#{response.code} at #{transmitter.uri}" unless response.code == "200" 57 | 58 | puts "AppSignal has been notified of this deploy!" 59 | rescue => e 60 | puts "Something went wrong while trying to notify AppSignal: #{e}" 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/appsignal/probes/gvl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Probes 5 | class GvlProbe 6 | include Helpers 7 | 8 | # @api private 9 | def self.dependencies_present? 10 | defined?(::GVLTools) && gvltools_0_2_or_newer? && ruby_3_2_or_newer? && 11 | !Appsignal::System.jruby? 12 | end 13 | 14 | # @api private 15 | def self.gvltools_0_2_or_newer? 16 | Gem::Version.new(::GVLTools::VERSION) >= Gem::Version.new("0.2.0") 17 | end 18 | 19 | # @api private 20 | def self.ruby_3_2_or_newer? 21 | Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2.0") 22 | end 23 | 24 | def initialize(appsignal: Appsignal, gvl_tools: ::GVLTools) 25 | Appsignal.internal_logger.debug("Initializing GVL probe") 26 | @appsignal = appsignal 27 | @gvl_tools = gvl_tools 28 | 29 | # Store the process name and ID at initialization time 30 | # to avoid picking up changes to the process name at runtime 31 | @process_name = File.basename($PROGRAM_NAME).split.first || "[unknown process]" 32 | @process_id = Process.pid 33 | end 34 | 35 | def call 36 | probe_global_timer 37 | probe_waiting_threads if @gvl_tools::WaitingThreads.enabled? 38 | end 39 | 40 | private 41 | 42 | def probe_global_timer 43 | monotonic_time_ns = @gvl_tools::GlobalTimer.monotonic_time 44 | gauge_delta :gvl_global_timer, monotonic_time_ns do |time_delta_ns| 45 | if time_delta_ns > 0 46 | time_delta_ms = time_delta_ns / 1_000_000 47 | set_gauges_with_hostname_and_process( 48 | "gvl_global_timer", 49 | time_delta_ms 50 | ) 51 | end 52 | end 53 | end 54 | 55 | def probe_waiting_threads 56 | set_gauges_with_hostname_and_process( 57 | "gvl_waiting_threads", 58 | @gvl_tools::WaitingThreads.count 59 | ) 60 | end 61 | 62 | def set_gauges_with_hostname_and_process(name, value) 63 | set_gauge_with_hostname(name, value, { 64 | :process_name => @process_name, 65 | :process_id => @process_id 66 | }) 67 | 68 | # Also set the gauge without the process name and ID for 69 | # compatibility with existing automated dashboards 70 | set_gauge_with_hostname(name, value) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/appsignal/probes/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Probes 5 | # @api private 6 | module Helpers 7 | private 8 | 9 | def gauge_delta_cache 10 | @gauge_delta_cache ||= {} 11 | end 12 | 13 | # Calculate the delta of two values for a gauge metric. 14 | # 15 | # When this method is called, the given value is stored in a cache 16 | # under the given cache key. 17 | # 18 | # A block must be passed to this method. The first time the method 19 | # is called for a given cache key, the block will not be yielded to. 20 | # In subsequent calls, the delta between the previously stored value 21 | # in the cache for that key and the value given in this invocation 22 | # will be yielded to the block. 23 | # 24 | # This is used for absolute counter values which we want to track as 25 | # gauges. 26 | # 27 | # @example 28 | # gauge_delta :with_block, 10 do |delta| 29 | # puts "this block will not be yielded to" 30 | # end 31 | # gauge_delta :with_block, 15 do |delta| 32 | # # `delta` has a value of `5` 33 | # puts "this block will be yielded to with delta = #{delta}" 34 | # end 35 | # 36 | def gauge_delta(cache_key, value) 37 | previous_value = gauge_delta_cache[cache_key] 38 | gauge_delta_cache[cache_key] = value 39 | return unless previous_value 40 | 41 | yield value - previous_value 42 | end 43 | 44 | def hostname 45 | return @hostname if defined?(@hostname) 46 | 47 | config = @appsignal.config 48 | # Auto detect hostname as fallback. May be inaccurate. 49 | @hostname = 50 | config[:hostname] || Socket.gethostname 51 | Appsignal.internal_logger.debug "Probe helper: Using hostname config " \ 52 | "option '#{@hostname.inspect}' as hostname" 53 | 54 | @hostname 55 | end 56 | 57 | def set_gauge_with_hostname(metric, value, tags = {}) 58 | @appsignal.set_gauge(metric, value, { :hostname => hostname }.merge(tags)) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/appsignal/rack/grape_middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Rack 5 | # @api public 6 | class GrapeMiddleware < Appsignal::Rack::AbstractMiddleware 7 | # @api private 8 | def initialize(app, options = {}) 9 | options[:instrument_event_name] = "process_request.grape" 10 | options[:report_errors] = lambda { |env| !env["grape.skip_appsignal_error"] } 11 | super 12 | end 13 | 14 | private 15 | 16 | def add_transaction_metadata_after(transaction, request) 17 | endpoint = request.env["api.endpoint"] 18 | unless endpoint&.options 19 | super 20 | return 21 | end 22 | 23 | options = endpoint.options 24 | request_method = options[:method].first.to_s.upcase 25 | klass = options[:for] 26 | namespace = endpoint.namespace 27 | namespace = "" if namespace == "/" 28 | 29 | path = options[:path].first.to_s 30 | path = "/#{path}" if path[0] != "/" 31 | path = "#{namespace}#{path}" 32 | 33 | transaction.set_action_if_nil("#{request_method}::#{klass}##{path}") 34 | 35 | super 36 | 37 | transaction.set_metadata("path", path) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/appsignal/rack/hanami_middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Rack 5 | # @api private 6 | class HanamiMiddleware < AbstractMiddleware 7 | def initialize(app, options = {}) 8 | options[:params_method] = nil 9 | options[:instrument_event_name] ||= "process_action.hanami" 10 | super 11 | end 12 | 13 | private 14 | 15 | HANAMI_ACTION_INSTANCE = "hanami.action_instance" 16 | ROUTER_PARAMS = "router.params" 17 | 18 | def add_transaction_metadata_after(transaction, request) 19 | action_name = fetch_hanami_action(request.env) 20 | transaction.set_action_if_nil(action_name) if action_name 21 | transaction.add_params { params_for(request) } 22 | end 23 | 24 | def params_for(request) 25 | request.env.fetch(ROUTER_PARAMS, nil) 26 | end 27 | 28 | def fetch_hanami_action(env) 29 | # This env key is available in Hanami 2.2+ 30 | action_instance = env.fetch(HANAMI_ACTION_INSTANCE, nil) 31 | return unless action_instance 32 | 33 | action_instance.class.name 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/appsignal/rack/rails_instrumentation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Rack 5 | # @api private 6 | class RailsInstrumentation < Appsignal::Rack::AbstractMiddleware 7 | def initialize(app, options = {}) 8 | options[:request_class] ||= ActionDispatch::Request 9 | options[:params_method] ||= :filtered_parameters 10 | options[:instrument_event_name] = nil 11 | options[:report_errors] = true 12 | super 13 | end 14 | 15 | private 16 | 17 | def add_transaction_metadata_after(transaction, request) 18 | controller = request.env["action_controller.instance"] 19 | transaction.set_action_if_nil("#{controller.class}##{controller.action_name}") if controller 20 | 21 | request_id = request.env["action_dispatch.request_id"] 22 | transaction.add_tags(:request_id => request_id) if request_id 23 | 24 | super 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/appsignal/rack/sinatra_instrumentation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Rack 5 | # Stub old middleware. Prevents Sinatra middleware being loaded twice. 6 | # This can happen when users use the old method of including 7 | # `use Appsignal::Rack::SinatraInstrumentation` in their modular Sinatra 8 | # applications. This is no longer needed. Instead Appsignal now includes 9 | # `use Appsignal::Rack::SinatraBaseInstrumentation` automatically. 10 | # 11 | # @api private 12 | class SinatraInstrumentation 13 | def initialize(app, options = {}) 14 | @app = app 15 | @options = options 16 | Appsignal.internal_logger.warn "Please remove Appsignal::Rack::SinatraInstrumentation " \ 17 | "from your Sinatra::Base class. This is no longer needed." 18 | end 19 | 20 | def call(env) 21 | @app.call(env) 22 | end 23 | 24 | def settings 25 | @app.settings 26 | end 27 | end 28 | 29 | class SinatraBaseInstrumentation < AbstractMiddleware 30 | attr_reader :raise_errors_on 31 | 32 | def initialize(app, options = {}) 33 | options[:request_class] ||= Sinatra::Request 34 | options[:params_method] ||= :params 35 | options[:instrument_event_name] ||= "process_action.sinatra" 36 | super 37 | @raise_errors_on = raise_errors?(app) 38 | end 39 | 40 | private 41 | 42 | def add_transaction_metadata_after(transaction, request) 43 | env = request.env 44 | transaction.set_action_if_nil(action_name(env)) 45 | # If raise_error is off versions of Sinatra don't raise errors, but store 46 | # them in the sinatra.error env var. 47 | if !raise_errors_on && env["sinatra.error"] && !env["sinatra.skip_appsignal_error"] 48 | transaction.set_error(env["sinatra.error"]) 49 | end 50 | 51 | super 52 | end 53 | 54 | def action_name(env) 55 | return unless env["sinatra.route"] 56 | 57 | if env["SCRIPT_NAME"] 58 | method, route = env["sinatra.route"].split 59 | "#{method} #{env["SCRIPT_NAME"]}#{route}" 60 | else 61 | env["sinatra.route"] 62 | end 63 | end 64 | 65 | def raise_errors?(app) 66 | app.respond_to?(:settings) && 67 | app.settings.respond_to?(:raise_errors) && 68 | app.settings.raise_errors 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/appsignal/span.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | # @api private 5 | class Span 6 | def initialize(namespace = nil, ext = nil) 7 | @ext = ext || Appsignal::Extension::Span.root(namespace || "") 8 | end 9 | 10 | def child 11 | Span.new(nil, @ext.child) 12 | end 13 | 14 | def name=(value) 15 | @ext.set_name(value) 16 | end 17 | 18 | def add_error(error) 19 | unless error.is_a?(Exception) 20 | Appsignal.internal_logger.error "Appsignal::Span#add_error: Cannot " \ 21 | "add error. The given value is not an exception: #{error.inspect}" 22 | return 23 | end 24 | return unless error 25 | 26 | backtrace = cleaned_backtrace(error.backtrace) 27 | @ext.add_error( 28 | error.class.name, 29 | error.message.to_s, 30 | backtrace ? Appsignal::Utils::Data.generate(backtrace) : Appsignal::Extension.data_array_new 31 | ) 32 | end 33 | 34 | def set_sample_data(key, data) 35 | return unless key && data && (data.is_a?(Array) || data.is_a?(Hash)) 36 | 37 | @ext.set_sample_data( 38 | key.to_s, 39 | Appsignal::Utils::Data.generate(data) 40 | ) 41 | end 42 | 43 | def []=(key, value) 44 | case value 45 | when String 46 | @ext.set_attribute_string(key.to_s, value) 47 | when Integer 48 | begin 49 | @ext.set_attribute_int(key.to_s, value) 50 | rescue RangeError 51 | @ext.set_attribute_string(key.to_s, "bigint:#{value}") 52 | end 53 | when TrueClass, FalseClass 54 | @ext.set_attribute_bool(key.to_s, value) 55 | when Float 56 | @ext.set_attribute_double(key.to_s, value) 57 | else 58 | raise TypeError, "value needs to be a string, int, bool or float" 59 | end 60 | end 61 | 62 | def to_h 63 | json = @ext.to_json 64 | return unless json 65 | 66 | JSON.parse(json) 67 | end 68 | 69 | def instrument 70 | yield self 71 | ensure 72 | close 73 | end 74 | 75 | def close 76 | @ext.close 77 | end 78 | 79 | def closed? 80 | to_h.nil? 81 | end 82 | 83 | private 84 | 85 | def cleaned_backtrace(backtrace) 86 | if defined?(::Rails) && backtrace 87 | ::Rails.backtrace_cleaner.clean(backtrace, nil) 88 | else 89 | backtrace 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/appsignal/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | # @api private 5 | module Utils 6 | end 7 | end 8 | 9 | require "appsignal/utils/integration_memory_logger" 10 | require "appsignal/utils/stdout_and_logger_message" 11 | require "appsignal/utils/data" 12 | require "appsignal/utils/sample_data_sanitizer" 13 | require "appsignal/utils/integration_logger" 14 | require "appsignal/utils/json" 15 | require "appsignal/utils/ndjson" 16 | require "appsignal/utils/query_params_sanitizer" 17 | -------------------------------------------------------------------------------- /lib/appsignal/utils/integration_logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Utils 5 | class IntegrationLogger < ::Logger 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/appsignal/utils/integration_memory_logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "logger" 4 | 5 | module Appsignal 6 | module Utils 7 | class IntegrationMemoryLogger 8 | LEVELS = { 9 | Logger::DEBUG => :DEBUG, 10 | Logger::INFO => :INFO, 11 | Logger::WARN => :WARN, 12 | Logger::ERROR => :ERROR, 13 | Logger::FATAL => :FATAL, 14 | Logger::UNKNOWN => :UNKNOWN 15 | }.freeze 16 | 17 | attr_accessor :formatter, :level 18 | 19 | def add(severity, message, _progname = nil) 20 | message = formatter.call(severity, Time.now, nil, message) if formatter 21 | messages[severity] << message 22 | end 23 | alias log add 24 | 25 | def debug(message) 26 | add(:DEBUG, message) 27 | end 28 | 29 | def info(message) 30 | add(:INFO, message) 31 | end 32 | 33 | def warn(message) 34 | add(:WARN, message) 35 | end 36 | 37 | def error(message) 38 | add(:ERROR, message) 39 | end 40 | 41 | def fatal(message) 42 | add(:FATAL, message) 43 | end 44 | 45 | def unknown(message) 46 | add(:UNKNOWN, message) 47 | end 48 | 49 | def clear 50 | messages.clear 51 | end 52 | 53 | def messages 54 | @messages ||= Hash.new { |hash, key| hash[key] = [] } 55 | end 56 | 57 | def messages_for_level(level) 58 | levels = LEVELS.select { |log_level| log_level >= level }.values 59 | messages 60 | .select { |log_level| levels.include?(log_level) } 61 | .flat_map { |_level, message| message } 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/appsignal/utils/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Utils 5 | class JSON 6 | class << self 7 | def generate(body) 8 | ::JSON.generate(jsonify(body)) 9 | end 10 | 11 | private 12 | 13 | def jsonify(value) 14 | case value 15 | when String 16 | encode_utf8(value) 17 | when Numeric, NilClass, TrueClass, FalseClass 18 | value 19 | when Hash 20 | value.each_with_object({}) do |(k, v), hash| 21 | hash[jsonify(k)] = jsonify(v) 22 | end 23 | when Array 24 | value.map { |v| jsonify(v) } 25 | else 26 | jsonify(value.to_s) 27 | end 28 | end 29 | 30 | def encode_utf8(value) 31 | value.encode( 32 | "utf-8", 33 | :invalid => :replace, 34 | :undef => :replace 35 | ) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/appsignal/utils/ndjson.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Utils 5 | class NDJSON 6 | class << self 7 | def generate(body) 8 | body.map do |element| 9 | Appsignal::Utils::JSON.generate(element) 10 | end.join("\n") 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/appsignal/utils/query_params_sanitizer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Utils 5 | class QueryParamsSanitizer 6 | REPLACEMENT_KEY = "?" 7 | 8 | module ClassMethods 9 | def sanitize(params, only_top_level = false, key_sanitizer = nil) 10 | case params 11 | when Hash 12 | sanitize_hash params, only_top_level, key_sanitizer 13 | when Array 14 | sanitize_array params, only_top_level, key_sanitizer 15 | else 16 | REPLACEMENT_KEY 17 | end 18 | end 19 | 20 | private 21 | 22 | def sanitize_hash(hash, only_top_level, key_sanitizer) 23 | {}.tap do |h| 24 | hash.each do |key, value| 25 | h[sanitize_key(key, key_sanitizer)] = 26 | if only_top_level 27 | REPLACEMENT_KEY 28 | else 29 | sanitize(value, only_top_level, key_sanitizer) 30 | end 31 | end 32 | end 33 | end 34 | 35 | def sanitize_array(array, only_top_level, key_sanitizer) 36 | if only_top_level 37 | [sanitize(array[0], only_top_level, key_sanitizer)] 38 | else 39 | array.map do |value| 40 | sanitize(value, only_top_level, key_sanitizer) 41 | end.uniq 42 | end 43 | end 44 | 45 | def sanitize_key(key, sanitizer) 46 | case sanitizer 47 | when :mongodb then key.to_s.gsub(/(\..+)/, ".#{REPLACEMENT_KEY}") 48 | else key 49 | end 50 | end 51 | end 52 | 53 | extend ClassMethods 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/appsignal/utils/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Utils 5 | module RailsHelper 6 | def self.detected_rails_app_name 7 | rails_class = Rails.application.class 8 | if rails_class.respond_to? :module_parent_name # Rails 6 9 | rails_class.module_parent_name 10 | else # Older Rails versions 11 | rails_class.parent_name 12 | end 13 | end 14 | 15 | def self.application_config_path 16 | File.expand_path(File.join(Dir.pwd, "config/application.rb")) 17 | end 18 | 19 | def self.environment_config_path 20 | File.expand_path(File.join(Dir.pwd, "config/environment.rb")) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/appsignal/utils/stdout_and_logger_message.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | module Utils 5 | module StdoutAndLoggerMessage 6 | def self.warning(message, logger = Appsignal.internal_logger) 7 | Kernel.warn "appsignal WARNING: #{message}" 8 | logger.warn message 9 | end 10 | 11 | def stdout_and_logger_warning(message, logger = Appsignal.internal_logger) 12 | Appsignal::Utils::StdoutAndLoggerMessage.warning(message, logger) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/appsignal/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appsignal 4 | VERSION = "4.5.15" 5 | end 6 | -------------------------------------------------------------------------------- /lib/sequel/extensions/appsignal_integration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This is just a placeholder file, as +Sequel.extensions+ forcefully loads it 4 | # instead of assuming we already did. If you're looking for the integration 5 | # implementation, you can find it in +lib/appsignal/instrumentations/sequel.rb+ 6 | -------------------------------------------------------------------------------- /mono.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | repo: "https://github.com/appsignal/appsignal-ruby" 4 | bootstrap: 5 | post: 6 | - "rake extension:install" 7 | clean: 8 | post: 9 | - "bundle exec rake extension:clean" 10 | - "rm -rf pkg" 11 | build: 12 | command: "bundle exec rake build:all" 13 | publish: 14 | gem_files_dir: pkg/ 15 | test: 16 | command: "bundle exec rake test" 17 | -------------------------------------------------------------------------------- /resources/appsignal.rb.erb: -------------------------------------------------------------------------------- 1 | # AppSignal Ruby gem configuration 2 | # Visit our documentation for a list of all available configuration options. 3 | # https://docs.appsignal.com/ruby/configuration/options.html 4 | Appsignal.configure do |config| 5 | config.activate_if_environment(<%= environments.map(&:inspect).join(", ") %>) 6 | config.name = <%= app_name.inspect %> 7 | # The application's Push API key 8 | # We recommend removing this line and setting this option with the 9 | # APPSIGNAL_PUSH_API_KEY environment variable instead. 10 | # https://docs.appsignal.com/ruby/configuration/options.html#option-push_api_key 11 | config.push_api_key = "<%= push_api_key %>" 12 | 13 | # Configure actions that should not be monitored by AppSignal. 14 | # For more information see our docs: 15 | # https://docs.appsignal.com/ruby/configuration/ignore-actions.html 16 | # config.ignore_actions << "ApplicationController#isup" 17 | 18 | # Configure errors that should not be recorded by AppSignal. 19 | # For more information see our docs: 20 | # https://docs.appsignal.com/ruby/configuration/ignore-errors.html 21 | # config.ignore_errors << "MyCustomError" 22 | end 23 | -------------------------------------------------------------------------------- /resources/appsignal.yml.erb: -------------------------------------------------------------------------------- 1 | default: &defaults 2 | # Your push api key, it is possible to set this dynamically using ERB: 3 | # push_api_key: "<%%= ENV['APPSIGNAL_PUSH_API_KEY'] %>" 4 | push_api_key: "<%= push_api_key %>" 5 | 6 | # Your app's name 7 | name: "<%= app_name %>" 8 | 9 | # Actions that should not be monitored by AppSignal 10 | # ignore_actions: 11 | # - ApplicationController#isup 12 | 13 | # Errors that should not be recorded by AppSignal 14 | # For more information see our docs: 15 | # https://docs.appsignal.com/ruby/configuration/ignore-errors.html 16 | # ignore_errors: 17 | # - Exception 18 | # - NoMemoryError 19 | # - ScriptError 20 | # - LoadError 21 | # - NotImplementedError 22 | # - SyntaxError 23 | # - SecurityError 24 | # - SignalException 25 | # - Interrupt 26 | # - SystemExit 27 | # - SystemStackError 28 | 29 | # See https://docs.appsignal.com/ruby/configuration/options.html for 30 | # all configuration options. 31 | 32 | # Configuration per environment, leave out an environment or set active 33 | # to false to not push metrics for that environment. 34 | <% environments.each do |environment| -%> 35 | <%= environment %>: 36 | <<: *defaults 37 | active: true 38 | <%= "\n" unless environment == environments.last -%> 39 | <% end -%> 40 | -------------------------------------------------------------------------------- /script/bundler_wrapper: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | case "${_BUNDLER_VERSION-"latest"}" in 6 | "latest") 7 | bundle $@ 8 | ;; 9 | *) 10 | bundle _${_BUNDLER_VERSION}_ $@ 11 | ;; 12 | esac 13 | -------------------------------------------------------------------------------- /script/install_deps: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | gem_args="--no-verbose --no-document" 6 | 7 | case "${_RUBYGEMS_VERSION-"latest"}" in 8 | "latest") 9 | echo "Updating rubygems" 10 | retry --times 5 --sleep 5 gem update $gem_args --system 11 | ;; 12 | *) 13 | echo "Updating rubygems to $_RUBYGEMS_VERSION}" 14 | retry --times 5 --sleep 5 gem update $gem_args --system $_RUBYGEMS_VERSION 15 | ;; 16 | esac 17 | 18 | case "${_BUNDLER_VERSION-"latest"}" in 19 | "latest") 20 | echo "Updating bundler" 21 | retry --times 5 --sleep 5 gem update bundler $gem_args 22 | ;; 23 | *) 24 | echo "Updating bundler to $_BUNDLER_VERSION" 25 | retry --times 5 --sleep 5 gem install bundler $gem_args --version $_BUNDLER_VERSION 26 | ;; 27 | esac 28 | -------------------------------------------------------------------------------- /spec/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: ../.rubocop.yml 2 | 3 | Metrics/BlockLength: 4 | Enabled: false 5 | 6 | # Layout/LineLength: 7 | # Max: 100 8 | -------------------------------------------------------------------------------- /spec/integration/runner.rb: -------------------------------------------------------------------------------- 1 | class Runner 2 | attr_reader :pid, :output, :status 3 | 4 | def initialize(name) 5 | @script_name = name 6 | @script_file = "#{@script_name}.rb" 7 | @pid = nil 8 | @output = nil 9 | @status = nil 10 | @post_spawn_wait = jruby? ? 10 : 1 # seconds 11 | @finish_timeout = jruby? ? 10 : 5 # seconds 12 | @read_timeout = 1 # seconds 13 | @read, @write = IO.pipe 14 | @has_run = false 15 | @finished = false 16 | end 17 | 18 | def has_run? 19 | @has_run 20 | end 21 | 22 | def finished? 23 | @finished 24 | end 25 | 26 | def run 27 | raise "Can't run runner more than once!" if @has_run 28 | 29 | @has_run = true 30 | executable = jruby? ? "jruby" : "ruby" 31 | directory = File.join(__dir__, "runners") 32 | @pid = spawn( 33 | "#{executable} #{@script_file}", 34 | { 35 | [:out, :err] => @write, 36 | :chdir => directory 37 | } 38 | ) 39 | sleep @post_spawn_wait # Let the app boot 40 | 41 | yield if block_given? 42 | 43 | begin 44 | Timeout.timeout(@finish_timeout) do 45 | _pid, @status = Process.wait2(@pid) # Wait until the command exits 46 | end 47 | rescue Timeout::Error 48 | Process.kill("TERM", @pid) 49 | raise "ERROR: Runner '#{@script_file}' timed out after #{@finish_timeout} seconds!" 50 | end 51 | read_output 52 | @finished = true 53 | end 54 | 55 | private 56 | 57 | def read_output 58 | Timeout.timeout(@read_timeout) do 59 | @write.close # Close output writer 60 | end 61 | 62 | # Read the output (STDOUT and STDERR) 63 | output_lines = [] 64 | begin 65 | while line = @read.readline # rubocop:disable Lint/AssignmentInCondition 66 | output_lines << line.rstrip 67 | end 68 | rescue EOFError 69 | # Nothing to read anymore. Reached end of "file". 70 | end 71 | @output = output_lines.join("\n") 72 | end 73 | 74 | def jruby? 75 | RUBY_PLATFORM == "java" 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/integration/runners/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appsignal/appsignal-ruby/ef8021b965d486bf65931f261a9d0828ebb6b03d/spec/integration/runners/log/.gitkeep -------------------------------------------------------------------------------- /spec/integration/runners/stop_with_trap.rb: -------------------------------------------------------------------------------- 1 | PROJECT_ROOT = "../../../".freeze 2 | $LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT)) 3 | $LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT)) 4 | 5 | require "fileutils" 6 | require "appsignal" 7 | 8 | Signal.trap("USR1") do 9 | puts "Received USR1 signal" 10 | Appsignal.stop("trap") 11 | puts "AppSignal has shut down without raising an error" 12 | exit 0 13 | end 14 | 15 | # Dummy config 16 | Appsignal.configure(:test) do |config| 17 | config.active = true 18 | config.push_api_key = "abc" 19 | config.name = "Signal app" 20 | 21 | # Use a working directory in the runner's tmp dir to avoid conflicts with the 22 | # host's /tmp dir 23 | working_directory = "tmp/appsignal" 24 | FileUtils.rm_f(working_directory) 25 | FileUtils.mkdir_p(working_directory) 26 | config.working_directory_path = File.join(__dir__, working_directory) 27 | end 28 | 29 | Appsignal.start 30 | 31 | puts "Waiting for USR1 signal..." 32 | # Wait to keep the script alive 33 | loop do 34 | sleep 0.1 35 | end 36 | -------------------------------------------------------------------------------- /spec/integration/stop_spec.rb: -------------------------------------------------------------------------------- 1 | describe "AppSignal stop" do 2 | it "doesn't exit the process with a ThreadError when receiving a signal trap" do 3 | runner = Runner.new("stop_with_trap") 4 | runner.run do 5 | # Send a problematic signal 6 | # "USR1" has no special meaning for this test, it's just a signal 7 | Process.kill("USR1", runner.pid) 8 | # Give it some time to receive the signal and shut down AppSignal 9 | sleep(DependencyHelper.running_jruby? ? 5 : 1) # seconds 10 | end 11 | 12 | output = runner.output 13 | 14 | # Make sure the app exited properly 15 | expect(runner.status.exitstatus).to eq(0) 16 | 17 | # Assert the output has no errors 18 | expect(output).to_not include("ERROR: ") 19 | # Assert the app has started as expected 20 | expect(output).to include("Waiting for USR1 signal...") 21 | # Assert the app has received the signal 22 | expect(output).to include("Received USR1 signal") 23 | # Assert the app continued after calling Appsignal.stop 24 | expect(output).to include("AppSignal has shut down without raising an error") 25 | 26 | # Assert no errors were printed 27 | expect(output).to_not include("ThreadError") 28 | expect(output).to_not include("can't be called from trap context") 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/appsignal/cli/demo_spec.rb: -------------------------------------------------------------------------------- 1 | require "appsignal/cli" 2 | 3 | describe Appsignal::CLI::Demo do 4 | include CLIHelpers 5 | 6 | let(:options) { {} } 7 | let(:out_stream) { std_stream } 8 | let(:output) { out_stream.read } 9 | before(:context) { Appsignal.stop } 10 | 11 | def run 12 | run_within_dir project_fixture_path 13 | end 14 | 15 | def run_within_dir(chdir) 16 | Dir.chdir chdir do 17 | capture_stdout(out_stream) { run_cli("demo", options) } 18 | end 19 | end 20 | 21 | context "without configuration" do 22 | it "returns an error" do 23 | expect { run_within_dir tmp_dir }.to raise_error(SystemExit) 24 | 25 | expect(output).to include("Error: Unable to start the AppSignal agent") 26 | end 27 | end 28 | 29 | context "with configuration" do 30 | before do 31 | # Ignore sleeps to speed up the test 32 | allow(Appsignal::Demo).to receive(:sleep) 33 | end 34 | let(:options) { { :environment => "development" } } 35 | 36 | it "calls Appsignal::Demo transmitter" do 37 | expect(Appsignal::Demo).to receive(:transmit).and_return(true) 38 | run 39 | end 40 | 41 | it "outputs message" do 42 | run 43 | expect(output).to include("Demonstration sample data sent!") 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/lib/appsignal/cli/diagnose/paths_spec.rb: -------------------------------------------------------------------------------- 1 | require "bundler/cli" 2 | require "bundler/cli/common" 3 | require "appsignal/cli/diagnose/paths" 4 | 5 | describe Appsignal::CLI::Diagnose::Paths do 6 | describe "#paths" do 7 | before { start_agent } 8 | 9 | it "returns gem installation path as package_install_path" do 10 | expect(described_class.new.paths[:package_install_path]).to eq( 11 | :label => "AppSignal gem path", 12 | :path => Bundler::CLI::Common.select_spec("appsignal").full_gem_path.strip 13 | ) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/lib/appsignal/cli/diagnose/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require "appsignal/cli/diagnose/utils" 2 | 3 | describe Appsignal::CLI::Diagnose::Utils do 4 | describe ".username_for_uid" do 5 | subject { described_class.username_for_uid(uid) } 6 | 7 | context "when user with id exists" do 8 | let(:uid) { 0 } 9 | 10 | it "returns username" do 11 | is_expected.to be_kind_of(String) 12 | end 13 | end 14 | 15 | context "when user with id does not exist" do 16 | let(:uid) { -1 } 17 | 18 | it "returns nil" do 19 | is_expected.to be_nil 20 | end 21 | end 22 | end 23 | 24 | describe ".group_for_gid" do 25 | subject { described_class.group_for_gid(uid) } 26 | 27 | context "when group with id exists" do 28 | let(:uid) { 0 } 29 | 30 | it "returns group name" do 31 | is_expected.to be_kind_of(String) 32 | end 33 | end 34 | 35 | context "when group with id does not exist" do 36 | let(:uid) { -3 } 37 | 38 | it "returns nil" do 39 | is_expected.to be_nil 40 | end 41 | end 42 | end 43 | 44 | describe ".read_file_content" do 45 | let(:path) { File.join(spec_system_tmp_dir, "test_file.txt") } 46 | let(:bytes_to_read) { 100 } 47 | subject { described_class.read_file_content(path, bytes_to_read) } 48 | before do 49 | File.write(path, file_contents) 50 | end 51 | 52 | context "when file is bigger than read size" do 53 | let(:file_contents) do 54 | "".tap do |s| 55 | 100.times do |i| 56 | s << "line #{i}\n" 57 | end 58 | end 59 | end 60 | 61 | it "returns the last X bytes" do 62 | is_expected 63 | .to eq(file_contents[(file_contents.length - bytes_to_read)..file_contents.length]) 64 | end 65 | end 66 | 67 | context "when file is smaller than read size" do 68 | let(:file_contents) { "line 1\n" } 69 | 70 | it "returns the whole file content" do 71 | is_expected.to eq(file_contents) 72 | end 73 | end 74 | 75 | context "when reading the file raises an illegal seek error" do 76 | let(:file_contents) { "line 1\n" } 77 | before do 78 | expect(File).to receive(:binread).and_raise(Errno::ESPIPE) 79 | end 80 | 81 | it "returns the error as the content" do 82 | expect { subject }.to raise_error(Errno::ESPIPE) 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/lib/appsignal/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require "appsignal/cli" 2 | 3 | describe Appsignal::CLI do 4 | let(:out_stream) { std_stream } 5 | let(:output) { out_stream.read } 6 | let(:cli) { Appsignal::CLI } 7 | before { allow(Dir).to receive(:pwd).and_return(project_fixture_path) } 8 | 9 | it "should print the help with no arguments, -h and --help" do 10 | [nil, "-h", "--help"].each do |arg| 11 | expect do 12 | capture_stdout(out_stream) do 13 | cli.run([arg].compact) 14 | end 15 | end.to raise_error(SystemExit) 16 | 17 | expect(output).to include "appsignal [options]" 18 | expect(output).to include \ 19 | "Available commands: demo, diagnose, install" 20 | end 21 | end 22 | 23 | it "should print the version with -v and --version" do 24 | ["-v", "--version"].each do |arg| 25 | expect do 26 | capture_stdout(out_stream) do 27 | cli.run([arg]) 28 | end 29 | end.to raise_error(SystemExit) 30 | 31 | expect(output).to include "AppSignal" 32 | expect(output).to include "." 33 | end 34 | end 35 | 36 | it "should print a notice if a command does not exist" do 37 | expect do 38 | capture_stdout(out_stream) do 39 | cli.run(["nonsense"]) 40 | end 41 | end.to raise_error(SystemExit) 42 | 43 | expect(output).to include "Command 'nonsense' does not exist, run " \ 44 | "appsignal -h to see the help" 45 | end 46 | 47 | describe "diagnose" do 48 | it "should call Appsignal::Diagnose.install" do 49 | expect(Appsignal::CLI::Diagnose).to receive(:run) 50 | 51 | cli.run([ 52 | "diagnose" 53 | ]) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::EventFormatter::ActionView::RenderFormatter do 2 | let(:klass) { Appsignal::EventFormatter::ActionView::RenderFormatter } 3 | 4 | if DependencyHelper.rails_present? 5 | require "action_view" 6 | 7 | context "when in a Rails app" do 8 | let(:formatter) { klass.new } 9 | before { allow(Rails.root).to receive(:to_s).and_return("/var/www/app/20130101") } 10 | 11 | it "registers render_partial.action_view and render_template.action_view" do 12 | expect(Appsignal::EventFormatter.registered?("render_partial.action_view", 13 | klass)).to be_truthy 14 | expect(Appsignal::EventFormatter.registered?("render_template.action_view", 15 | klass)).to be_truthy 16 | end 17 | 18 | describe "#root_path" do 19 | subject { formatter.root_path } 20 | 21 | it "returns Rails root path" do 22 | is_expected.to eq "/var/www/app/20130101/" 23 | end 24 | end 25 | 26 | describe "#format" do 27 | subject { formatter.format(payload) } 28 | 29 | context "with an identifier" do 30 | let(:payload) { { :identifier => "/var/www/app/20130101/app/views/home/index/html.erb" } } 31 | 32 | it { is_expected.to eq ["app/views/home/index/html.erb", nil] } 33 | end 34 | 35 | context "with a frozen identifier" do 36 | let(:payload) do 37 | { :identifier => "/var/www/app/20130101/app/views/home/index/html.erb".freeze } 38 | end 39 | 40 | it { is_expected.to eq ["app/views/home/index/html.erb", nil] } 41 | end 42 | 43 | context "without an identifier" do 44 | let(:payload) { {} } 45 | 46 | it { is_expected.to be_nil } 47 | end 48 | end 49 | end 50 | else 51 | context "when not in a Rails app" do 52 | it "does not register the event formatter" do 53 | expect(Appsignal::EventFormatter.registered?("render_partial.action_view", 54 | klass)).to be_falsy 55 | expect(Appsignal::EventFormatter.registered?("render_template.action_view", 56 | klass)).to be_falsy 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/lib/appsignal/event_formatter/active_record/instantiation_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::EventFormatter::ActiveRecord::InstantiationFormatter do 2 | let(:klass) { Appsignal::EventFormatter::ActiveRecord::InstantiationFormatter } 3 | let(:formatter) { klass.new } 4 | 5 | it "should register instantiation.active_record" do 6 | expect(Appsignal::EventFormatter.registered?("instantiation.active_record", klass)).to be_truthy 7 | end 8 | 9 | describe "#format" do 10 | let(:payload) do 11 | { 12 | :record_count => 1, 13 | :class_name => "User" 14 | } 15 | end 16 | 17 | subject { formatter.format(payload) } 18 | 19 | it { is_expected.to eq ["User", nil] } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/lib/appsignal/event_formatter/active_record/sql_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::EventFormatter::ActiveRecord::SqlFormatter do 2 | let(:klass) { described_class } 3 | let(:formatter) { klass.new } 4 | 5 | it "should register sql.active_record" do 6 | expect(Appsignal::EventFormatter.registered?("sql.active_record", klass)).to be_truthy 7 | end 8 | 9 | describe "#format" do 10 | let(:payload) do 11 | { 12 | :name => "User load", 13 | :sql => "SELECT * FROM users" 14 | } 15 | end 16 | 17 | subject { formatter.format(payload) } 18 | 19 | it { is_expected.to eq ["User load", "SELECT * FROM users", 1] } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/lib/appsignal/event_formatter/elastic_search/search_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::EventFormatter::ElasticSearch::SearchFormatter do 2 | let(:klass) { Appsignal::EventFormatter::ElasticSearch::SearchFormatter } 3 | let(:formatter) { klass.new } 4 | 5 | it "should register search.elasticsearch" do 6 | expect( 7 | Appsignal::EventFormatter.registered?("search.elasticsearch", klass) 8 | ).to be_truthy 9 | end 10 | 11 | describe "#format" do 12 | let(:payload) do 13 | { 14 | :name => "Search", 15 | :klass => "User", 16 | :search => { :index => "users", :type => "user", :q => "John Doe" } 17 | } 18 | end 19 | 20 | it "should return a payload with name and sanitized body" do 21 | query = 22 | if DependencyHelper.ruby_3_4_or_newer? 23 | "{index: \"users\", type: \"user\", q: \"?\"}" 24 | else 25 | "{:index=>\"users\", :type=>\"user\", :q=>\"?\"}" 26 | end 27 | expect(formatter.format(payload)).to eql([ 28 | "Search: User", 29 | query 30 | ]) 31 | end 32 | end 33 | 34 | describe "#sanitized_search" do 35 | let(:search) do 36 | { 37 | :index => "users", 38 | :type => "user", 39 | :q => "John Doe", 40 | :other => "Other" 41 | } 42 | end 43 | 44 | it "should sanitize non-allowlisted params" do 45 | expect( 46 | formatter.sanitized_search(search) 47 | ).to eql(:index => "users", :type => "user", :q => "?", :other => "?") 48 | end 49 | 50 | it "should return nil string when search is nil" do 51 | expect(formatter.sanitized_search(nil)).to be_nil 52 | end 53 | 54 | it "should return nil string when search is not a hash" do 55 | expect(formatter.sanitized_search([])).to be_nil 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/lib/appsignal/event_formatter/faraday/request_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::EventFormatter::Faraday::RequestFormatter do 2 | let(:klass) { Appsignal::EventFormatter::Faraday::RequestFormatter } 3 | let(:formatter) { klass.new } 4 | 5 | it "should register request.faraday" do 6 | expect(Appsignal::EventFormatter.registered?("request.faraday", klass)).to be_truthy 7 | end 8 | 9 | describe "#format" do 10 | let(:payload) do 11 | { 12 | :method => :get, 13 | :url => URI.parse("http://example.org/hello/world?some=param") 14 | } 15 | end 16 | 17 | subject { formatter.format(payload) } 18 | 19 | it { is_expected.to eq ["GET http://example.org", "GET http://example.org/hello/world"] } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/lib/appsignal/event_formatter/rom/sql_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Appsignal::EventFormatter::Rom::SqlFormatter do 4 | let(:klass) { described_class } 5 | let(:formatter) { klass.new } 6 | 7 | it "registers the sql event formatter" do 8 | expect(Appsignal::EventFormatter.registered?("sql.dry", klass)).to be_truthy 9 | end 10 | 11 | describe "#format" do 12 | let(:payload) do 13 | { 14 | :name => "postgres", 15 | :query => "SELECT * FROM users" 16 | } 17 | end 18 | subject { formatter.format(payload) } 19 | 20 | it { is_expected.to eq ["query.postgres", "SELECT * FROM users", 1] } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/lib/appsignal/event_formatter/sequel/sql_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::EventFormatter::Sequel::SqlFormatter do 2 | let(:klass) { described_class } 3 | let(:formatter) { klass.new } 4 | 5 | it "registers the sql.sequel event formatter" do 6 | expect(Appsignal::EventFormatter.registered?("sql.sequel", klass)).to be_truthy 7 | end 8 | 9 | describe "#format" do 10 | before do 11 | stub_const( 12 | "SequelDatabaseTypeClass", 13 | Class.new do 14 | def self.to_s 15 | "SequelDatabaseTypeClassToString" 16 | end 17 | end 18 | ) 19 | end 20 | let(:payload) do 21 | { 22 | :name => SequelDatabaseTypeClass, 23 | :sql => "SELECT * FROM users" 24 | } 25 | end 26 | subject { formatter.format(payload) } 27 | 28 | it { is_expected.to eq ["SequelDatabaseTypeClassToString", "SELECT * FROM users", 1] } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/appsignal/event_formatter/view_component/render_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::EventFormatter::ViewComponent::RenderFormatter do 2 | let(:klass) { Appsignal::EventFormatter::ViewComponent::RenderFormatter } 3 | 4 | if DependencyHelper.rails_present? 5 | context "when in a Rails app" do 6 | let(:formatter) { klass.new } 7 | before { allow(Rails.root).to receive(:to_s).and_return("/var/www/app/20130101") } 8 | 9 | it "registers render.view_component and (deprecated) !render.view_component" do 10 | expect(Appsignal::EventFormatter.registered?("render.view_component", 11 | klass)).to be_truthy 12 | expect(Appsignal::EventFormatter.registered?("!render.view_component", 13 | klass)).to be_truthy 14 | end 15 | 16 | describe "#format" do 17 | subject { formatter.format(payload) } 18 | 19 | context "with a name and identifier" do 20 | let(:payload) do 21 | { 22 | :name => "WhateverComponent", 23 | :identifier => "/var/www/app/20130101/app/components/whatever_component.rb" 24 | } 25 | end 26 | 27 | it { is_expected.to eq ["WhateverComponent", "app/components/whatever_component.rb"] } 28 | end 29 | end 30 | end 31 | else 32 | context "when not in a Rails app" do 33 | it "does not register the event formatter" do 34 | expect(Appsignal::EventFormatter.registered?("render.view_component", 35 | klass)).to be_falsy 36 | expect(Appsignal::EventFormatter.registered?("!render.view_component", 37 | klass)).to be_falsy 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/lib/appsignal/extension/jruby_spec.rb: -------------------------------------------------------------------------------- 1 | describe "JRuby extension", :jruby do 2 | let(:extension) { Appsignal::Extension } 3 | let(:jruby_module) { Appsignal::Extension::Jruby } 4 | 5 | it "creates a JRuby extension module" do 6 | expect(Appsignal::Extension::Jruby).to be_kind_of(Module) 7 | end 8 | 9 | describe "string conversions" do 10 | it "keeps the same value during string type conversions" do 11 | # UTF-8 string with NULL 12 | # Tests if the conversions between the conversions without breaking on 13 | # NULL terminated strings in C. 14 | string = "Merry Christmas! \u0000 🎄" 15 | 16 | appsignal_string = extension.make_appsignal_string(string) 17 | ruby_string = extension.make_ruby_string(appsignal_string) 18 | 19 | expect(ruby_string).to eq("Merry Christmas! \u0000 🎄") 20 | end 21 | end 22 | 23 | it "loads libappsignal with FFI" do 24 | expect(jruby_module.ffi_libraries.map(&:name).first).to include "libappsignal" 25 | end 26 | 27 | describe ".lib_extension" do 28 | subject { jruby_module.lib_extension } 29 | 30 | context "when on a darwin system" do 31 | before { expect(Appsignal::System).to receive(:agent_platform).and_return("darwin") } 32 | 33 | it "returns the extension for darwin" do 34 | is_expected.to eq "dylib" 35 | end 36 | end 37 | 38 | context "when on a linux system" do 39 | before { expect(Appsignal::System).to receive(:agent_platform).and_return("linux") } 40 | 41 | it "returns the lib extension for linux" do 42 | is_expected.to eq "so" 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/lib/appsignal/extension_install_failure_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Extension, :extension_installation_failure do 2 | context "when the extension library cannot be loaded" do 3 | it "prints and logs an error" do 4 | require "open3" 5 | _stdout, stderr, _status = Open3.capture3("bin/appsignal --version") 6 | expect(stderr).to include("ERROR: AppSignal failed to load extension") 7 | error_message = 8 | if DependencyHelper.running_jruby? 9 | if DependencyHelper.macos? 10 | "Could not open library" 11 | else 12 | "cannot open shared object file" 13 | end 14 | else 15 | "LoadError: cannot load such file" 16 | end 17 | expect(stderr).to include(error_message) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/action_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::ActionMailerHook do 2 | if DependencyHelper.action_mailer_present? && 3 | DependencyHelper.rails_version >= Gem::Version.new("4.0.0") 4 | context "with ActionMailer" do 5 | require "action_mailer" 6 | 7 | class UserMailer < ActionMailer::Base 8 | default :from => "test@example.com" 9 | 10 | def welcome 11 | mail(:to => "test@example.com", :subject => "ActionMailer test", 12 | :content_type => "text/html") do |format| 13 | format.html { render :html => "This is a test" } 14 | end 15 | end 16 | end 17 | UserMailer.delivery_method = :test 18 | 19 | describe ".dependencies_present?" do 20 | subject { described_class.new.dependencies_present? } 21 | 22 | it "returns true" do 23 | is_expected.to be_truthy 24 | end 25 | end 26 | 27 | describe ".install" do 28 | before do 29 | start_agent 30 | expect(Appsignal.active?).to be_truthy 31 | end 32 | 33 | it "is subscribed to 'process.action_mailer' and processes instrumentation" do 34 | expect(Appsignal).to receive(:increment_counter).with( 35 | :action_mailer_process, 36 | 1, 37 | :mailer => "UserMailer", :action => :welcome 38 | ) 39 | 40 | UserMailer.welcome.deliver_now 41 | end 42 | end 43 | end 44 | else 45 | context "without ActionMailer" do 46 | describe ".dependencies_present?" do 47 | subject { described_class.new.dependencies_present? } 48 | 49 | it "returns false" do 50 | is_expected.to be_falsy 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/active_support_notifications/finish_with_state_shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples "activesupport finish_with_state override" do 2 | let(:instrumenter) { as.instrumenter } 3 | 4 | it "instruments an ActiveSupport::Notifications.start/finish event with payload on finish" do 5 | listeners_state = instrumenter.start("sql.active_record", {}) 6 | instrumenter.finish_with_state(listeners_state, "sql.active_record", :sql => "SQL") 7 | 8 | expect(transaction).to include_event( 9 | "body" => "SQL", 10 | "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT, 11 | "count" => 1, 12 | "name" => "sql.active_record", 13 | "title" => "" 14 | ) 15 | end 16 | 17 | it "does not instrument events whose name starts with a bang" do 18 | listeners_state = instrumenter.start("!sql.active_record", {}) 19 | instrumenter.finish_with_state(listeners_state, "!sql.active_record", :sql => "SQL") 20 | 21 | expect(transaction).to_not include_events 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/active_support_notifications/start_finish_shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples "activesupport start finish override" do 2 | let(:instrumenter) { as.instrumenter } 3 | 4 | it "instruments start/finish events with payload on start ignores payload" do 5 | instrumenter.start("sql.active_record", :sql => "SQL") 6 | instrumenter.finish("sql.active_record", {}) 7 | 8 | expect(transaction).to include_event( 9 | "body" => "", 10 | "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT, 11 | "count" => 1, 12 | "name" => "sql.active_record", 13 | "title" => "" 14 | ) 15 | end 16 | 17 | it "instruments an ActiveSupport::Notifications.start/finish event with payload on finish" do 18 | instrumenter.start("sql.active_record", {}) 19 | instrumenter.finish("sql.active_record", :sql => "SQL") 20 | 21 | expect(transaction).to include_event( 22 | "body" => "SQL", 23 | "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT, 24 | "count" => 1, 25 | "name" => "sql.active_record", 26 | "title" => "" 27 | ) 28 | end 29 | 30 | it "does not instrument events whose name starts with a bang" do 31 | instrumenter.start("!sql.active_record", {}) 32 | instrumenter.finish("!sql.active_record", {}) 33 | 34 | expect(transaction).to_not include_events 35 | end 36 | 37 | context "when a transaction is completed in an instrumented block" do 38 | it "does not complete the ActiveSupport::Notifications.instrument event" do 39 | instrumenter.start("sql.active_record", {}) 40 | Appsignal::Transaction.complete_current! 41 | instrumenter.finish("sql.active_record", {}) 42 | 43 | expect(transaction).to_not include_events 44 | expect(transaction).to be_completed 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/active_support_notifications_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "active_support_notifications/instrument_shared_examples" 2 | 3 | describe Appsignal::Hooks::ActiveSupportNotificationsHook do 4 | if active_support_present? 5 | let(:notifier) { ActiveSupport::Notifications::Fanout.new } 6 | let(:as) { ActiveSupport::Notifications } 7 | let(:transaction) { http_request_transaction } 8 | before do 9 | start_agent 10 | set_current_transaction(transaction) 11 | as.notifier = notifier 12 | end 13 | around { |example| keep_transactions { example.run } } 14 | 15 | describe "#dependencies_present?" do 16 | subject { described_class.new.dependencies_present? } 17 | 18 | it { is_expected.to be_truthy } 19 | end 20 | 21 | it_behaves_like "activesupport instrument override" 22 | 23 | if defined?(::ActiveSupport::Notifications::Fanout::Handle) 24 | require_relative "active_support_notifications/start_finish_shared_examples" 25 | 26 | it_behaves_like "activesupport start finish override" 27 | end 28 | 29 | if ::ActiveSupport::Notifications::Instrumenter.method_defined?(:start) 30 | require_relative "active_support_notifications/start_finish_shared_examples" 31 | 32 | it_behaves_like "activesupport start finish override" 33 | end 34 | 35 | if ::ActiveSupport::Notifications::Instrumenter.method_defined?(:finish_with_state) 36 | require_relative "active_support_notifications/finish_with_state_shared_examples" 37 | 38 | it_behaves_like "activesupport finish_with_state override" 39 | end 40 | else 41 | describe "#dependencies_present?" do 42 | subject { described_class.new.dependencies_present? } 43 | 44 | it { is_expected.to be_falsy } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/celluloid_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::CelluloidHook do 2 | context "with celluloid" do 3 | before do 4 | stub_const("Celluloid", Module.new do 5 | def self.shutdown 6 | @shut_down = true 7 | end 8 | 9 | def self.shut_down? 10 | @shut_down == true 11 | end 12 | end) 13 | Appsignal::Hooks::CelluloidHook.new.install 14 | end 15 | 16 | describe "#dependencies_present?" do 17 | subject { described_class.new.dependencies_present? } 18 | 19 | it { is_expected.to be_truthy } 20 | end 21 | 22 | describe "#install" do 23 | it "calls Appsignal.stop on shutdown" do 24 | expect(Appsignal).to receive(:stop) 25 | Celluloid.shutdown 26 | expect(Celluloid.shut_down?).to be true 27 | end 28 | end 29 | end 30 | 31 | context "without celluloid" do 32 | describe "#dependencies_present?" do 33 | subject { described_class.new.dependencies_present? } 34 | 35 | it { is_expected.to be_falsy } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/data_mapper_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::DataMapperHook do 2 | context "with datamapper" do 3 | before do 4 | stub_const("DataMapper", Module.new) 5 | stub_const("DataObjects", Module.new) 6 | stub_const("DataObjects::Connection", Class.new) 7 | Appsignal::Hooks::DataMapperHook.new.install 8 | end 9 | 10 | describe "#dependencies_present?" do 11 | subject { described_class.new.dependencies_present? } 12 | 13 | it { is_expected.to be_truthy } 14 | end 15 | 16 | it "should install the listener" do 17 | expect(::DataObjects::Connection).to receive(:include) 18 | .with(Appsignal::Hooks::DataMapperLogListener) 19 | 20 | Appsignal::Hooks::DataMapperHook.new.install 21 | end 22 | end 23 | 24 | context "without datamapper" do 25 | describe "#dependencies_present?" do 26 | subject { described_class.new.dependencies_present? } 27 | 28 | it { is_expected.to be_falsy } 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/delayed_job_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::DelayedJobHook do 2 | context "with delayed job" do 3 | before do 4 | stub_const("Delayed::Plugin", Class.new do 5 | def self.callbacks 6 | end 7 | end) 8 | stub_const("Delayed::Worker", Class.new do 9 | def self.plugins 10 | @plugins ||= [] 11 | end 12 | end) 13 | start_agent 14 | end 15 | 16 | describe "#dependencies_present?" do 17 | subject { described_class.new.dependencies_present? } 18 | 19 | it { is_expected.to be_truthy } 20 | end 21 | 22 | it "adds the plugin" do 23 | expect(::Delayed::Worker.plugins).to include(Appsignal::Integrations::DelayedJobPlugin) 24 | end 25 | end 26 | 27 | context "without delayed job" do 28 | describe "#dependencies_present?" do 29 | subject { described_class.new.dependencies_present? } 30 | 31 | it { is_expected.to be_falsy } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/dry_monitor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if DependencyHelper.dry_monitor_present? 4 | require "dry-monitor" 5 | 6 | describe Appsignal::Hooks::DryMonitorHook do 7 | describe "#dependencies_present?" do 8 | subject { described_class.new.dependencies_present? } 9 | 10 | context "when Dry::Monitor::Notifications constant is found" do 11 | before { stub_const "Dry::Monitor::Notifications", Class.new } 12 | 13 | it { is_expected.to be_truthy } 14 | end 15 | 16 | context "when Dry::Monitor::Notifications constant is not found" do 17 | before { hide_const "Dry::Monitor::Notifications" } 18 | 19 | it { is_expected.to be_falsy } 20 | end 21 | end 22 | end 23 | 24 | describe "#install" do 25 | it "installs the dry-monitor hook" do 26 | start_agent 27 | 28 | expect(Dry::Monitor::Notifications.included_modules).to include( 29 | Appsignal::Integrations::DryMonitorIntegration 30 | ) 31 | end 32 | end 33 | 34 | describe "Dry Monitor Integration" do 35 | let(:notifications) { Dry::Monitor::Notifications.new(:test) } 36 | let(:transaction) { http_request_transaction } 37 | before do 38 | start_agent 39 | set_current_transaction(transaction) 40 | end 41 | 42 | context "when is a dry-sql event" do 43 | let(:event_id) { :sql } 44 | let(:payload) do 45 | { 46 | :name => "postgres", 47 | :query => "SELECT * FROM users" 48 | } 49 | end 50 | 51 | it "creates an sql event" do 52 | notifications.instrument(event_id, payload) 53 | expect(transaction).to include_event( 54 | "body" => "SELECT * FROM users", 55 | "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT, 56 | "count" => 1, 57 | "name" => "query.postgres", 58 | "title" => "query.postgres" 59 | ) 60 | end 61 | end 62 | 63 | context "when is an unregistered formatter event" do 64 | let(:event_id) { :foo } 65 | let(:payload) do 66 | { 67 | :name => "foo" 68 | } 69 | end 70 | 71 | it "creates a generic event" do 72 | notifications.instrument(event_id, payload) 73 | expect(transaction).to include_event( 74 | "body" => "", 75 | "body_format" => Appsignal::EventFormatter::DEFAULT, 76 | "count" => 1, 77 | "name" => "foo", 78 | "title" => "" 79 | ) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/excon_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::ExconHook do 2 | before { start_agent } 3 | 4 | context "with Excon" do 5 | before do 6 | stub_const("Excon", Class.new do 7 | def self.defaults 8 | @defaults ||= {} 9 | end 10 | end) 11 | Appsignal::Hooks::ExconHook.new.install 12 | end 13 | 14 | describe "#dependencies_present?" do 15 | subject { described_class.new.dependencies_present? } 16 | 17 | it { is_expected.to be_truthy } 18 | end 19 | 20 | describe "#install" do 21 | it "adds the AppSignal instrumentor to Excon" do 22 | expect(Excon.defaults[:instrumentor]).to eql(Appsignal::Integrations::ExconIntegration) 23 | end 24 | end 25 | 26 | describe "instrumentation" do 27 | let(:transaction) { http_request_transaction } 28 | before { set_current_transaction(transaction) } 29 | around { |example| keep_transactions { example.run } } 30 | 31 | it "instruments a http request" do 32 | data = { 33 | :host => "www.google.com", 34 | :method => :get, 35 | :scheme => "http" 36 | } 37 | Excon.defaults[:instrumentor].instrument("excon.request", data) {} # rubocop:disable Lint/EmptyBlock 38 | 39 | expect(transaction).to include_event( 40 | "name" => "request.excon", 41 | "title" => "GET http://www.google.com", 42 | "body" => "" 43 | ) 44 | end 45 | 46 | it "instruments a http response" do 47 | data = { :host => "www.google.com" } 48 | Excon.defaults[:instrumentor].instrument("excon.response", data) {} # rubocop:disable Lint/EmptyBlock 49 | 50 | expect(transaction).to include_event( 51 | "name" => "response.excon", 52 | "title" => "www.google.com", 53 | "body" => "" 54 | ) 55 | end 56 | end 57 | end 58 | 59 | context "without Excon" do 60 | describe "#dependencies_present?" do 61 | subject { described_class.new.dependencies_present? } 62 | 63 | it { is_expected.to be_falsy } 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/http_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe Appsignal::Hooks::HttpHook do 4 | let(:options) { {} } 5 | before { start_agent(:options => options) } 6 | 7 | if DependencyHelper.http_present? 8 | context "with instrument_http_rb set to true" do 9 | describe "#dependencies_present?" do 10 | subject { described_class.new.dependencies_present? } 11 | 12 | it { is_expected.to be_truthy } 13 | end 14 | 15 | it "installs the HTTP plugin" do 16 | expect(HTTP::Client.included_modules) 17 | .to include(Appsignal::Integrations::HttpIntegration) 18 | end 19 | end 20 | 21 | context "with instrument_http_rb set to false" do 22 | let(:options) { { :instrument_http_rb => false } } 23 | 24 | describe "#dependencies_present?" do 25 | subject { described_class.new.dependencies_present? } 26 | 27 | it { is_expected.to be_falsy } 28 | end 29 | end 30 | else 31 | describe "#dependencies_present?" do 32 | subject { described_class.new.dependencies_present? } 33 | 34 | it { is_expected.to be_falsy } 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/mongo_ruby_driver_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::MongoRubyDriverHook do 2 | require "appsignal/integrations/mongo_ruby_driver" 3 | 4 | context "with mongo ruby driver" do 5 | let(:subscriber) { Appsignal::Hooks::MongoMonitorSubscriber.new } 6 | before do 7 | allow(Appsignal::Hooks::MongoMonitorSubscriber).to receive(:new).and_return(subscriber) 8 | end 9 | 10 | before do 11 | stub_const("Mongo::Monitoring", Module.new) 12 | stub_const("Mongo::Monitoring::COMMAND", "command") 13 | stub_const("Mongo::Monitoring::Global", Class.new do 14 | def subscribe 15 | end 16 | end) 17 | end 18 | 19 | describe "#dependencies_present?" do 20 | subject { described_class.new.dependencies_present? } 21 | 22 | it { is_expected.to be_truthy } 23 | end 24 | 25 | it "adds a subscriber to Mongo::Monitoring" do 26 | expect(Mongo::Monitoring::Global).to receive(:subscribe) 27 | .with("command", subscriber) 28 | .at_least(:once) 29 | 30 | Appsignal::Hooks::MongoRubyDriverHook.new.install 31 | end 32 | end 33 | 34 | context "without mongo ruby driver" do 35 | describe "#dependencies_present?" do 36 | subject { described_class.new.dependencies_present? } 37 | 38 | it { is_expected.to be_falsy } 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/mri_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::MriHook do 2 | describe "#dependencies_present?" do 3 | subject { described_class.new.dependencies_present? } 4 | 5 | if DependencyHelper.running_jruby? 6 | it { is_expected.to be_falsy } 7 | else 8 | it { is_expected.to be_truthy } 9 | end 10 | end 11 | 12 | unless DependencyHelper.running_jruby? 13 | context "install" do 14 | before do 15 | Appsignal::Hooks.load_hooks 16 | end 17 | 18 | it "should be added to minutely probes" do 19 | expect(Appsignal::Probes.probes[:mri]).to be Appsignal::Probes::MriProbe 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/net_http_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::NetHttpHook do 2 | let(:options) { {} } 3 | before { start_agent(:options => options) } 4 | 5 | describe "#dependencies_present?" do 6 | subject { described_class.new.dependencies_present? } 7 | 8 | context "with Net::HTTP instrumentation enabled" do 9 | it { is_expected.to be_truthy } 10 | end 11 | 12 | context "with Net::HTTP instrumentation disabled" do 13 | let(:options) { { :instrument_net_http => false } } 14 | 15 | it { is_expected.to be_falsy } 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/passenger_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::PassengerHook do 2 | context "with passenger" do 3 | before do 4 | stub_const("PhusionPassenger", Module.new) 5 | end 6 | 7 | describe "#dependencies_present?" do 8 | subject { described_class.new.dependencies_present? } 9 | 10 | it { is_expected.to be_truthy } 11 | end 12 | 13 | it "adds behavior to stopping_worker_process and starting_worker_process" do 14 | expect(PhusionPassenger).to receive(:on_event).with(:starting_worker_process) 15 | expect(PhusionPassenger).to receive(:on_event).with(:stopping_worker_process) 16 | 17 | Appsignal::Hooks::PassengerHook.new.install 18 | end 19 | end 20 | 21 | context "without passenger" do 22 | describe "#dependencies_present?" do 23 | subject { described_class.new.dependencies_present? } 24 | 25 | it { is_expected.to be_falsy } 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/puma_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::PumaHook do 2 | context "with puma" do 3 | let(:puma_version) { "6.0.0" } 4 | before do 5 | stub_const("Puma", PumaMock) 6 | stub_const("Puma::Const::VERSION", puma_version) 7 | end 8 | 9 | describe "#dependencies_present?" do 10 | subject { described_class.new.dependencies_present? } 11 | 12 | context "when Puma present" do 13 | context "when Puma is newer than version 3.0.0" do 14 | let(:puma_version) { "3.0.0" } 15 | 16 | it { is_expected.to be_truthy } 17 | end 18 | 19 | context "when Puma is older than version 3.0.0" do 20 | let(:puma_version) { "2.9.9" } 21 | 22 | it { is_expected.to be_falsey } 23 | end 24 | end 25 | 26 | context "when Puma is not present" do 27 | before do 28 | hide_const("Puma") 29 | end 30 | 31 | it { is_expected.to be_falsey } 32 | end 33 | end 34 | 35 | describe "installation" do 36 | before { Appsignal::Probes.probes.clear } 37 | 38 | it "adds the Puma::Server patch" do 39 | Appsignal::Hooks::PumaHook.new.install 40 | expect(::Puma::Server.included_modules).to include(Appsignal::Integrations::PumaServer) 41 | end 42 | 43 | context "when not clustered mode" do 44 | it "does not add AppSignal stop behavior Puma::Cluster" do 45 | expect(defined?(::Puma::Cluster)).to be_falsy 46 | # Does not error on call 47 | Appsignal::Hooks::PumaHook.new.install 48 | end 49 | end 50 | 51 | context "when in clustered mode" do 52 | before do 53 | stub_const("Puma::Cluster", Class.new do 54 | def stop_workers 55 | @called = true 56 | end 57 | end) 58 | end 59 | 60 | it "adds behavior to Puma::Cluster.stop_workers" do 61 | Appsignal::Hooks::PumaHook.new.install 62 | cluster = Puma::Cluster.new 63 | 64 | expect(cluster.instance_variable_defined?(:@called)).to be_falsy 65 | expect(Appsignal).to receive(:stop).and_call_original 66 | cluster.stop_workers 67 | expect(cluster.instance_variable_get(:@called)).to be(true) 68 | end 69 | end 70 | end 71 | end 72 | 73 | context "without puma" do 74 | describe "#dependencies_present?" do 75 | subject { described_class.new.dependencies_present? } 76 | 77 | it { is_expected.to be_falsy } 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/que_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::QueHook do 2 | if DependencyHelper.que_present? 3 | describe "#dependencies_present?" do 4 | subject { described_class.new.dependencies_present? } 5 | 6 | it { is_expected.to be_truthy } 7 | end 8 | 9 | it "installs the QuePlugin" do 10 | expect(Que::Job.included_modules).to include(Appsignal::Integrations::QuePlugin) 11 | end 12 | else 13 | describe "#dependencies_present?" do 14 | subject { described_class.new.dependencies_present? } 15 | 16 | it { is_expected.to be_falsy } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/resque_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::ResqueHook do 2 | describe "#dependency_present?" do 3 | subject { described_class.new.dependencies_present? } 4 | 5 | context "when Resque is loaded" do 6 | before { stub_const "Resque", 1 } 7 | 8 | it { is_expected.to be_truthy } 9 | end 10 | 11 | context "when Resque is not loaded" do 12 | before { hide_const "Resque" } 13 | 14 | it { is_expected.to be_falsy } 15 | end 16 | end 17 | 18 | if DependencyHelper.resque_present? 19 | describe "#install" do 20 | before { start_agent } 21 | 22 | it "adds the ResqueIntegration module to Resque::Job" do 23 | expect(Resque::Job.included_modules).to include(Appsignal::Integrations::ResqueIntegration) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/sequel_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::SequelHook do 2 | if DependencyHelper.sequel_present? 3 | let(:db) do 4 | if DependencyHelper.running_jruby? 5 | Sequel.connect("jdbc:sqlite::memory:") 6 | else 7 | Sequel.sqlite 8 | end 9 | end 10 | 11 | before { start_agent } 12 | 13 | describe "#dependencies_present?" do 14 | subject { described_class.new.dependencies_present? } 15 | 16 | it { is_expected.to be_truthy } 17 | end 18 | 19 | context "with a transaction" do 20 | let(:transaction) { http_request_transaction } 21 | before do 22 | set_current_transaction(transaction) 23 | db.logger = Logger.new($stdout) # To test #log_duration call 24 | end 25 | 26 | it "should instrument queries" do 27 | expect(transaction).to receive(:start_event).at_least(:once) 28 | expect(transaction).to receive(:finish_event) 29 | .at_least(:once) 30 | .with("sql.sequel", nil, kind_of(String), 1) 31 | 32 | expect(db).to receive(:log_duration).at_least(:once) 33 | 34 | db["SELECT 1"].all.to_a 35 | end 36 | end 37 | else 38 | describe "#dependencies_present?" do 39 | subject { described_class.new.dependencies_present? } 40 | 41 | it { is_expected.to be_falsy } 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/shoryuken_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::ShoryukenHook do 2 | context "with shoryuken" do 3 | before do 4 | stub_const("Shoryuken", Module.new do 5 | def self.configure_server 6 | end 7 | end) 8 | Appsignal::Hooks::ShoryukenHook.new.install 9 | end 10 | 11 | describe "#dependencies_present?" do 12 | subject { described_class.new.dependencies_present? } 13 | 14 | it { is_expected.to be_truthy } 15 | end 16 | end 17 | 18 | context "without shoryuken" do 19 | describe "#dependencies_present?" do 20 | subject { described_class.new.dependencies_present? } 21 | 22 | it { is_expected.to be_falsy } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/unicorn_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::UnicornHook do 2 | context "with unicorn" do 3 | before do 4 | stub_const("Unicorn", Module.new) 5 | stub_const("Unicorn::HttpServer", Class.new do 6 | def worker_loop(_worker) 7 | @worker_loop = true 8 | end 9 | 10 | def worker_loop? 11 | @worker_loop == true 12 | end 13 | end) 14 | stub_const("Unicorn::Worker", Class.new do 15 | def close 16 | @close = true 17 | end 18 | 19 | def close? 20 | @close == true 21 | end 22 | end) 23 | Appsignal::Hooks::UnicornHook.new.install 24 | end 25 | 26 | describe "#dependencies_present?" do 27 | subject { described_class.new.dependencies_present? } 28 | 29 | it { is_expected.to be_truthy } 30 | end 31 | 32 | it "adds behavior to Unicorn::HttpServer#worker_loop" do 33 | server = Unicorn::HttpServer.new 34 | worker = double 35 | 36 | expect(Appsignal).to receive(:forked) 37 | 38 | server.worker_loop(worker) 39 | 40 | expect(server.worker_loop?).to be true 41 | end 42 | 43 | it "adds behavior to Unicorn::Worker#close" do 44 | worker = Unicorn::Worker.new 45 | 46 | expect(Appsignal).to receive(:stop) 47 | 48 | worker.close 49 | expect(worker.close?).to be true 50 | end 51 | end 52 | 53 | context "without unicorn" do 54 | describe "#dependencies_present?" do 55 | subject { described_class.new.dependencies_present? } 56 | 57 | it { is_expected.to be_falsy } 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/lib/appsignal/hooks/webmachine_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Hooks::WebmachineHook do 2 | if DependencyHelper.webmachine_present? 3 | context "with webmachine" do 4 | let(:fsm) { Webmachine::Decision::FSM.new(double(:trace? => false), double, double) } 5 | before { start_agent } 6 | 7 | describe "#dependencies_present?" do 8 | subject { described_class.new.dependencies_present? } 9 | 10 | it { is_expected.to be_truthy } 11 | end 12 | 13 | it "adds behavior to Webmachine::Decision::FSM" do 14 | expect(fsm.class.ancestors.first).to eq(Appsignal::Integrations::WebmachineIntegration) 15 | end 16 | end 17 | else 18 | describe "#dependencies_present?" do 19 | subject { described_class.new.dependencies_present? } 20 | 21 | it { is_expected.to be_falsy } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/lib/appsignal/integrations/data_mapper_spec.rb: -------------------------------------------------------------------------------- 1 | require "appsignal/integrations/data_mapper" 2 | 3 | describe Appsignal::Hooks::DataMapperLogListener do 4 | describe "#log" do 5 | let(:transaction) { http_request_transaction } 6 | let(:message) do 7 | double( 8 | :query => "SELECT * from users", 9 | :duration => 100_000_000 # nanoseconds 10 | ) 11 | end 12 | before do 13 | stub_const("DataMapperLog", Module.new do 14 | def log(message) 15 | end 16 | end) 17 | stub_const("DataObjects", Module.new) 18 | start_agent 19 | set_current_transaction(transaction) 20 | end 21 | around { |example| keep_transactions { example.run } } 22 | 23 | def log_message 24 | connection_class.new.log(message) 25 | end 26 | 27 | context "when the scheme is SQL-like" do 28 | let(:connection_class) { DataObjects::Sqlite3::Connection } 29 | before do 30 | stub_const("DataObjects::Sqlite3::Connection", Class.new do 31 | include DataMapperLog 32 | include Appsignal::Hooks::DataMapperLogListener 33 | end) 34 | end 35 | 36 | it "records the log entry in an event" do 37 | log_message 38 | 39 | expect(transaction).to include_event( 40 | "name" => "query.data_mapper", 41 | "title" => "DataMapper Query", 42 | "body" => "SELECT * from users", 43 | "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT, 44 | "duration" => 100.0 45 | ) 46 | end 47 | end 48 | 49 | context "when the scheme is not SQL-like" do 50 | let(:connection_class) { DataObjects::MongoDB::Connection } 51 | before do 52 | stub_const("DataObjects::MongoDB::Connection", Class.new do 53 | include DataMapperLog 54 | include Appsignal::Hooks::DataMapperLogListener 55 | end) 56 | end 57 | 58 | it "records the log entry in an event without body" do 59 | log_message 60 | 61 | expect(transaction).to include_event( 62 | "name" => "query.data_mapper", 63 | "title" => "DataMapper Query", 64 | "body" => "", 65 | "body_format" => Appsignal::EventFormatter::DEFAULT, 66 | "duration" => 100.0 67 | ) 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/lib/appsignal/integrations/net_http_spec.rb: -------------------------------------------------------------------------------- 1 | require "appsignal/integrations/net_http" 2 | 3 | describe Appsignal::Integrations::NetHttpIntegration do 4 | let(:transaction) { http_request_transaction } 5 | before { start_agent } 6 | before { set_current_transaction transaction } 7 | around { |example| keep_transactions { example.run } } 8 | 9 | it "instruments a http request" do 10 | stub_request(:any, "http://www.google.com/") 11 | 12 | Net::HTTP.get_response(URI.parse("http://www.google.com")) 13 | 14 | expect(transaction).to include_event( 15 | "name" => "request.net_http", 16 | "title" => "GET http://www.google.com" 17 | ) 18 | end 19 | 20 | it "instruments a https request" do 21 | stub_request(:any, "https://www.google.com/") 22 | 23 | uri = URI.parse("https://www.google.com") 24 | http = Net::HTTP.new(uri.host, uri.port) 25 | http.use_ssl = true 26 | http.get(uri.request_uri) 27 | 28 | expect(transaction).to include_event( 29 | "name" => "request.net_http", 30 | "title" => "GET https://www.google.com" 31 | ) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/lib/appsignal/loaders/grape_spec.rb: -------------------------------------------------------------------------------- 1 | if DependencyHelper.grape_present? 2 | describe "Appsignal::Loaders::PadrinoLoader" do 3 | describe "#on_load" do 4 | it "ensures the Grape middleware is loaded" do 5 | load_loader(:grape) 6 | 7 | # Calling this doesn't raise a NameError 8 | Appsignal::Rack::GrapeMiddleware 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/lib/appsignal/loaders/sinatra_spec.rb: -------------------------------------------------------------------------------- 1 | if DependencyHelper.sinatra_present? 2 | describe "Appsignal::Loaders::SinatraLoader" do 3 | describe "#on_load" do 4 | it "registers Sinatra default config" do 5 | ::Sinatra::Application.settings.root = "/some/path" 6 | load_loader(:sinatra) 7 | 8 | expect(Appsignal::Config.loader_defaults).to include( 9 | :name => :sinatra, 10 | :root_path => "/some/path", 11 | :env => :test, 12 | :options => {} 13 | ) 14 | end 15 | end 16 | 17 | describe "#on_start" do 18 | after { uninstall_sinatra_integration } 19 | 20 | def uninstall_sinatra_integration 21 | expected_middleware = [ 22 | Rack::Events, 23 | Appsignal::Rack::SinatraBaseInstrumentation 24 | ] 25 | Sinatra::Base.instance_variable_get(:@middleware).delete_if do |middleware| 26 | expected_middleware.include?(middleware.first) 27 | end 28 | end 29 | 30 | it "adds the instrumentation middleware to Sinatra::Base" do 31 | load_loader(:sinatra) 32 | start_loader(:sinatra) 33 | 34 | middlewares = Sinatra::Base.middleware.to_a 35 | expect(middlewares).to include( 36 | [Rack::Events, [[instance_of(Appsignal::Rack::EventHandler)]], nil] 37 | ) 38 | expect(middlewares).to include( 39 | [Appsignal::Rack::SinatraBaseInstrumentation, [], nil] 40 | ) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/lib/appsignal/marker_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Marker do 2 | let(:config) { build_config } 3 | let(:marker) do 4 | described_class.new( 5 | { 6 | :revision => "503ce0923ed177a3ce000005", 7 | :repository => "main", 8 | :user => "batman", 9 | :rails_env => "production" 10 | }, 11 | config 12 | ) 13 | end 14 | let(:out_stream) { std_stream } 15 | let(:output) { out_stream.read } 16 | 17 | describe "#transmit" do 18 | def stub_marker_request 19 | stub_api_request config, "markers", marker.marker_data 20 | end 21 | 22 | def run 23 | capture_stdout(out_stream) { marker.transmit } 24 | end 25 | 26 | context "when request is valid" do 27 | before { stub_marker_request.to_return(:status => 200) } 28 | 29 | it "outputs success" do 30 | run 31 | expect(output).to include \ 32 | "Notifying AppSignal of 'production' deploy with revision: 503ce0923ed177a3ce000005, " \ 33 | "user: batman", 34 | "AppSignal has been notified of this deploy!" 35 | end 36 | end 37 | 38 | context "when request is invalid" do 39 | before { stub_marker_request.to_return(:status => 500) } 40 | 41 | it "outputs failure" do 42 | run 43 | expect(output).to include \ 44 | "Notifying AppSignal of 'production' deploy with revision: 503ce0923ed177a3ce000005, " \ 45 | "user: batman", 46 | "Something went wrong while trying to notify AppSignal: 500 at " \ 47 | "#{config[:endpoint]}/1/markers" 48 | expect(output).to_not include \ 49 | "AppSignal has been notified of this deploy!" 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/lib/appsignal/rack/hanami_middleware_spec.rb: -------------------------------------------------------------------------------- 1 | require "appsignal/rack/hanami_middleware" 2 | 3 | if DependencyHelper.hanami2_present? 4 | describe Appsignal::Rack::HanamiMiddleware do 5 | let(:app) { double(:call => true) } 6 | let(:router_params) { nil } 7 | let(:env) do 8 | options = {} 9 | options["router.params"] = router_params if router_params 10 | Rack::MockRequest.env_for( 11 | "/some/path", 12 | options 13 | ) 14 | end 15 | let(:middleware) { Appsignal::Rack::HanamiMiddleware.new(app, {}) } 16 | 17 | before { start_agent } 18 | around { |example| keep_transactions { example.run } } 19 | 20 | def make_request(env) 21 | if DependencyHelper.hanami2_2_present? 22 | instance = 23 | Class.new do 24 | def self.name 25 | "HanamiApp::Actions::Books::Index" 26 | end 27 | end.new 28 | env["hanami.action_instance"] = instance 29 | end 30 | middleware.call(env) 31 | end 32 | 33 | context "without params" do 34 | it "sets no request parameters on the transaction" do 35 | make_request(env) 36 | 37 | expect(last_transaction).to_not include_params 38 | end 39 | end 40 | 41 | context "with params" do 42 | let(:router_params) { { "param1" => "value1", "param2" => "value2" } } 43 | 44 | it "sets request parameters on the transaction" do 45 | make_request(env) 46 | 47 | expect(last_transaction).to include_params("param1" => "value1", "param2" => "value2") 48 | end 49 | end 50 | 51 | it "reports a process_action.hanami event" do 52 | make_request(env) 53 | 54 | expect(last_transaction).to include_event("name" => "process_action.hanami") 55 | end 56 | 57 | if DependencyHelper.hanami2_2_present? 58 | it "sets action name on the transaction" do 59 | make_request(env) 60 | 61 | expect(last_transaction).to have_action("HanamiApp::Actions::Books::Index") 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/lib/appsignal/rack/instrumentation_middleware_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Rack::InstrumentationMiddleware do 2 | let(:app) { DummyApp.new } 3 | let(:env) { Rack::MockRequest.env_for("/some/path") } 4 | let(:middleware) { described_class.new(app, {}) } 5 | 6 | before { start_agent } 7 | around { |example| keep_transactions { example.run } } 8 | 9 | def make_request(env) 10 | middleware.call(env) 11 | end 12 | 13 | context "without an exception" do 14 | it "reports a process_request_middleware.rack event" do 15 | make_request(env) 16 | 17 | expect(last_transaction).to include_event("name" => "process_request_middleware.rack") 18 | end 19 | end 20 | 21 | context "with custom action name" do 22 | let(:app) { DummyApp.new { |_env| Appsignal.set_action("MyAction") } } 23 | 24 | it "reports the custom action name" do 25 | make_request(env) 26 | 27 | expect(last_transaction).to have_action("MyAction") 28 | end 29 | end 30 | 31 | context "without action name metadata" do 32 | it "reports no action name" do 33 | make_request(env) 34 | 35 | expect(last_transaction).to_not have_action 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/lib/appsignal/utils/integration_logger_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Utils::IntegrationLogger do 2 | let(:log_stream) { std_stream } 3 | let(:logs) { log_contents(log_stream) } 4 | let(:logger) do 5 | Appsignal::Utils::IntegrationLogger.new(log_stream).tap do |l| 6 | l.formatter = logger_formatter 7 | end 8 | end 9 | 10 | it "logs messages" do 11 | logger.debug("debug message") 12 | logger.info("info message") 13 | logger.warn("warning message") 14 | logger.error("error message") 15 | 16 | expect(logs).to contains_log(:debug, "debug message") 17 | expect(logs).to contains_log(:info, "info message") 18 | expect(logs).to contains_log(:warn, "warning message") 19 | expect(logs).to contains_log(:error, "error message") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/lib/appsignal/utils/json_spec.rb: -------------------------------------------------------------------------------- 1 | describe Appsignal::Utils::JSON do 2 | describe ".generate" do 3 | subject { Appsignal::Utils::JSON.generate(body) } 4 | 5 | context "with a valid body" do 6 | let(:body) do 7 | { 8 | "the" => "payload", 9 | 1 => true, 10 | nil => "test", 11 | :foo => [1, 2, "three"], 12 | "bar" => nil, 13 | "baz" => { "foo" => "bar" } 14 | } 15 | end 16 | 17 | it "returns a JSON string" do 18 | is_expected.to eq %({"the":"payload","1":true,"":"test",) + 19 | %("foo":[1,2,"three"],"bar":null,"baz":{"foo":"bar"}}) 20 | end 21 | end 22 | 23 | context "with a body that contains strings with invalid UTF-8 content" do 24 | let(:string_with_invalid_utf8) { [0x61, 0x61, 0x85].pack("c*") } 25 | let(:body) do 26 | { 27 | "field_one" => [0x61, 0x61].pack("c*"), 28 | :field_two => string_with_invalid_utf8, 29 | "field_three" => [ 30 | "one", string_with_invalid_utf8 31 | ], 32 | "field_four" => { 33 | "one" => string_with_invalid_utf8 34 | } 35 | } 36 | end 37 | 38 | it "returns a JSON string with invalid UTF-8 content" do 39 | is_expected.to eq %({"field_one":"aa","field_two":"aa�",) + 40 | %("field_three":["one","aa�"],"field_four":{"one":"aa�"}}) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/support/fixtures/generated_config.yml: -------------------------------------------------------------------------------- 1 | default: &defaults 2 | # Your push api key, it is possible to set this dynamically using ERB: 3 | # push_api_key: "<%= ENV['APPSIGNAL_PUSH_API_KEY'] %>" 4 | push_api_key: "my_app_key" 5 | 6 | # Your app's name 7 | name: "MyApp" 8 | 9 | # The cuttoff point in ms above which a request is considered slow, default is 200 10 | # slow_request_threshold: 200 11 | 12 | # Actions that should not be monitored by AppSignal 13 | # ignore_actions: 14 | # - ApplicationController#isup 15 | 16 | # Configuration per environment, leave out an environment or set active 17 | # to false to not push metrics for that environment. 18 | development: 19 | <<: *defaults 20 | active: true 21 | 22 | production: 23 | <<: *defaults 24 | active: true 25 | -------------------------------------------------------------------------------- /spec/support/fixtures/projects/broken/config/appsignal.yml: -------------------------------------------------------------------------------- 1 | <%= ENV.fetch("I AM A KEY THAT DOES NOT EXIST") %> 2 | -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid/config/appsignal.yml: -------------------------------------------------------------------------------- 1 | default: &defaults 2 | push_api_key: "abc" 3 | name: "TestApp" 4 | enable_minutely_probes: false 5 | 6 | production: 7 | <<: *defaults 8 | active: true 9 | 10 | development: 11 | <<: *defaults 12 | active: true 13 | 14 | test: 15 | <<: *defaults 16 | endpoint: "http://localhost:3000" 17 | log_level: debug 18 | active: true 19 | 20 | old_config: 21 | api_key: "def" 22 | active: true 23 | ignore_exceptions: 24 | - StandardError 25 | request_headers: [ 26 | "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING", 27 | "HTTP_ACCEPT_LANGUAGE", "HTTP_CACHE_CONTROL", "HTTP_CONNECTION", 28 | "CONTENT_LENGTH", "PATH_INFO", "HTTP_RANGE", "HTTP_REFERER", 29 | "REQUEST_METHOD", "REQUEST_PATH", "SERVER_NAME", "SERVER_PORT", 30 | "SERVER_PROTOCOL", "HTTP_USER_AGENT" 31 | ] 32 | 33 | old_config_mixed_with_new_config: 34 | push_api_key: "ghi" 35 | api_key: "def" 36 | active: true 37 | ignore_errors: 38 | - NoMethodError 39 | ignore_exceptions: 40 | - StandardError 41 | request_headers: [ 42 | "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING", 43 | "HTTP_ACCEPT_LANGUAGE", "HTTP_CACHE_CONTROL", "HTTP_CONNECTION", 44 | "CONTENT_LENGTH", "PATH_INFO", "HTTP_RANGE", "HTTP_REFERER", 45 | "REQUEST_METHOD", "REQUEST_PATH", "SERVER_NAME", "SERVER_PORT", 46 | "SERVER_PROTOCOL", "HTTP_USER_AGENT" 47 | ] 48 | 49 | rack_env: 50 | <<: *defaults 51 | 52 | rails_env: 53 | <<: *defaults 54 | 55 | inactive_env: 56 | <<: *defaults 57 | active: false 58 | -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appsignal/appsignal-ruby/ef8021b965d486bf65931f261a9d0828ebb6b03d/spec/support/fixtures/projects/valid/log/.gitkeep -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid_with_rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require "rails" 2 | 3 | module MyApp 4 | class Application < Rails::Application 5 | config.active_support.deprecation = proc { |message, stack| } 6 | config.eager_load = false 7 | 8 | def self.initialize! 9 | # Prevent errors about Rails being initialized more than once 10 | return if defined?(@initialized) 11 | 12 | super 13 | @initialized = true 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid_with_rails_app/config/appsignal.yml: -------------------------------------------------------------------------------- 1 | default: &defaults 2 | push_api_key: "abc" 3 | name: "TestApp" 4 | enable_minutely_probes: false 5 | 6 | production: 7 | <<: *defaults 8 | active: true 9 | 10 | development: 11 | <<: *defaults 12 | active: true 13 | 14 | test: 15 | <<: *defaults 16 | log_level: debug 17 | active: true 18 | 19 | old_config: 20 | api_key: "def" 21 | active: true 22 | ignore_exceptions: 23 | - StandardError 24 | request_headers: [ 25 | "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING", 26 | "HTTP_ACCEPT_LANGUAGE", "HTTP_CACHE_CONTROL", "HTTP_CONNECTION", 27 | "CONTENT_LENGTH", "PATH_INFO", "HTTP_RANGE", "HTTP_REFERER", 28 | "REQUEST_METHOD", "REQUEST_PATH", "SERVER_NAME", "SERVER_PORT", 29 | "SERVER_PROTOCOL", "HTTP_USER_AGENT" 30 | ] 31 | 32 | old_config_mixed_with_new_config: 33 | push_api_key: "ghi" 34 | api_key: "def" 35 | active: true 36 | ignore_errors: 37 | - NoMethodError 38 | ignore_exceptions: 39 | - StandardError 40 | request_headers: [ 41 | "HTTP_ACCEPT", "HTTP_ACCEPT_CHARSET", "HTTP_ACCEPT_ENCODING", 42 | "HTTP_ACCEPT_LANGUAGE", "HTTP_CACHE_CONTROL", "HTTP_CONNECTION", 43 | "CONTENT_LENGTH", "PATH_INFO", "HTTP_RANGE", "HTTP_REFERER", 44 | "REQUEST_METHOD", "REQUEST_PATH", "SERVER_NAME", "SERVER_PORT", 45 | "SERVER_PROTOCOL", "HTTP_USER_AGENT" 46 | ] 47 | 48 | rack_env: 49 | <<: *defaults 50 | 51 | rails_env: 52 | <<: *defaults 53 | 54 | inactive_env: 55 | <<: *defaults 56 | active: false 57 | -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid_with_rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | MyApp::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid_with_rails_app/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appsignal/appsignal-ruby/ef8021b965d486bf65931f261a9d0828ebb6b03d/spec/support/fixtures/projects/valid_with_rails_app/log/.gitkeep -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid_with_rails_app_with_config_rb/config/application.rb: -------------------------------------------------------------------------------- 1 | require "rails" 2 | 3 | module MyApp 4 | class Application < Rails::Application 5 | config.active_support.deprecation = proc { |message, stack| } 6 | config.eager_load = false 7 | 8 | def self.initialize! 9 | # Prevent errors about Rails being initialized more than once 10 | return if defined?(@initialized) 11 | 12 | super 13 | @initialized = true 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid_with_rails_app_with_config_rb/config/appsignal.rb: -------------------------------------------------------------------------------- 1 | Appsignal.configure do |config| 2 | config.activate_if_environment(:production, :development, :test) 3 | config.name = "TestApp" 4 | config.push_api_key = "abc" 5 | config.enable_minutely_probes = false 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid_with_rails_app_with_config_rb/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | MyApp::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/support/fixtures/projects/valid_with_rails_app_with_config_rb/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appsignal/appsignal-ruby/ef8021b965d486bf65931f261a9d0828ebb6b03d/spec/support/fixtures/projects/valid_with_rails_app_with_config_rb/log/.gitkeep -------------------------------------------------------------------------------- /spec/support/fixtures/uploaded_file.txt: -------------------------------------------------------------------------------- 1 | 123456 2 | -------------------------------------------------------------------------------- /spec/support/hanami/hanami_app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami" 4 | require "hanami/action" 5 | 6 | module HanamiApp 7 | class App < Hanami::App 8 | end 9 | 10 | class Routes < Hanami::Routes 11 | get "/books", :to => "books.index" 12 | end 13 | 14 | module Actions 15 | module Books 16 | class Index < Hanami::Action 17 | def handle(_request, response) 18 | response.body = "YOU REQUESTED BOOKS!" 19 | end 20 | end 21 | 22 | class Error < Hanami::Action 23 | def handle(_request, _response) 24 | raise ExampleException, "exception message" 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/support/helpers/action_mailer_helpers.rb: -------------------------------------------------------------------------------- 1 | module ActionMailerHelpers 2 | def perform_action_mailer(mailer, method, args = nil) 3 | if DependencyHelper.rails_version >= Gem::Version.new("5.2.0") 4 | case args 5 | when Array 6 | mailer.send(method, *args).deliver_later 7 | when Hash 8 | mailer.with(args).send(method).deliver_later 9 | when NilClass 10 | mailer.send(method).deliver_later 11 | else 12 | raise "Unknown scenario for arguments: #{args}" 13 | end 14 | else 15 | # Rails 5.1 and lower 16 | mailer_object = 17 | if args 18 | mailer.send(method, *args) 19 | else 20 | mailer.send(method) 21 | end 22 | mailer_object.deliver_later 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/support/helpers/activejob_helpers.rb: -------------------------------------------------------------------------------- 1 | module ActiveJobHelpers 2 | def active_job_args_wrapper(args: [], params: nil) 3 | if DependencyHelper.active_job_wraps_args? 4 | wrapped_args = {} 5 | 6 | if params 7 | if DependencyHelper.rails7_present? 8 | wrapped_args["_aj_ruby2_keywords"] = ["params", "args"] 9 | wrapped_args["args"] = [] 10 | wrapped_args["params"] = { 11 | "_aj_symbol_keys" => ["foo"] 12 | }.merge(params) 13 | else 14 | wrapped_args["_aj_symbol_keys"] = ["foo"] 15 | wrapped_args.merge!(params) 16 | end 17 | else 18 | wrapped_args["_aj_ruby2_keywords"] = ["args"] 19 | wrapped_args["args"] = args 20 | end 21 | 22 | [wrapped_args] 23 | else 24 | params.nil? ? args : args + [params] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/helpers/cli_helpers.rb: -------------------------------------------------------------------------------- 1 | require "appsignal/cli/helpers" 2 | 3 | module CLIHelpers 4 | def cli 5 | Appsignal::CLI 6 | end 7 | 8 | def run_cli(command, options = {}) 9 | cli.run(format_cli_arguments_and_options(command, options)) 10 | end 11 | 12 | def format_cli_arguments_and_options(command, options = {}) 13 | [*command].tap do |o| 14 | options.each do |key, value| 15 | o << (value.nil? ? "--#{key}" : "--#{key}=#{value}") 16 | end 17 | end 18 | end 19 | 20 | def add_cli_input(value) 21 | $stdin.puts value 22 | end 23 | 24 | def prepare_cli_input 25 | # Prepare the input by rewinding the pointer in the StringIO 26 | $stdin.rewind 27 | end 28 | 29 | def colorize(*args) 30 | ColorizeHelper.colorize(*args) 31 | end 32 | end 33 | 34 | module ColorizeHelper 35 | extend Appsignal::CLI::Helpers 36 | 37 | def self.colorize(*_args) 38 | super 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/support/helpers/config_helpers.rb: -------------------------------------------------------------------------------- 1 | module ConfigHelpers 2 | def project_fixture_path 3 | File.expand_path( 4 | File.join(File.dirname(__FILE__), "../fixtures/projects/valid") 5 | ) 6 | end 7 | module_function :project_fixture_path 8 | 9 | def rails_project_fixture_path 10 | File.expand_path( 11 | File.join(File.dirname(__FILE__), "../fixtures/projects/valid_with_rails_app") 12 | ) 13 | end 14 | module_function :rails_project_fixture_path 15 | 16 | def rails_project_with_config_rb_fixture_path 17 | File.expand_path( 18 | File.join(File.dirname(__FILE__), "../fixtures/projects/valid_with_rails_app_with_config_rb") 19 | ) 20 | end 21 | module_function :rails_project_fixture_path 22 | 23 | def build_config( 24 | root_path: project_fixture_path, 25 | env: "production", 26 | options: {} 27 | ) 28 | Appsignal::Config.new( 29 | root_path, 30 | env 31 | ).tap do |c| 32 | # Mimics {Appsignal._load_config!} order 33 | c.merge_dsl_options(options) if options.any? 34 | c.apply_overrides 35 | c.validate 36 | end 37 | end 38 | module_function :build_config 39 | 40 | def configure(env: :default, root_path: nil, options: {}) 41 | env = "production" if env == :default 42 | env ||= "production" 43 | Appsignal.configure(env, :root_path => root_path || project_fixture_path) do |config| 44 | options.each do |option, value| 45 | config.send("#{option}=", value) 46 | end 47 | end 48 | end 49 | 50 | def start_agent( 51 | env: "production", 52 | root_path: nil, 53 | options: {}, 54 | internal_logger: nil 55 | ) 56 | configure(:env => env, :root_path => root_path, :options => options) 57 | Appsignal.start 58 | Appsignal.internal_logger = internal_logger if internal_logger 59 | end 60 | 61 | def clear_integration_env_vars! 62 | ENV.delete("RAILS_ENV") 63 | ENV.delete("RACK_ENV") 64 | ENV.delete("PADRINO_ENV") 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/support/helpers/directory_helper.rb: -------------------------------------------------------------------------------- 1 | module DirectoryHelper 2 | module_function 3 | 4 | def project_dir 5 | @project_dir ||= File.expand_path("..", spec_dir) 6 | end 7 | 8 | def spec_dir 9 | APPSIGNAL_SPEC_DIR 10 | end 11 | 12 | def support_dir 13 | @support_dir ||= File.join(spec_dir, "support") 14 | end 15 | 16 | def tmp_dir 17 | @tmp_dir ||= File.join(spec_dir, "tmp") 18 | end 19 | 20 | def write_file(path, contents) 21 | parent_dir = File.dirname(path) 22 | FileUtils.mkdir_p(parent_dir) 23 | File.write(path, contents) 24 | end 25 | 26 | def fixtures_dir 27 | @fixtures_dir ||= File.join(support_dir, "fixtures") 28 | end 29 | 30 | def resources_dir 31 | @resources_dir ||= File.join(project_dir, "resources") 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/helpers/env_helpers.rb: -------------------------------------------------------------------------------- 1 | module EnvHelpers 2 | def http_request_env_with_data(args = {}) 3 | with_queue_start = args.delete(:with_queue_start) 4 | path = args.delete(:path) || "/blog" 5 | request = Rack::MockRequest.env_for( 6 | path, 7 | :params => args[:params] || { 8 | "controller" => "blog_posts", 9 | "action" => "show", 10 | "id" => "1" 11 | } 12 | ).merge( 13 | :controller => "BlogPostsController", 14 | :action => "show", 15 | :request_format => "html", 16 | :request_method => "GET", 17 | :status => "200", 18 | :view_runtime => 500, 19 | :db_runtime => 500, 20 | :metadata => { :key => "value" } 21 | ).merge(args) 22 | 23 | # Set default queue value 24 | if with_queue_start 25 | request["HTTP_X_QUEUE_START"] = "t=#{(fixed_time * 1_000).to_i}" # in milliseconds 26 | end 27 | 28 | request 29 | end 30 | 31 | def background_env_with_data(args = {}) 32 | { 33 | :class => "BackgroundJob", 34 | :method => "perform", 35 | :priority => 1, 36 | :attempts => 0, 37 | :queue => "default", 38 | :queue_start => fixed_time 39 | }.merge(args) 40 | end 41 | 42 | def set_rails_session_data(request, data) 43 | ActionDispatch::Request::Session.create( 44 | rails_session_store(data), 45 | request, 46 | {} 47 | ) 48 | end 49 | 50 | def rails_session_store(data) 51 | Class.new do 52 | def initialize(data) 53 | @data = data 54 | end 55 | 56 | def load_session(_env) 57 | [1, @data] 58 | end 59 | 60 | def session_exists?(_env) 61 | true 62 | end 63 | 64 | def delete_session(_env, _id, _options) 65 | 123 66 | end 67 | end.new(data) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/support/helpers/environment_metdata_helper.rb: -------------------------------------------------------------------------------- 1 | module EnvironmentMetadataHelper 2 | def capture_environment_metadata_report_calls 3 | allow(Appsignal::Extension).to receive(:set_environment_metadata) 4 | .and_call_original 5 | end 6 | 7 | def expect_environment_metadata(key, value) 8 | expect(Appsignal::Extension).to have_received(:set_environment_metadata) 9 | .with(key, value) 10 | end 11 | 12 | def expect_not_environment_metadata(key) 13 | expect(Appsignal::Extension).to_not have_received(:set_environment_metadata) 14 | .with(key, anything) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/helpers/example_exception.rb: -------------------------------------------------------------------------------- 1 | # This ExampleException is used for throwing Exceptions in specs that are 2 | # allowed or expected. 3 | # 4 | # For example, this error can be thrown to raise an exception in AppSignal's 5 | # run, which should stop the program and the appsignal gem, but not crash the 6 | # test suite. 7 | # 8 | # There's also {ExampleStandardError}, use this when you need to test against 9 | # StandardError-level Ruby exceptions. 10 | # 11 | # @see ExampleStandardError 12 | class ExampleException < Exception # rubocop:disable Lint/InheritException 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/helpers/example_standard_error.rb: -------------------------------------------------------------------------------- 1 | # This ExampleStandardError is used for throwing errors in specs that are 2 | # allowed or expected. 3 | # 4 | # For example, this error can be thrown to raise an exception in AppSignal's 5 | # run, which should stop the program and the appsignal gem, but not crash the 6 | # test suite. 7 | # 8 | # There's also {ExampleException}, use this when you need to test against 9 | # Exception-level Ruby exceptions. 10 | # 11 | # @see ExampleException 12 | class ExampleStandardError < StandardError 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/helpers/loader_helper.rb: -------------------------------------------------------------------------------- 1 | module LoaderHelper 2 | def load_loader(name) 3 | Appsignal.load(name) 4 | end 5 | 6 | def start_loader(name) 7 | Appsignal::Loaders.instances.fetch(name).on_start 8 | end 9 | 10 | def unregister_loader(name) 11 | Appsignal::Loaders.unregister(name) 12 | end 13 | 14 | def define_loader(name, &block) 15 | Appsignal::Testing.registered_loaders << name 16 | Class.new(Appsignal::Loaders::Loader) do 17 | register name 18 | class_eval(&block) if block_given? 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/helpers/log_helpers.rb: -------------------------------------------------------------------------------- 1 | module LogHelpers 2 | def capture_logs(&block) 3 | log = std_stream 4 | use_logger_with(log, &block) 5 | log_contents(log) 6 | end 7 | 8 | def use_logger_with(log) 9 | Appsignal.internal_logger = test_logger(log) 10 | yield 11 | ensure 12 | Appsignal.internal_logger = nil 13 | end 14 | 15 | def test_logger(log) 16 | Appsignal::Utils::IntegrationLogger.new(log).tap do |logger| 17 | logger.formatter = logger_formatter 18 | end 19 | end 20 | 21 | def logger_formatter 22 | proc do |severity, _datetime, _progname, msg| 23 | log_line(severity, msg) 24 | end 25 | end 26 | 27 | def log_line(severity, message) 28 | # This format is used in the `contains_log` matcher. 29 | "[#{severity}] #{message}\n" 30 | end 31 | 32 | def log_contents(log) 33 | log.rewind 34 | log.read 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/support/helpers/rails_helper.rb: -------------------------------------------------------------------------------- 1 | module RailsHelper 2 | def run_appsignal_railtie 3 | app = MyApp::Application.new 4 | Appsignal::Integrations::Railtie.initializers.each do |initializer| 5 | initializer.run(app) 6 | end 7 | ActiveSupport.run_load_hooks(:after_initialize, app) 8 | end 9 | 10 | def with_rails_error_reporter 11 | if Rails.respond_to? :error 12 | clear_rails_error_reporter! 13 | Appsignal::Integrations::Railtie.initialize_error_reporter 14 | end 15 | yield 16 | ensure 17 | clear_rails_error_reporter! 18 | end 19 | 20 | def clear_rails_error_reporter! 21 | return unless Rails.respond_to? :error 22 | 23 | Rails 24 | .error 25 | .instance_variable_get(:@subscribers) 26 | .reject! { |s| s == Appsignal::Integrations::RailsErrorReporterSubscriber } 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/support/helpers/system_helpers.rb: -------------------------------------------------------------------------------- 1 | module SystemHelpers 2 | def recognize_as_heroku 3 | ENV["DYNO"] = "dyno1" 4 | value = yield 5 | ENV.delete "DYNO" 6 | value 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/support/helpers/take_at_most_helper.rb: -------------------------------------------------------------------------------- 1 | module TakeAtMostHelper 2 | # Assert that it takes at most a certain amount of time to run a block. 3 | # 4 | # @example 5 | # # Assert that it takes at most 1 second to run the block 6 | # take_at_most(1) { sleep 0.5 } 7 | # 8 | # @param time [Integer, Float] The maximum amount of time the block is allowed to 9 | # run in seconds. 10 | # @yield Block to run. 11 | # @raise [StandardError] Raises error if the block takes longer than the 12 | # specified time to run. 13 | def take_at_most(time) 14 | start = Time.now 15 | yield 16 | elapsed = Time.now - start 17 | return if elapsed <= time 18 | 19 | raise "Expected block to take at most #{time} seconds, but took #{elapsed}" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/helpers/time_helpers.rb: -------------------------------------------------------------------------------- 1 | module TimeHelpers 2 | def fixed_time 3 | @fixed_time ||= Time.utc(2014, 1, 15, 11, 0, 0).to_f 4 | end 5 | 6 | def advance_frozen_time(time, addition) 7 | Time.at(time.to_f + addition).tap do |new_time| 8 | Timecop.freeze(new_time) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/helpers/wait_for_helper.rb: -------------------------------------------------------------------------------- 1 | module WaitForHelper 2 | # Wait for a condition to be met 3 | # 4 | # @example 5 | # # Perform threaded operation 6 | # wait_for("enough probe calls") { probe.calls >= 2 } 7 | # # Assert on result 8 | # 9 | # @param name [String] The name of the condition to check. Used in the 10 | # error when it fails. 11 | # @yield Assertion to check. 12 | # @yieldreturn [Boolean] True/False value that indicates if the condition 13 | # is met. 14 | # @raise [StandardError] Raises error if the condition is not met after 5 15 | # seconds, 5_000 tries. 16 | def wait_for(name) 17 | max_wait = 5_000 18 | i = 0 19 | error = nil 20 | while i < max_wait 21 | begin 22 | result = yield 23 | break if result 24 | rescue Exception => e # rubocop:disable Lint/RescueException 25 | # Capture error so we know if it exited with an error 26 | error = e 27 | ensure 28 | i += 1 29 | sleep 0.001 30 | end 31 | end 32 | 33 | return unless i >= max_wait 34 | 35 | error_message = 36 | ("\nError: #{error.class}: #{error.message}\n#{error.backtrace.join("\n")}" if error) 37 | raise "Waited 5 seconds for #{name} condition, but was not met.#{error_message}" 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/support/matchers/contains_log.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :contains_log do |level, message| 2 | log_level_prefix = level.upcase 3 | 4 | match do |actual| 5 | case message 6 | when Regexp 7 | /\[#{log_level_prefix}\] #{message}/.match?(actual) 8 | else 9 | expected_log_line = "[#{log_level_prefix}] #{message}" 10 | actual.include?(expected_log_line) 11 | end 12 | end 13 | 14 | failure_message do |actual| 15 | <<~MESSAGE 16 | Did not contain log line: 17 | Log level: #{log_level_prefix} 18 | Message: #{message} 19 | 20 | Received logs: 21 | #{actual} 22 | MESSAGE 23 | end 24 | 25 | diffable 26 | end 27 | -------------------------------------------------------------------------------- /spec/support/matchers/have_colorized_text.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :have_colorized_text do |color, text| 2 | match do |actual| 3 | color_codes = Appsignal::CLI::Helpers::COLOR_CODES 4 | reset_color_code = color_codes.fetch(:default) 5 | color_code = color_codes.fetch(color) 6 | 7 | @expected = "\e[#{color_code}m#{text}\e[#{reset_color_code}m" 8 | expect(actual).to include(@expected) 9 | end 10 | 11 | diffable 12 | attr_reader :expected 13 | end 14 | 15 | COLOR_TAG_MATCHER_REGEX = /\e\[(\d+)m/.freeze 16 | RSpec::Matchers.define :have_color_markers do 17 | match do |actual| 18 | actual =~ COLOR_TAG_MATCHER_REGEX 19 | end 20 | 21 | failure_message do 22 | "expected that output contains color markers: /\\e[\\d+m/" 23 | end 24 | 25 | failure_message_when_negated do 26 | "expected that output does not contain color markers: /\\e[\\d+m/" 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/support/mocks/appsignal_mock.rb: -------------------------------------------------------------------------------- 1 | class AppsignalMock 2 | attr_reader :gauges 3 | 4 | def initialize(hostname: nil) 5 | @hostname = hostname 6 | @gauges = [] 7 | end 8 | 9 | def config 10 | options = {} 11 | options[:hostname] = @hostname if @hostname 12 | ConfigHelpers.build_config(:options => options) 13 | end 14 | 15 | def set_gauge(*args) 16 | @gauges << args 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/mocks/dummy_app.rb: -------------------------------------------------------------------------------- 1 | class DummyApp 2 | def initialize(&app) 3 | @app = app 4 | @called = false 5 | end 6 | 7 | def call(env) 8 | if @app 9 | @app&.call(env) 10 | else 11 | [200, {}, ["body"]] 12 | end 13 | ensure 14 | @called = true 15 | end 16 | 17 | def called? 18 | @called 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/support/mocks/fake_gc_profiler.rb: -------------------------------------------------------------------------------- 1 | class FakeGCProfiler 2 | attr_accessor :total_time 3 | attr_writer :clear_delay 4 | 5 | def initialize(total_time = 0) 6 | @total_time = total_time 7 | end 8 | 9 | def clear 10 | sleep clear_delay 11 | @total_time = 0 12 | end 13 | 14 | private 15 | 16 | def clear_delay 17 | @clear_delay ||= 0 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/mocks/fake_gvl_tools.rb: -------------------------------------------------------------------------------- 1 | module FakeGVLTools 2 | def self.reset 3 | self::GlobalTimer.monotonic_time = 0 4 | self::WaitingThreads.count = 0 5 | end 6 | 7 | module GlobalTimer 8 | @monotonic_time = 0 9 | 10 | class << self 11 | attr_accessor :monotonic_time 12 | end 13 | end 14 | 15 | module WaitingThreads 16 | @count = 0 17 | @enabled = false 18 | 19 | class << self 20 | attr_accessor :count 21 | attr_writer :enabled 22 | 23 | def enabled? 24 | @enabled 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/support/mocks/hash_like.rb: -------------------------------------------------------------------------------- 1 | class HashLike < Hash 2 | def initialize(value) 3 | super 4 | @value = value 5 | end 6 | 7 | def to_h 8 | @value 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/mocks/mock_probe.rb: -------------------------------------------------------------------------------- 1 | class MockProbe 2 | attr_reader :calls 3 | 4 | def initialize 5 | Appsignal::Testing.store[:mock_probe_call] = 0 6 | @calls = 0 7 | end 8 | 9 | def call 10 | Appsignal::Testing.store[:mock_probe_call] += 1 11 | @calls += 1 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/mocks/puma_mock.rb: -------------------------------------------------------------------------------- 1 | class PumaMock 2 | module MiniSSL 3 | class SSLError < StandardError 4 | def self.to_s 5 | "Puma::MiniSSL::SSLError" 6 | end 7 | end 8 | end 9 | 10 | class HttpParserError < StandardError 11 | def self.to_s 12 | "Puma::HttpParserError" 13 | end 14 | end 15 | 16 | class HttpParserError501 < StandardError 17 | def self.to_s 18 | "Puma::HttpParserError501" 19 | end 20 | end 21 | 22 | def self.stats 23 | end 24 | 25 | def self.cli_config 26 | @cli_config ||= CliConfig.new 27 | end 28 | 29 | class Server 30 | end 31 | 32 | module Const 33 | VERSION = "6.0.0".freeze 34 | end 35 | 36 | class CliConfig 37 | attr_accessor :options 38 | 39 | def initialize 40 | @options = {} 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/support/shared_examples/instrument.rb: -------------------------------------------------------------------------------- 1 | RSpec.shared_examples "instrument helper" do 2 | around { |example| keep_transactions { example.run } } 3 | let(:stub) { double(:method_call => "return value") } 4 | 5 | it "records an event around the given block" do 6 | return_value = instrumenter.instrument "name", "title", "body" do 7 | stub.method_call 8 | end 9 | expect(return_value).to eq "return value" 10 | 11 | expect_transaction_to_have_event 12 | end 13 | 14 | context "with an error raised in the passed block" do 15 | it "records an event around the given block" do 16 | expect do 17 | instrumenter.instrument "name", "title", "body" do 18 | stub.method_call 19 | raise ExampleException, "foo" 20 | end 21 | end.to raise_error(ExampleException, "foo") 22 | 23 | expect_transaction_to_have_event 24 | end 25 | end 26 | 27 | context "with an error raise in the passed block" do 28 | it "records an event around the given block" do 29 | expect do 30 | instrumenter.instrument "name", "title", "body" do 31 | stub.method_call 32 | throw :foo 33 | end 34 | end.to throw_symbol(:foo) 35 | 36 | expect_transaction_to_have_event 37 | end 38 | end 39 | 40 | def expect_transaction_to_have_event 41 | expect(transaction).to include_event( 42 | "name" => "name", 43 | "title" => "title", 44 | "body" => "body", 45 | "body_format" => Appsignal::EventFormatter::DEFAULT 46 | ) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/support/stubs/appsignal/loaders/loader_stub.rb: -------------------------------------------------------------------------------- 1 | module Appsignal 2 | module Loaders 3 | class LoaderStub < Loader 4 | register :loader_stub 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/stubs/delayed_job.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appsignal/appsignal-ruby/ef8021b965d486bf65931f261a9d0828ebb6b03d/spec/support/stubs/delayed_job.rb -------------------------------------------------------------------------------- /spec/support/stubs/sidekiq/api.rb: -------------------------------------------------------------------------------- 1 | module Sidekiq 2 | class Stats 3 | end 4 | end 5 | --------------------------------------------------------------------------------