diff --git a/.github/versions.yml b/.github/versions.yml index e2ae82ae91..57decf9bc4 100644 --- a/.github/versions.yml +++ b/.github/versions.yml @@ -1,8 +1,8 @@ --- # This file is consumed by lib/tasks/gha.rake ruby/setup-ruby: - :tag: v1.180.0 - :sha: ff740bc00a01b3a50fffc55a1071b1060eeae9dc + :tag: v1.186.0 + :sha: 2a9a743e19810b9f3c38060637daf594dbd7b37f actions/checkout: :tag: v4.1.2 :sha: 9bb56186c3b09b4f86b1c65136769dd318469633 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec55746139..2260625d45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - name: Configure git run: 'git config --global init.defaultBranch main' - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 - - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + - uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: '3.3' - run: bundle @@ -36,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: [2.4.10, 3.3.3] + ruby-version: [2.4.10, 3.3.4] steps: - name: Configure git run: 'git config --global init.defaultBranch main' @@ -49,7 +49,7 @@ jobs: run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libcurl4-nss-dev libsasl2-dev libxslt1-dev - name: Install Ruby ${{ matrix.ruby-version }} - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: ${{ matrix.ruby-version }} @@ -62,8 +62,8 @@ jobs: "2.4.10": { "rails": "norails,rails42,rails52" }, - "3.3.3": { - "rails": "norails,rails61,rails70" + "3.3.4": { + "rails": "norails,rails61,rails72" } } @@ -190,7 +190,7 @@ jobs: fail-fast: false matrix: multiverse: [agent, ai, background, background_2, database, frameworks, httpclients, httpclients_2, rails, rest] - ruby-version: [2.4.10, 3.3.3] + ruby-version: [2.4.10, 3.3.4] steps: - name: Configure git @@ -204,7 +204,7 @@ jobs: run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libcurl4-nss-dev libsasl2-dev libxslt1-dev - name: Install Ruby ${{ matrix.ruby-version }} - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: ${{ matrix.ruby-version }} @@ -290,14 +290,14 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: [2.7.8, 3.3.3] + ruby-version: [2.7.8, 3.3.4] steps: - name: Configure git run: 'git config --global init.defaultBranch main' - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 - name: Install Ruby ${{ matrix.ruby-version }} - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: ${{ matrix.ruby-version }} @@ -337,7 +337,7 @@ jobs: - name: Configure git run: 'git config --global init.defaultBranch main' - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 - - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + - uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: '3.3' - run: bundle diff --git a/.github/workflows/ci_cron.yml b/.github/workflows/ci_cron.yml index 5ea52835dc..f098e47082 100644 --- a/.github/workflows/ci_cron.yml +++ b/.github/workflows/ci_cron.yml @@ -16,7 +16,7 @@ jobs: - name: Configure git run: 'git config --global init.defaultBranch main' - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 - - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + - uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: '3.3' - run: bundle @@ -36,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: [2.4.10, 2.5.9, 2.6.10, 2.7.8, 3.0.7, 3.1.6, 3.2.4, 3.3.3, 3.4.0-preview1] + ruby-version: [2.4.10, 2.5.9, 2.6.10, 2.7.8, 3.0.7, 3.1.6, 3.2.4, 3.3.4, 3.4.0-preview1] steps: - name: Configure git @@ -50,7 +50,7 @@ jobs: run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libcurl4-nss-dev libsasl2-dev libxslt1-dev - name: Install Ruby ${{ matrix.ruby-version }} - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: ${{ matrix.ruby-version }} @@ -70,22 +70,22 @@ jobs: "rails": "norails,rails61,rails60,rails52,rails51,rails50,rails42" }, "2.7.8": { - "rails": "norails,rails61,rails60,rails70" + "rails": "norails,rails61,rails60,rails70,rails71" }, "3.0.7": { - "rails": "norails,rails61,rails60,rails70" + "rails": "norails,rails61,rails60,rails70,rails71" }, "3.1.6": { - "rails": "norails,rails61,rails70" + "rails": "norails,rails61,rails70,rails71,rails72,railsedge" }, "3.2.4": { - "rails": "norails,rails61,rails70" + "rails": "norails,rails61,rails70,rails71,rails72,railsedge" }, - "3.3.3": { - "rails": "norails,rails61,rails70" + "3.3.4": { + "rails": "norails,rails61,rails70,rails71,rails72,railsedge" }, "3.4.0-preview1": { - "rails": "norails,rails61,rails70" + "rails": "norails,rails61,rails70,rails71,rails72,railsedge" } } @@ -203,7 +203,7 @@ jobs: fail-fast: false matrix: multiverse: [agent, ai, background, background_2, database, frameworks, httpclients, httpclients_2, rails, rest] - ruby-version: [2.4.10, 2.5.9, 2.6.10, 2.7.8, 3.0.7, 3.1.6, 3.2.4, 3.3.3, 3.4.0-preview1] + ruby-version: [2.4.10, 2.5.9, 2.6.10, 2.7.8, 3.0.7, 3.1.6, 3.2.4, 3.3.4, 3.4.0-preview1] steps: - name: Configure git run: 'git config --global init.defaultBranch main' @@ -216,7 +216,7 @@ jobs: run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libcurl4-nss-dev libsasl2-dev libxslt1-dev - name: Install Ruby ${{ matrix.ruby-version }} - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: ${{ matrix.ruby-version }} @@ -281,14 +281,14 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: [2.7.8, 3.0.7, 3.1.6, 3.2.4, 3.3.3, 3.4.0-preview1] + ruby-version: [2.7.8, 3.0.7, 3.1.6, 3.2.4, 3.3.4, 3.4.0-preview1] steps: - name: Configure git run: 'git config --global init.defaultBranch main' - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 - name: Install Ruby ${{ matrix.ruby-version }} - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: ${{ matrix.ruby-version }} diff --git a/.github/workflows/ci_jruby.yml b/.github/workflows/ci_jruby.yml index 421977d1c9..1d592b4607 100644 --- a/.github/workflows/ci_jruby.yml +++ b/.github/workflows/ci_jruby.yml @@ -16,9 +16,9 @@ jobs: - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 - name: Install JRuby - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: - ruby-version: jruby-9.4.7.0 + ruby-version: jruby-9.4.8.0 - name: Bundle run: bundle install @@ -49,9 +49,9 @@ jobs: uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 - name: Install JRuby - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: - ruby-version: jruby-9.4.7.0 + ruby-version: jruby-9.4.8.0 - name: Bundle run: bundle install diff --git a/.github/workflows/ci_special.yml b/.github/workflows/ci_special.yml new file mode 100644 index 0000000000..7a5f61c49f --- /dev/null +++ b/.github/workflows/ci_special.yml @@ -0,0 +1,80 @@ +name: URL Test CI + +on: + schedule: + - cron: '0 9 * * *' + workflow_dispatch: + +jobs: + unit_tests: + runs-on: ubuntu-22.04 + + steps: + - name: Configure git + run: 'git config --global init.defaultBranch main' + - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 + + # - curl is needed for Curb + # - xslt is needed for older Nokogiris, RUBY_VERSION < 2.5 + # - sasl is needed for memcached + - name: Install OS packages + run: sudo apt-get update; sudo apt-get install -y --no-install-recommends libcurl4-nss-dev libsasl2-dev libxslt1-dev + + - name: Install Ruby 3.4.0-preview1 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 + with: + ruby-version: 3.4.0-preview1 + + - name: Setup bundler + run: ./.github/workflows/scripts/setup_bundler + env: + RUBY_VERSION: 3.4.0-preview1 + + - name: Run Unit Tests + uses: nick-fields/retry@7152eba30c6575329ac0576536151aca5a72780e # tag v3.0.0 + with: + timeout_minutes: 30 + max_attempts: 2 + command: TEST=test/new_relic/healthy_urls_test bundle exec rake test + env: + VERBOSE_TEST_OUTPUT: true + SPECIAL_CI: true + + + notify_slack_fail: + name: Notify slack fail + needs: [unit_tests] + runs-on: ubuntu-22.04 + if: always() + steps: + - uses: technote-space/workflow-conclusion-action@45ce8e0eb155657ab8ccf346ade734257fd196a5 # tag v3.0.3 + - uses: voxmedia/github-action-slack-notify-build@3665186a8c1a022b28a1dbe0954e73aa9081ea9e # tag v1.6.0 + if: ${{ env.WORKFLOW_CONCLUSION == 'failure' && github.event_name != 'workflow_dispatch' }} + env: + SLACK_BOT_TOKEN: ${{ secrets.RUBY_GITHUB_ACTIONS_BOT_WEBHOOK }} + with: + channel: ruby-agent-notifications + status: FAILED + color: danger + + + notify_slack_success: + name: Notify slack success + needs: [unit_tests] + runs-on: ubuntu-22.04 + if: always() + steps: + - uses: technote-space/workflow-conclusion-action@45ce8e0eb155657ab8ccf346ade734257fd196a5 # tag v3.0.3 + - run: echo ${{ github.event_name }} + - uses: Mercymeilya/last-workflow-status@3418710aefe8556d73b6f173a0564d38bcfd9a43 # tag v0.3.3 + id: last_status + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + - uses: voxmedia/github-action-slack-notify-build@3665186a8c1a022b28a1dbe0954e73aa9081ea9e # tag v1.6.0 + if: ${{ env.WORKFLOW_CONCLUSION == 'success' && steps.last_status.outputs.last_status == 'failure' && github.event_name != 'workflow_dispatch' }} + env: + SLACK_BOT_TOKEN: ${{ secrets.RUBY_GITHUB_ACTIONS_BOT_WEBHOOK }} + with: + channel: ruby-agent-notifications + status: SUCCESS + color: good diff --git a/.github/workflows/config_docs.yml b/.github/workflows/config_docs.yml index 3ac1fd65c7..37d9ecce52 100644 --- a/.github/workflows/config_docs.yml +++ b/.github/workflows/config_docs.yml @@ -15,7 +15,7 @@ jobs: pull-requests: write steps: - name: Install Ruby 3.3 - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: 3.3 diff --git a/.github/workflows/performance_tests.yml b/.github/workflows/performance_tests.yml index b3c25088bb..d46f7c6067 100644 --- a/.github/workflows/performance_tests.yml +++ b/.github/workflows/performance_tests.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 with: ref: 'main' - - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + - uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: '3.3' - run: bundle diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index fef0a5a6f0..eed97976e5 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -11,7 +11,7 @@ jobs: pull-requests: write steps: - name: Install Ruby 3.3 - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: 3.3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7887d8a83e..d545e12231 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: with: fetch-depth: 0 - - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + - uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: 3.3 diff --git a/.github/workflows/release_notes.yml b/.github/workflows/release_notes.yml index b0bf79887d..f2ab5c1b75 100644 --- a/.github/workflows/release_notes.yml +++ b/.github/workflows/release_notes.yml @@ -13,7 +13,7 @@ jobs: contents: write pull-requests: write steps: - - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + - uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: 3.3 - name: Checkout code diff --git a/.github/workflows/release_pr.yml b/.github/workflows/release_pr.yml index 9e68e4c333..75046b3856 100644 --- a/.github/workflows/release_pr.yml +++ b/.github/workflows/release_pr.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Install Ruby 3.3 - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: 3.3 diff --git a/.github/workflows/slack_notifications.yml b/.github/workflows/slack_notifications.yml index efdd74db92..ba1a28f672 100644 --- a/.github/workflows/slack_notifications.yml +++ b/.github/workflows/slack_notifications.yml @@ -8,7 +8,7 @@ jobs: gem_notifications: runs-on: ubuntu-22.04 steps: - - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + - uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: 3.3 - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 @@ -46,7 +46,7 @@ jobs: cve_notifications: runs-on: ubuntu-22.04 steps: - - uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # tag v1.180.0 + - uses: ruby/setup-ruby@2a9a743e19810b9f3c38060637daf594dbd7b37f # tag v1.186.0 with: ruby-version: 3.3 - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # tag v4.1.2 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b1f2816be7..a7d2e1dbcd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,12 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2023-05-18 21:20:20 UTC using RuboCop version 1.51.0. +# on 2023-10-26 22:54:31 UTC using RuboCop version 1.54.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 30 +# Offense count: 31 # Configuration parameters: EnforcedStyle, AllowedGems, Include. # SupportedStyles: Gemfile, gems.rb, gemspec # Include: **/*.gemspec, **/Gemfile, **/gems.rb @@ -15,15 +15,20 @@ Gemspec/DevelopmentDependencies: - 'infinite_tracing/newrelic-infinite_tracing.gemspec' - 'newrelic_rpm.gemspec' -# Offense count: 416 +# Offense count: 443 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 40 Exclude: + - 'lib/new_relic/agent/configuration/default_source.rb' - infinite_tracing/test/**/* - lib/new_relic/cli/commands/deployments.rb - test/**/* +Metrics/CollectionLiteralLength: + Exclude: + - 'lib/new_relic/agent/configuration/default_source.rb' + # Offense count: 7 Minitest/AssertRaisesCompoundBody: Exclude: @@ -37,7 +42,7 @@ Minitest/DuplicateTestRun: - 'test/multiverse/suites/rails/error_tracing_test.rb' - 'test/multiverse/suites/sinatra/ignoring_test.rb' -# Offense count: 276 +# Offense count: 284 Minitest/MultipleAssertions: Max: 28 @@ -45,7 +50,7 @@ Minitest/MultipleAssertions: Minitest/TestFileName: Enabled: false -# Offense count: 22 +# Offense count: 20 # This cop supports safe autocorrection (--autocorrect). Minitest/TestMethodName: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f6e1094d..64fd57650c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,79 @@ # New Relic Ruby Agent Release Notes +## v9.12.0 + +Version 9.12.0 adds support for the `newrelic_security` agent, introduces instrumentation for the LogStasher gem, improves instrumentation for the `redis-clustering` gem, and updates the Elasticsearch instrumentation to only attempt to get the cluster name once per client, even if it fails. + +- **Feature: Add support for the newrelic_security agent** + + [New Relic Interactive Application Security Testing (IAST)](https://docs.newrelic.com/docs/iast/introduction/) can help you prevent cyberattacks and breaches on your applications by probing your running code for exploitable vulnerabilities. + + The `newrelic_security` gem provides this feature for Ruby. It depends on `newrelic_rpm`. This is the first version of `newrelic_rpm` compatible with `newrelic_security`. + + At this time, the security agent is intended for use only within a dedicated security testing environment with data that can tolerate modification or deletion. The security agent is available as a separate Ruby gem, `newrelic_security`. It is recommended that this separate gem only be introduced to a security testing environment by leveraging Bundler grouping like so: + + ```ruby + # Gemfile + gem 'newrelic_rpm' # New Relic APM observability agent + gem 'newrelic-infinite_tracing' # New Relic Infinite Tracing + + group :security do + gem 'newrelic_security', require: false # New Relic security agent + end + ``` + + In order to run the security agent, you need to update your configuration. At a minimum, `security.agent.enabled` and `security.enabled` must be set to `true`. They are `false` by default. Similar to the gem installation, we recommend you set these configurations for a special security testing environment only. + + Here's an example using `newrelic.yml`: + + ```yaml + common: &default_settings + license_key: <%= ENV['NEW_RELIC_LICENSE_KEY'] %> + app_name: "Example app" + + development: + <<: *default_settings + app_name: <%= app_name %> (Development) + + security: + <<: *default_settings + security.enabled: true + security.agent.enabled: true + + production: + <<: *default_settings + ``` + + The following configuration relate to the `newrelic_security` gem: + + | Configuration name | Default | Behavior | + | ------------------ | ------- |----------| + | security.agent.enabled | `false` | If `true`, the security agent is loaded (a Ruby 'require' is performed) | + | security.enabled | `false` | If `true`, the security agent is started (the agent runs in its event loop) | + | security.mode | `'IAST'` | Defines the mode for the security agent to operate in. Currently only 'IAST' is supported | + | security.validator_service_url | `'wss://csec.nr-data.net'` | Defines the endpoint URL for posting security related data | + | security.detection.rci.enabled | `true` | If `true`, enables RCI (remote code injection) detection | + | security.detection.rxss.enabled | `true` | If `true`, enables RXSS (reflected cross-site scripting) detection | + | security.detection.deserialization.enabled | `true` | If `true`, enables deserialization detection | + | security.application_info.port | `nil` | An Integer representing the port the application is listening on. This setting is mandatory for Passenger servers. Other servers should be detected by default. | + +- **Feature: Add instrumentation for LogStasher** + + The agent will now record logs generated by [LogStasher](https://github.com/shadabahmed/logstasher). Versions 1.0.0 and above of the LogStasher gem are supported. [PR#2559](https://github.com/newrelic/newrelic-ruby-agent/pull/2559) + +- **Feature: Add instrumentation for redis-clustering** + + Version 5.x of the `redis` gem moved cluster behavior into a different gem, `redis-clustering`. This gem can access instrumentation registered through `RedisClient::Middleware`. Previously, the agent only instrumented the `call_pipelined` method through this approach, but now users of the `redis-clustering` gem will also have instrumentation registered for `connect` and `call` methods. In addition, the way the `database_name` attribute is set for Redis datastore spans is now compatible with all versions of Redis supported by the New Relic Ruby agent. Thank you, [@praveen-ks](https://github.com/praveen-ks) for bringing this to our attention. [Issue#2444](https://github.com/newrelic/newrelic-ruby-agent/issues/2444) [PR#2720](https://github.com/newrelic/newrelic-ruby-agent/pull/2720) + +- **Bugfix: Update Elasticsearch instrumentation to only attempt to get the cluster name once per client** + + Previously, the agent would attempt to get the cluster name every time a call was made if it was not already captured. This could lead to a large number of failures if the cluster name could not be retrieved. Now, the agent will only attempt to get the cluster name once per client, even if it fails. Thank you, [@ascoppa](https://github.com/ascoppa) for bringing this to our attention. [Issue#2730](https://github.com/newrelic/newrelic-ruby-agent/issues/2730) [PR#2743](https://github.com/newrelic/newrelic-ruby-agent/pull/2743) + +- **Feature: Produce metrics for 4 additional Action Controller Rails notifications** + + Four additional Action Controller related Rails notifications are now subscribed to by the agent to produce telemetry. These 4 are `exist_fragment?`, `expire_fragment`, `read_fragment`, and `write_fragment`. As with instrumentation for Action Controller itself, these notifications are enabled by default and can be disabled by setting `:disable_action_controller` to `true` in the agent's `newrelic.yml` configuration file. [PR#2745](https://github.com/newrelic/newrelic-ruby-agent/pull/2745) + + ## v9.11.0 Version 9.11.0 introduces instrumentation for the aws-sdk-sqs gem, fixes a bug related to expected errors not bearing a "true" value for the "expected" attribute if expected as a result of an HTTP status code match and changes the way Stripe instrumentation metrics are named to prevent high-cardinality issues. diff --git a/README.md b/README.md index 20d1d22195..bbdaf18c33 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ You can also monitor non-web applications. Refer to the "Other Environments" section below. We offer an AWS Lambda layer for instrumenting your serverless Ruby functions. -Details can be found on our [getting started guide](https://docs.newrelic.com/docs/serverless-function-monitoring/aws-lambda-monitoring/get-started/monitoring-aws-lambda-serverless-monitoring/). +Details can be found on our [getting started guide](https://docs.newrelic.com/docs/serverless-function-monitoring/aws-lambda-monitoring/instrument-lambda-function/instrument-your-own/). ## Installing and Using diff --git a/infinite_tracing/CHANGELOG.md b/infinite_tracing/CHANGELOG.md index a725414cb1..bd484a51f3 100644 --- a/infinite_tracing/CHANGELOG.md +++ b/infinite_tracing/CHANGELOG.md @@ -1,5 +1,9 @@ # New Relic Infinite Tracing for Ruby Agent Release Notes # + ## v9.12.0 + + * Pin google-protobuf dependency to < 4.0 due to compatibility issues with version 4+. + ## v8.9.0 * **Bugfix: Infinite Tracing hung on connection restart** diff --git a/infinite_tracing/newrelic-infinite_tracing.gemspec b/infinite_tracing/newrelic-infinite_tracing.gemspec index 729cbef34a..3c94f78297 100644 --- a/infinite_tracing/newrelic-infinite_tracing.gemspec +++ b/infinite_tracing/newrelic-infinite_tracing.gemspec @@ -73,6 +73,7 @@ Gem::Specification.new do |s| s.add_dependency 'newrelic_rpm', NewRelic::VERSION::STRING s.add_dependency 'grpc', '~> 1.34' + s.add_dependency 'google-protobuf', '< 4.0' s.add_development_dependency 'rake', '12.3.3' s.add_development_dependency 'rb-inotify' diff --git a/lib/new_relic/agent/agent_logger.rb b/lib/new_relic/agent/agent_logger.rb index 0bcda55cf6..90f94c3a29 100644 --- a/lib/new_relic/agent/agent_logger.rb +++ b/lib/new_relic/agent/agent_logger.rb @@ -4,6 +4,7 @@ require 'thread' require 'logger' +require 'singleton' require 'new_relic/agent/hostname' require 'new_relic/agent/log_once' require 'new_relic/agent/instrumentation/logger/instrumentation' diff --git a/lib/new_relic/agent/configuration/default_source.rb b/lib/new_relic/agent/configuration/default_source.rb index 367bc920ed..75a119d875 100644 --- a/lib/new_relic/agent/configuration/default_source.rb +++ b/lib/new_relic/agent/configuration/default_source.rb @@ -72,7 +72,7 @@ def self.transform_for(key) value_from_defaults(key, :transform) end - def self.config_search_paths # rubocop:disable Metrics/AbcSize + def self.config_search_paths proc { yaml = 'newrelic.yml' config_yaml = File.join('config', yaml) @@ -1112,7 +1112,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) :public => true, :type => Boolean, :allowed_from_server => true, - :description => "If `true`, the agent will report source code level metrics for traced methods.\nsee: " \ + :description => "If `true`, the agent will report source code level metrics for traced methods.\nSee: " \ 'https://docs.newrelic.com/docs/apm/agents/ruby-agent/features/ruby-codestream-integration/' }, # Cross application tracer @@ -1150,7 +1150,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) :allowed_from_server => true, :dynamic_name => true, :description => <<~DESC - * Specify a maximum number of custom events to buffer in memory at a time.' + * Specify a maximum number of custom events to buffer in memory at a time. * When configuring the agent for [AI monitoring](/docs/ai-monitoring/intro-to-ai-monitoring), \ set to max value `100000`. This ensures the agent captures the maximum amount of LLM events. DESC @@ -1444,6 +1444,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.async_http' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1452,6 +1453,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.bunny' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1468,6 +1470,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.dynamodb' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1476,6 +1479,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.fiber' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1484,6 +1488,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.concurrent_ruby' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1510,6 +1515,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.elasticsearch' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1518,6 +1524,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.ethon' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1535,6 +1542,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.grape' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1603,8 +1611,18 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) :allowed_from_server => false, :description => 'Controls auto-instrumentation of Ruby standard library Logger at start-up. May be one of: `auto`, `prepend`, `chain`, `disabled`.' }, + :'instrumentation.logstasher' => { + :default => instrumentation_value_from_boolean(:'application_logging.enabled'), + :documentation_default => 'auto', + :public => true, + :type => String, + :dynamic_name => true, + :allowed_from_server => false, + :description => 'Controls auto-instrumentation of the LogStasher library at start-up. May be one of: `auto`, `prepend`, `chain`, `disabled`.' + }, :'instrumentation.memcache' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1698,6 +1716,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.rake' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1706,6 +1725,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.redis' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1723,6 +1743,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.roda' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1731,6 +1752,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.sinatra' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1746,6 +1768,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.view_component' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1782,6 +1805,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.thread' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -1804,6 +1828,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) }, :'instrumentation.tilt' => { :default => 'auto', + :documentation_default => 'auto', :public => true, :type => String, :dynamic_name => true, @@ -2514,6 +2539,84 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) :type => Integer, :allowed_from_server => false, :description => 'This value represents the total amount of memory available to the host (not the process), in mebibytes (1024 squared or 1,048,576 bytes).' + }, + # security agent + :'security.agent.enabled' => { + :default => false, + :external => true, + :public => true, + :type => Boolean, + :allowed_from_server => false, + :description => "If `true`, the security agent is loaded (a Ruby 'require' is performed)" + }, + :'security.enabled' => { + :default => false, + :external => true, + :public => true, + :type => Boolean, + :allowed_from_server => false, + :description => 'If `true`, the security agent is started (the agent runs in its event loop)' + }, + :'security.mode' => { + :default => 'IAST', + :external => true, + :public => true, + :type => String, + :allowed_from_server => true, + :allowlist => %w[IAST RASP], + :description => 'Defines the mode for the security agent to operate in. Currently only `IAST` is supported', + :dynamic_name => true + }, + :'security.validator_service_url' => { + :default => 'wss://csec.nr-data.net', + :external => true, + :public => true, + :type => String, + :allowed_from_server => true, + :description => 'Defines the endpoint URL for posting security-related data', + :dynamic_name => true + }, + :'security.detection.rci.enabled' => { + :default => true, + :external => true, + :public => true, + :type => Boolean, + :allowed_from_server => false, + :description => 'If `true`, enables RCI (remote code injection) detection' + }, + :'security.detection.rxss.enabled' => { + :default => true, + :external => true, + :public => true, + :type => Boolean, + :allowed_from_server => false, + :description => 'If `true`, enables RXSS (reflected cross-site scripting) detection' + }, + :'security.detection.deserialization.enabled' => { + :default => true, + :external => true, + :public => true, + :type => Boolean, + :allowed_from_server => false, + :description => 'If `true`, enables deserialization detection' + }, + :'security.application_info.port' => { + :default => nil, + :allow_nil => true, + :public => true, + :type => Integer, + :external => true, + :allowed_from_server => false, + :description => 'The port the application is listening on. This setting is mandatory for Passenger servers. Other servers should be detected by default.' + }, + :'security.request.body_limit' => { + :default => 300, + :allow_nil => true, + :public => true, + :type => Integer, + :external => true, + :allowed_from_server => false, + :description => 'Defines the request body limit to process in security events (in KB). The default value is 300, for 300KB.' } }.freeze # rubocop:enable Metrics/CollectionLiteralLength diff --git a/lib/new_relic/agent/database/obfuscator.rb b/lib/new_relic/agent/database/obfuscator.rb index 0a90dcdb6c..5063fdcfa0 100644 --- a/lib/new_relic/agent/database/obfuscator.rb +++ b/lib/new_relic/agent/database/obfuscator.rb @@ -2,6 +2,7 @@ # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. # frozen_string_literal: true +require 'singleton' require 'new_relic/agent/database/obfuscation_helpers' module NewRelic diff --git a/lib/new_relic/agent/instrumentation/bunny/instrumentation.rb b/lib/new_relic/agent/instrumentation/bunny/instrumentation.rb index 7467ef3aef..84a826f7d3 100644 --- a/lib/new_relic/agent/instrumentation/bunny/instrumentation.rb +++ b/lib/new_relic/agent/instrumentation/bunny/instrumentation.rb @@ -48,6 +48,12 @@ def publish_with_tracing(payload, opts = {}) correlation_id: opts[:correlation_id], exchange_type: type ) + if segment + segment.add_agent_attribute('server.address', channel&.connection&.hostname) + segment.add_agent_attribute('server.port', channel&.connection&.port) + segment.add_agent_attribute('messaging.destination.name', destination) # for produce, this is exchange name + segment.add_agent_attribute('messaging.rabbitmq.destination.routing_key', opts[:routing_key]) + end rescue => e NewRelic::Agent.logger.error('Error starting message broker segment in Bunny::Exchange#publish', e) yield @@ -94,6 +100,14 @@ def pop_with_tracing queue_name: name, start_time: t0 ) + if segment + segment.add_agent_attribute('server.address', channel&.connection&.hostname) + segment.add_agent_attribute('server.port', channel&.connection&.port) + segment.add_agent_attribute('messaging.destination.name', name) # for consume, this is queue name + segment.add_agent_attribute('messaging.destination_publish.name', exch_name) + segment.add_agent_attribute('message.queueName', name) + segment.add_agent_attribute('messaging.rabbitmq.destination.routing_key', delivery_info&.routing_key) + end rescue => e NewRelic::Agent.logger.error('Error starting message broker segment in Bunny::Queue#pop', e) else diff --git a/lib/new_relic/agent/instrumentation/elasticsearch/instrumentation.rb b/lib/new_relic/agent/instrumentation/elasticsearch/instrumentation.rb index ba91aea6b4..74b42642de 100644 --- a/lib/new_relic/agent/instrumentation/elasticsearch/instrumentation.rb +++ b/lib/new_relic/agent/instrumentation/elasticsearch/instrumentation.rb @@ -102,7 +102,7 @@ def nr_reported_query(query) end def nr_cluster_name - return @nr_cluster_name if @nr_cluster_name + return @nr_cluster_name if defined?(@nr_cluster_name) return if nr_hosts.empty? NewRelic::Agent.disable_all_tracing do diff --git a/lib/new_relic/agent/instrumentation/logstasher.rb b/lib/new_relic/agent/instrumentation/logstasher.rb new file mode 100644 index 0000000000..2799996e5e --- /dev/null +++ b/lib/new_relic/agent/instrumentation/logstasher.rb @@ -0,0 +1,27 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require_relative 'logstasher/instrumentation' +require_relative 'logstasher/chain' +require_relative 'logstasher/prepend' + +DependencyDetection.defer do + named :logstasher + + depends_on do + defined?(LogStasher) && + Gem::Version.new(LogStasher::VERSION) >= Gem::Version.new('1.0.0') && + NewRelic::Agent.config[:'application_logging.enabled'] + end + + executes do + NewRelic::Agent.logger.info('Installing LogStasher instrumentation') + + if use_prepend? + prepend_instrument LogStasher.singleton_class, NewRelic::Agent::Instrumentation::LogStasher::Prepend + else + chain_instrument NewRelic::Agent::Instrumentation::LogStasher::Chain + end + end +end diff --git a/lib/new_relic/agent/instrumentation/logstasher/chain.rb b/lib/new_relic/agent/instrumentation/logstasher/chain.rb new file mode 100644 index 0000000000..9dc6e59017 --- /dev/null +++ b/lib/new_relic/agent/instrumentation/logstasher/chain.rb @@ -0,0 +1,21 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +module NewRelic::Agent::Instrumentation + module LogStasher::Chain + def self.instrument! + ::LogStasher.singleton_class.class_eval do + include NewRelic::Agent::Instrumentation::LogStasher + + alias_method(:build_logstash_event_without_new_relic, :build_logstash_event) + + def build_logstash_event(*args) + build_logstash_event_with_new_relic(*args) do + build_logstash_event_without_new_relic(*args) + end + end + end + end + end +end diff --git a/lib/new_relic/agent/instrumentation/logstasher/instrumentation.rb b/lib/new_relic/agent/instrumentation/logstasher/instrumentation.rb new file mode 100644 index 0000000000..ca0fce5215 --- /dev/null +++ b/lib/new_relic/agent/instrumentation/logstasher/instrumentation.rb @@ -0,0 +1,24 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +module NewRelic::Agent::Instrumentation + module LogStasher + INSTRUMENTATION_NAME = NewRelic::Agent.base_name(name) + + def self.enabled? + NewRelic::Agent.config[:'instrumentation.logstasher'] != 'disabled' + end + + def build_logstash_event_with_new_relic(*args) + logstasher_event = yield + log = logstasher_event.instance_variable_get(:@data) + + ::NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME) + ::NewRelic::Agent.agent.log_event_aggregator.record_logstasher_event(log) + ::NewRelic::Agent::LocalLogDecorator.decorate(log) + + logstasher_event + end + end +end diff --git a/lib/new_relic/agent/instrumentation/logstasher/prepend.rb b/lib/new_relic/agent/instrumentation/logstasher/prepend.rb new file mode 100644 index 0000000000..504022afd4 --- /dev/null +++ b/lib/new_relic/agent/instrumentation/logstasher/prepend.rb @@ -0,0 +1,13 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +module NewRelic::Agent::Instrumentation + module LogStasher::Prepend + include NewRelic::Agent::Instrumentation::LogStasher + + def build_logstash_event(*args) + build_logstash_event_with_new_relic(*args) { super } + end + end +end diff --git a/lib/new_relic/agent/instrumentation/rack/instrumentation.rb b/lib/new_relic/agent/instrumentation/rack/instrumentation.rb index 7882b6127c..c39bde8586 100644 --- a/lib/new_relic/agent/instrumentation/rack/instrumentation.rb +++ b/lib/new_relic/agent/instrumentation/rack/instrumentation.rb @@ -13,6 +13,7 @@ class << builder_class attr_accessor :_nr_deferred_detection_ran end builder_class._nr_deferred_detection_ran = false + NewRelic::Control::SecurityInterface.instance.wait = true end def deferred_dependency_check @@ -21,6 +22,8 @@ def deferred_dependency_check NewRelic::Agent.logger.info('Doing deferred dependency-detection before Rack startup') DependencyDetection.detect! self.class._nr_deferred_detection_ran = true + NewRelic::Control::SecurityInterface.instance.wait = false + NewRelic::Control::SecurityInterface.instance.init_agent end def check_for_late_instrumentation(app) diff --git a/lib/new_relic/agent/instrumentation/rails_notifications/action_controller.rb b/lib/new_relic/agent/instrumentation/rails_notifications/action_controller.rb index dd0285e248..2750b0f923 100644 --- a/lib/new_relic/agent/instrumentation/rails_notifications/action_controller.rb +++ b/lib/new_relic/agent/instrumentation/rails_notifications/action_controller.rb @@ -32,15 +32,19 @@ NewRelic::Agent::Instrumentation::ActionControllerSubscriber \ .subscribe(/^process_action.action_controller$/) - subs = %w[send_file + subs = %w[exist_fragment? + expire_fragment + halted_callback + read_fragment + redirect_to send_data + send_file send_stream - redirect_to - halted_callback - unpermitted_parameters] + write_fragment + unpermitted_parameters].map { |s| Regexp.escape(s) } # have to double escape period because its going from string -> regex NewRelic::Agent::Instrumentation::ActionControllerOtherSubscriber \ - .subscribe(Regexp.new("^(#{subs.join('|')})\\.action_controller$")) + .subscribe(Regexp.new("^(?:#{subs.join('|')})\\.action_controller$")) end end diff --git a/lib/new_relic/agent/instrumentation/redis.rb b/lib/new_relic/agent/instrumentation/redis.rb index ef609676ab..691c8e87c3 100644 --- a/lib/new_relic/agent/instrumentation/redis.rb +++ b/lib/new_relic/agent/instrumentation/redis.rb @@ -10,6 +10,7 @@ require_relative 'redis/constants' require_relative 'redis/prepend' require_relative 'redis/middleware' +require_relative 'redis/cluster_middleware' DependencyDetection.defer do # Why not :redis? newrelic-redis used that name, so avoid conflicting @@ -33,6 +34,10 @@ NewRelic::Agent.logger.info('Installing Redis Instrumentation') if NewRelic::Agent::Instrumentation::Redis::Constants::HAS_REDIS_CLIENT RedisClient.register(NewRelic::Agent::Instrumentation::RedisClient::Middleware) + + if defined?(Redis::Cluster::Client) + return RedisClient.register(NewRelic::Agent::Instrumentation::RedisClient::ClusterMiddleware) + end end if use_prepend? diff --git a/lib/new_relic/agent/instrumentation/redis/cluster_middleware.rb b/lib/new_relic/agent/instrumentation/redis/cluster_middleware.rb new file mode 100644 index 0000000000..5be2334b8b --- /dev/null +++ b/lib/new_relic/agent/instrumentation/redis/cluster_middleware.rb @@ -0,0 +1,26 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +module NewRelic::Agent::Instrumentation + module RedisClient + module ClusterMiddleware + include NewRelic::Agent::Instrumentation::Redis + + # Until we decide to move our Redis instrumentation entirely off patches + # keep the middleware instrumentation for the call and connect methods + # limited to the redis-clustering instrumentation. + # + # Redis's middleware option does not capture errors as high in the stack + # as our patches. Leaving the patches for call and connect on the main + # Redis gem limits the feature disparity our customers experience. + def call(*args, &block) + call_with_tracing(args[0]) { super } + end + + def connect(*args, &block) + connect_with_tracing { super } + end + end + end +end diff --git a/lib/new_relic/agent/instrumentation/redis/instrumentation.rb b/lib/new_relic/agent/instrumentation/redis/instrumentation.rb index f8c02982cc..ca1ba59057 100644 --- a/lib/new_relic/agent/instrumentation/redis/instrumentation.rb +++ b/lib/new_relic/agent/instrumentation/redis/instrumentation.rb @@ -9,14 +9,14 @@ module Redis INSTRUMENTATION_NAME = NewRelic::Agent.base_name(name) def connect_with_tracing - with_tracing(Constants::CONNECT, database: db) { yield } + with_tracing(Constants::CONNECT, database: _nr_db) { yield } end def call_with_tracing(command, &block) operation = command[0] statement = ::NewRelic::Agent::Datastores::Redis.format_command(command) - with_tracing(operation, statement: statement, database: db) { yield } + with_tracing(operation, statement: statement, database: _nr_db) { yield } end # Used for Redis 4.x and 3.x @@ -24,22 +24,15 @@ def call_pipeline_with_tracing(pipeline) operation = pipeline.is_a?(::Redis::Pipeline::Multi) ? Constants::MULTI_OPERATION : Constants::PIPELINE_OPERATION statement = ::NewRelic::Agent::Datastores::Redis.format_pipeline_commands(pipeline.commands) - with_tracing(operation, statement: statement, database: db) { yield } + with_tracing(operation, statement: statement, database: _nr_db) { yield } end # Used for Redis 5.x+ def call_pipelined_with_tracing(pipeline) - db = begin - _nr_redis_client_config.db - rescue StandardError => e - NewRelic::Agent.logger.error("Failed to determine configured Redis db value: #{e.class} - #{e.message}") - nil - end - operation = pipeline.flatten.include?('MULTI') ? Constants::MULTI_OPERATION : Constants::PIPELINE_OPERATION statement = ::NewRelic::Agent::Datastores::Redis.format_pipeline_commands(pipeline) - with_tracing(operation, statement: statement, database: db) { yield } + with_tracing(operation, statement: statement, database: _nr_db) { yield } end private @@ -94,5 +87,15 @@ def _nr_redis_client_config config end end + + def _nr_db + # db is a method on the Redis client in versions < 5.x + return db if respond_to?(:db) + # db is accessible through the RedisClient::Config object in versions > 5.x + return _nr_redis_client_config.db if _nr_redis_client_config.respond_to?(:db) + rescue StandardError => e + NewRelic::Agent.logger.debug("Failed to determine configured Redis db value: #{e.class} - #{e.message}") + nil + end end end diff --git a/lib/new_relic/agent/instrumentation/redis/middleware.rb b/lib/new_relic/agent/instrumentation/redis/middleware.rb index ef55bb8010..d601fff9fb 100644 --- a/lib/new_relic/agent/instrumentation/redis/middleware.rb +++ b/lib/new_relic/agent/instrumentation/redis/middleware.rb @@ -6,6 +6,9 @@ module NewRelic::Agent::Instrumentation module RedisClient module Middleware # This module is used to instrument Redis 5.x+ + # + # It only instruments call_pipelined because connect and call are accessed + # too late in the stack to capture all errors include NewRelic::Agent::Instrumentation::Redis def call_pipelined(*args, &block) diff --git a/lib/new_relic/agent/local_log_decorator.rb b/lib/new_relic/agent/local_log_decorator.rb index 257868346c..e5a5abe60e 100644 --- a/lib/new_relic/agent/local_log_decorator.rb +++ b/lib/new_relic/agent/local_log_decorator.rb @@ -12,6 +12,12 @@ def decorate(message) return message unless decorating_enabled? metadata = NewRelic::Agent.linking_metadata + + if message.is_a?(Hash) + message.merge!(metadata) unless message.frozen? + return + end + formatted_metadata = " NR-LINKING|#{metadata[ENTITY_GUID_KEY]}|#{metadata[HOSTNAME_KEY]}|" \ "#{metadata[TRACE_ID_KEY]}|#{metadata[SPAN_ID_KEY]}|" \ "#{escape_entity_name(metadata[ENTITY_NAME_KEY])}|" @@ -23,7 +29,8 @@ def decorate(message) def decorating_enabled? NewRelic::Agent.config[:'application_logging.enabled'] && - NewRelic::Agent::Instrumentation::Logger.enabled? && + (NewRelic::Agent::Instrumentation::Logger.enabled? || + NewRelic::Agent::Instrumentation::LogStasher.enabled?) && NewRelic::Agent.config[:'application_logging.local_decorating.enabled'] end diff --git a/lib/new_relic/agent/log_event_aggregator.rb b/lib/new_relic/agent/log_event_aggregator.rb index ff31b07d4a..05c754308d 100644 --- a/lib/new_relic/agent/log_event_aggregator.rb +++ b/lib/new_relic/agent/log_event_aggregator.rb @@ -20,7 +20,8 @@ class LogEventAggregator < EventAggregator DROPPED_METRIC = 'Logging/Forwarding/Dropped'.freeze SEEN_METRIC = 'Supportability/Logging/Forwarding/Seen'.freeze SENT_METRIC = 'Supportability/Logging/Forwarding/Sent'.freeze - OVERALL_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Ruby/Logger/%s'.freeze + LOGGER_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Ruby/Logger/%s'.freeze + LOGSTASHER_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Ruby/LogStasher/%s'.freeze METRICS_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Metrics/Ruby/%s'.freeze FORWARDING_SUPPORTABILITY_FORMAT = 'Supportability/Logging/Forwarding/Ruby/%s'.freeze DECORATING_SUPPORTABILITY_FORMAT = 'Supportability/Logging/LocalDecorating/Ruby/%s'.freeze @@ -58,38 +59,71 @@ def capacity end def record(formatted_message, severity) - return unless enabled? + return unless logger_enabled? severity = 'UNKNOWN' if severity.nil? || severity.empty? + increment_event_counters(severity) + + return if formatted_message.nil? || formatted_message.empty? + return unless monitoring_conditions_met?(severity) + + txn = NewRelic::Agent::Transaction.tl_current + priority = LogPriority.priority_for(txn) - if NewRelic::Agent.config[METRICS_ENABLED_KEY] - @counter_lock.synchronize do - @seen += 1 - @seen_by_severity[severity] += 1 + return txn.add_log_event(create_event(priority, formatted_message, severity)) if txn + + @lock.synchronize do + @buffer.append(priority: priority) do + create_event(priority, formatted_message, severity) end end + rescue + nil + end - return if severity_too_low?(severity) - return if formatted_message.nil? || formatted_message.empty? - return unless NewRelic::Agent.config[FORWARDING_ENABLED_KEY] - return if @high_security + def record_logstasher_event(log) + return unless logstasher_enabled? + + # LogStasher logs do not inherently include a message key, so most logs are recorded. + # But when the key exists, we should not record the log if the message value is nil or empty. + return if log.key?('message') && (log['message'].nil? || log['message'].empty?) + + severity = determine_severity(log) + increment_event_counters(severity) + + return unless monitoring_conditions_met?(severity) txn = NewRelic::Agent::Transaction.tl_current priority = LogPriority.priority_for(txn) - if txn - return txn.add_log_event(create_event(priority, formatted_message, severity)) - else - return @lock.synchronize do - @buffer.append(priority: priority) do - create_event(priority, formatted_message, severity) - end + return txn.add_log_event(create_logstasher_event(priority, severity, log)) if txn + + @lock.synchronize do + @buffer.append(priority: priority) do + create_logstasher_event(priority, severity, log) end end rescue nil end + def monitoring_conditions_met?(severity) + !severity_too_low?(severity) && NewRelic::Agent.config[FORWARDING_ENABLED_KEY] && !@high_security + end + + def determine_severity(log) + log['level'] ? log['level'].to_s.upcase : 'UNKNOWN' + end + + def increment_event_counters(severity) + return unless NewRelic::Agent.config[METRICS_ENABLED_KEY] + + @counter_lock.synchronize do + @seen += 1 + @seen_by_severity[severity] += 1 + end + end + def record_batch(txn, logs) # Ensure we have the same shared priority priority = LogPriority.priority_for(txn) @@ -104,15 +138,17 @@ def record_batch(txn, logs) end end - def create_event(priority, formatted_message, severity) - formatted_message = truncate_message(formatted_message) - - event = LinkingMetadata.append_trace_linking_metadata({ + def add_event_metadata(formatted_message, severity) + metadata = { LEVEL_KEY => severity, - MESSAGE_KEY => formatted_message, TIMESTAMP_KEY => Process.clock_gettime(Process::CLOCK_REALTIME) * 1000 - }) + } + metadata[MESSAGE_KEY] = formatted_message unless formatted_message.nil? + + LinkingMetadata.append_trace_linking_metadata(metadata) + end + def create_prioritized_event(priority, event) [ { PrioritySampledBuffer::PRIORITY_KEY => priority @@ -121,6 +157,31 @@ def create_event(priority, formatted_message, severity) ] end + def create_event(priority, formatted_message, severity) + formatted_message = truncate_message(formatted_message) + event = add_event_metadata(formatted_message, severity) + + create_prioritized_event(priority, event) + end + + def create_logstasher_event(priority, severity, log) + formatted_message = log['message'] ? truncate_message(log['message']) : nil + event = add_event_metadata(formatted_message, severity) + add_logstasher_event_attributes(event, log) + + create_prioritized_event(priority, event) + end + + def add_logstasher_event_attributes(event, log) + log_copy = log.dup + # Delete previously reported attributes + log_copy.delete('message') + log_copy.delete('level') + log_copy.delete('@timestamp') + + event['attributes'] = log_copy + end + def add_custom_attributes(custom_attributes) attributes.add_custom_attributes(custom_attributes) end @@ -166,10 +227,14 @@ def reset! super end - def enabled? + def logger_enabled? @enabled && @instrumentation_logger_enabled end + def logstasher_enabled? + @enabled && NewRelic::Agent::Instrumentation::LogStasher.enabled? + end + private # We record once-per-connect metrics for enabled/disabled state at the @@ -177,8 +242,8 @@ def enabled? def register_for_done_configuring(events) events.subscribe(:server_source_configuration_added) do @high_security = NewRelic::Agent.config[:high_security] - - record_configuration_metric(OVERALL_SUPPORTABILITY_FORMAT, OVERALL_ENABLED_KEY) + record_configuration_metric(LOGGER_SUPPORTABILITY_FORMAT, OVERALL_ENABLED_KEY) + record_configuration_metric(LOGSTASHER_SUPPORTABILITY_FORMAT, OVERALL_ENABLED_KEY) record_configuration_metric(METRICS_SUPPORTABILITY_FORMAT, METRICS_ENABLED_KEY) record_configuration_metric(FORWARDING_SUPPORTABILITY_FORMAT, FORWARDING_ENABLED_KEY) record_configuration_metric(DECORATING_SUPPORTABILITY_FORMAT, DECORATING_ENABLED_KEY) diff --git a/lib/new_relic/control.rb b/lib/new_relic/control.rb index fda4a2fac4..66d4c8ab29 100644 --- a/lib/new_relic/control.rb +++ b/lib/new_relic/control.rb @@ -8,7 +8,6 @@ require 'new_relic/language_support' require 'new_relic/helper' -require 'singleton' require 'erb' require 'socket' require 'net/https' @@ -18,6 +17,7 @@ require 'new_relic/control/instrumentation' require 'new_relic/control/class_methods' require 'new_relic/control/instance_methods' +require 'new_relic/control/security_interface' require 'new_relic/agent' require 'new_relic/delayed_job_injection' diff --git a/lib/new_relic/control/instance_methods.rb b/lib/new_relic/control/instance_methods.rb index e0cb566d2b..c64dee0297 100644 --- a/lib/new_relic/control/instance_methods.rb +++ b/lib/new_relic/control/instance_methods.rb @@ -73,6 +73,7 @@ def init_plugin(options = {}) init_config(options) NewRelic::Agent.agent = NewRelic::Agent::Agent.instance init_instrumentation + init_security_agent end def determine_env(options) diff --git a/lib/new_relic/control/private_instance_methods.rb b/lib/new_relic/control/private_instance_methods.rb index bcd14c7a81..2cd45cc827 100644 --- a/lib/new_relic/control/private_instance_methods.rb +++ b/lib/new_relic/control/private_instance_methods.rb @@ -43,6 +43,10 @@ def init_instrumentation DependencyDetection.detect! end end + + def init_security_agent + SecurityInterface.instance.init_agent + end end end end diff --git a/lib/new_relic/control/security_interface.rb b/lib/new_relic/control/security_interface.rb new file mode 100644 index 0000000000..7edbea5c62 --- /dev/null +++ b/lib/new_relic/control/security_interface.rb @@ -0,0 +1,57 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require 'singleton' + +module NewRelic + class Control + class SecurityInterface + include Singleton + + attr_accessor :wait + + SUPPORTABILITY_PREFIX_SECURITY = 'Supportability/Ruby/SecurityAgent/Enabled/' + SUPPORTABILITY_PREFIX_SECURITY_AGENT = 'Supportability/Ruby/SecurityAgent/Agent/Enabled/' + ENABLED = 'enabled' + DISABLED = 'disabled' + + def agent_started? + (@agent_started ||= false) == true + end + + def waiting? + (@wait ||= false) == true + end + + def init_agent + return if agent_started? || waiting? + + record_supportability_metrics + + if Agent.config[:'security.agent.enabled'] && !Agent.config[:high_security] + Agent.logger.info('Invoking New Relic security module') + require 'newrelic_security' + + @agent_started = true + else + Agent.logger.info('New Relic Security is completely disabled by one of the user-provided configurations: `security.agent.enabled` or `high_security`. Not loading security capabilities.') + Agent.logger.info("high_security = #{Agent.config[:high_security]}") + Agent.logger.info("security.agent.enabled = #{Agent.config[:'security.agent.enabled']}") + end + rescue LoadError + Agent.logger.info('New Relic security agent not found - skipping') + rescue StandardError => exception + Agent.logger.error("Exception in New Relic security module loading: #{exception} #{exception.backtrace}") + end + + def record_supportability_metrics + Agent.config[:'security.agent.enabled'] ? security_agent_metric(ENABLED) : security_agent_metric(DISABLED) + end + + def security_agent_metric(setting) + NewRelic::Agent.record_metric_once(SUPPORTABILITY_PREFIX_SECURITY_AGENT + setting) + end + end + end +end diff --git a/lib/new_relic/rack/browser_monitoring.rb b/lib/new_relic/rack/browser_monitoring.rb index a87b2cefae..0401e0e337 100644 --- a/lib/new_relic/rack/browser_monitoring.rb +++ b/lib/new_relic/rack/browser_monitoring.rb @@ -20,15 +20,15 @@ class BrowserMonitoring < AgentMiddleware # examine in order to look for a RUM insertion point. SCAN_LIMIT = 50_000 - CONTENT_TYPE = 'Content-Type'.freeze - CONTENT_DISPOSITION = 'Content-Disposition'.freeze - CONTENT_LENGTH = 'Content-Length'.freeze + CONTENT_TYPE = 'Content-Type' + CONTENT_DISPOSITION = 'Content-Disposition' + CONTENT_LENGTH = 'Content-Length' ATTACHMENT = /attachment/.freeze TEXT_HTML = %r{text/html}.freeze - BODY_START = ']+charset\s*=[^>]*>/im.freeze @@ -120,7 +120,11 @@ def attachment?(headers) end def streaming?(env, headers) - # Chunked transfer encoding is a streaming data transfer mechanism available only in HTTP/1.1 + # Up until version 8.0, Rails would set 'Transfer-Encoding' to 'chunked' + # to trigger the desired HTTP/1.1 based streaming functionality in Rack. + # With version v8.0+, Rails assumes that the web server will be using + # Rack v3+ or an equally modern alternative and simply leaves the + # streaming behavior up to them. return true if headers && headers['Transfer-Encoding'] == 'chunked' defined?(ActionController::Live) && diff --git a/lib/new_relic/version.rb b/lib/new_relic/version.rb index 3baf6cb032..eb65139f10 100644 --- a/lib/new_relic/version.rb +++ b/lib/new_relic/version.rb @@ -6,7 +6,7 @@ module NewRelic module VERSION # :nodoc: MAJOR = 9 - MINOR = 11 + MINOR = 12 TINY = 0 STRING = "#{MAJOR}.#{MINOR}.#{TINY}" diff --git a/lib/tasks/config.rake b/lib/tasks/config.rake index 9ba66bbcb2..4ef9da672b 100644 --- a/lib/tasks/config.rake +++ b/lib/tasks/config.rake @@ -23,13 +23,16 @@ namespace :newrelic do 'browser_monitoring' => "The [page load timing](/docs/browser/new-relic-browser/page-load-timing/page-load-timing-process) feature (sometimes referred to as real user monitoring or RUM) gives you insight into the performance real users are experiencing with your website. This is accomplished by measuring the time it takes for your users' browsers to download and render your web pages by injecting a small amount of JavaScript code into the header and footer of each page.", 'application_logging' => "The Ruby agent supports [APM logs in context](/docs/apm/new-relic-apm/getting-started/get-started-logs-context). For some tips on configuring logs for the Ruby agent, see [Configure Ruby logs in context](/docs/logs/logs-context/configure-logs-context-ruby).\n\nAvailable logging-related config options include:", 'analytics_events' => '[New Relic dashboards](/docs/query-your-data/explore-query-data/dashboards/introduction-new-relic-one-dashboards) is a resource to gather and visualize data about your software and what it says about your business. With it you can quickly and easily create real-time dashboards to get immediate answers about end-user experiences, clickstreams, mobile activities, and server transactions.', - 'ai_monitoring' => "This section includes Ruby agent configurations for setting up AI monitoring.\n\nYou need to enable distributed tracing to capture trace and feedback data. It is turned on by default in Ruby agents 8.0.0 and higher." + 'ai_monitoring' => "This section includes Ruby agent configurations for setting up AI monitoring.\n\nYou need to enable distributed tracing to capture trace and feedback data. It is turned on by default in Ruby agents 8.0.0 and higher.", + 'security_agent' => "[New Relic Interactive Application Security Testing](https://docs.newrelic.com/docs/iast/introduction/) (IAST) tests your applications for any exploitable vulnerability by replaying the generated HTTP request with vulnerable payloads.\n\nRun IAST with non-production deployments only to avoid exposing vulnerabilities on your production software. \ + IAST mode requires Ruby agent version 9.12.0 or higher and the [newrelic_security](https://rubygems.org/gems/newrelic_security) gem. Security agent configurations are disabled by default." } NAME_OVERRIDES = { 'slow_sql' => 'Slow SQL [#slow-sql]', 'custom_insights_events' => 'Custom Events [#custom-events]', - 'ai_monitoring' => 'AI Monitoring [#ai-monitoring]' + 'ai_monitoring' => 'AI Monitoring [#ai-monitoring]', + 'security_agent' => 'Security Agent [#security-agent]' } desc 'Describe available New Relic configuration settings' diff --git a/lib/tasks/helpers/config.html.erb b/lib/tasks/helpers/config.html.erb index 64ede173b1..fff66a972d 100644 --- a/lib/tasks/helpers/config.html.erb +++ b/lib/tasks/helpers/config.html.erb @@ -76,7 +76,7 @@ When running the Ruby agent in a Rails app, the agent first looks for the `NEW_R When you edit the config file, be sure to: * Indent only with two spaces. -* Indent only where relevant, in sections such as **`error_collector`**. +* Indent only where relevant, in sections such as **`error_collector`**. If you do not indent correctly, the agent may throw an `Unable to parse configuration file` error on startup. diff --git a/lib/tasks/helpers/format.rb b/lib/tasks/helpers/format.rb index 2c4a3119df..fc4f2e4f22 100644 --- a/lib/tasks/helpers/format.rb +++ b/lib/tasks/helpers/format.rb @@ -69,7 +69,7 @@ def format_default_value(spec) def format_description(value) description = '' - description += '**DEPRECATED**' if value[:deprecated] + description += '**DEPRECATED** ' if value[:deprecated] description += value[:description] description end diff --git a/lib/tasks/helpers/newrelicyml.rb b/lib/tasks/helpers/newrelicyml.rb index f9bc190aa4..1058996881 100644 --- a/lib/tasks/helpers/newrelicyml.rb +++ b/lib/tasks/helpers/newrelicyml.rb @@ -105,8 +105,9 @@ def self.sanitize_description(description) def self.format_description(description) # remove leading and trailing whitespace description.strip! - # wrap text after 80 characters - description.gsub!(/(.{1,80})(\s+|\Z)/, "\\1\n") + # wrap text after 80 characters, assuming we're at one tabstop's (two + # spaces') level of indentation already + description.gsub!(/(.{1,78})(\s+|\Z)/, "\\1\n") # add hashtags to lines description = description.split("\n").map { |line| " # #{line}" }.join("\n") diff --git a/lib/tasks/instrumentation_generator/instrumentation.thor b/lib/tasks/instrumentation_generator/instrumentation.thor index d2ac0b4170..06305c1074 100644 --- a/lib/tasks/instrumentation_generator/instrumentation.thor +++ b/lib/tasks/instrumentation_generator/instrumentation.thor @@ -103,6 +103,7 @@ class Instrumentation < Thor <<-CONFIG :'instrumentation.#{snake_name}' => { :default => 'auto', + :documentation_default => 'auto' :public => true, :type => String, :dynamic_name => true, diff --git a/newrelic.yml b/newrelic.yml index 3c19b3eb4a..ccddfde376 100644 --- a/newrelic.yml +++ b/newrelic.yml @@ -26,16 +26,16 @@ common: &default_settings # All of the following configuration options are optional. Review them, and # uncomment or edit them if they appear relevant to your application needs. - # An array of ActiveSupport custom event names to subscribe to and instrument. For - # example, + # An array of ActiveSupport custom event names to subscribe to and instrument. + # For example, # - one.custom.event # - another.event # - a.third.event # active_support_custom_events_names: [] - # If false, all LLM instrumentation (OpenAI only for now) will be disabled and no - # metrics, events, or spans will be sent. AI Monitoring is automatically disabled - # if high_security mode is enabled. + # If false, all LLM instrumentation (OpenAI only for now) will be disabled and + # no metrics, events, or spans will be sent. AI Monitoring is automatically + # disabled if high_security mode is enabled. # ai_monitoring.enabled: false # If false, LLM instrumentation (OpenAI only for now) will not capture input and @@ -43,23 +43,23 @@ common: &default_settings # The excluded attributes include: # * content from LlmChatCompletionMessage events # * input from LlmEmbedding events - # This is an optional security setting to prevent recording sensitive data sent to - # and received from your LLMs. + # This is an optional security setting to prevent recording sensitive data sent + # to and received from your LLMs. # ai_monitoring.record_content.enabled: true # If true, enables capture of all HTTP request headers for all destinations. # allow_all_headers: false - # Your New Relic userKey. Required when using the New Relic REST API v2 to record - # deployments using the newrelic deployments command. + # Your New Relic userKey. Required when using the New Relic REST API v2 to + # record deployments using the newrelic deployments command. # api_key: "" # If true, enables log decoration and the collection of log events and metrics. # application_logging.enabled: true # A hash with key/value pairs to add as custom attributes to all log events - # forwarded to New Relic. If sending using an environment variable, the value must - # be formatted like: "key1=value1,key2=value2" + # forwarded to New Relic. If sending using an environment variable, the value + # must be formatted like: "key1=value1,key2=value2" # application_logging.forwarding.custom_attributes: {} # If true, the agent captures log records emitted by your application. @@ -126,16 +126,16 @@ common: &default_settings # instrument. For example, "assets:precompile,db:migrate". # autostart.denylisted_rake_tasks: about,assets:clean,assets:clobber,assets:environment,assets:precompile,assets:precompile:all,db:create,db:drop,db:fixtures:load,db:migrate,db:migrate:status,db:rollback,db:schema:cache:clear,db:schema:cache:dump,db:schema:dump,db:schema:load,db:seed,db:setup,db:structure:dump,db:version,doc:app,log:clear,middleware,notes,notes:custom,rails:template,rails:update,routes,secret,spec,spec:features,spec:requests,spec:controllers,spec:helpers,spec:models,spec:views,spec:routing,spec:rcov,stats,test,test:all,test:all:db,test:recent,test:single,test:uncommitted,time:zones:all,tmp:clear,tmp:create,webpacker:compile - # Backports the faster ActiveRecord connection lookup introduced in Rails 6, which - # improves agent performance when instrumenting ActiveRecord. Note that this - # setting may not be compatible with other gems that patch ActiveRecord. + # Backports the faster ActiveRecord connection lookup introduced in Rails 6, + # which improves agent performance when instrumenting ActiveRecord. Note that + # this setting may not be compatible with other gems that patch ActiveRecord. # backport_fast_active_record_connection_lookup: false # If true, the agent captures attributes from browser monitoring. # browser_monitoring.attributes.enabled: false - # Prefix of attributes to exclude from browser monitoring. Allows * as wildcard at - # end. + # Prefix of attributes to exclude from browser monitoring. Allows * as wildcard + # at end. # browser_monitoring.attributes.exclude: [] # Prefix of attributes to include in browser monitoring. Allows * as wildcard at @@ -160,17 +160,17 @@ common: &default_settings # When true, the agent captures HTTP request parameters and attaches them to # transaction traces, traced errors, and TransactionError events. - # When using the capture_params setting, the Ruby agent will not attempt to filter - # secret information. Recommendation: To filter secret information from request - # parameters, use the attributes.include setting instead. For more information, - # see the Ruby attribute examples. + # When using the capture_params setting, the Ruby agent will not attempt to + # filter secret information. Recommendation: To filter secret information from + # request parameters, use the attributes.include setting instead. For more + # information, see the Ruby attribute examples. # capture_params: false # If true, the agent will clear Tracer::State in Agent.drop_buffered_data. # clear_transaction_state_after_fork: false # If true, the agent will report source code level metrics for traced methods. - # see: + # See: # https://docs.newrelic.com/docs/apm/agents/ruby-agent/features/ruby-codestream-integration/ # code_level_metrics.enabled: true @@ -188,17 +188,17 @@ common: &default_settings # If true, the agent captures custom events. # custom_insights_events.enabled: true - # * Specify a maximum number of custom events to buffer in memory at a time.' + # * Specify a maximum number of custom events to buffer in memory at a time. # * When configuring the agent for AI monitoring, set to max value 100000. This # ensures the agent captures the maximum amount of LLM events. # custom_insights_events.max_samples_stored: 3000 - # If false, the agent will not add database_name parameter to transaction or slow - # sql traces. + # If false, the agent will not add database_name parameter to transaction or + # slow sql traces. # datastore_tracer.database_name_reporting.enabled: true - # If false, the agent will not report datastore instance metrics, nor add host or - # port_path_or_id parameters to transaction or slow SQL traces. + # If false, the agent will not report datastore instance metrics, nor add host + # or port_path_or_id parameters to transaction or slow SQL traces. # datastore_tracer.instance_reporting.enabled: true # If true, disables Action Cable instrumentation. @@ -244,16 +244,16 @@ common: &default_settings # If true, the agent won't wrap third-party middlewares in instrumentation # (regardless of whether they are installed via Rack::Builder or Rails). # When middleware instrumentation is disabled, if an application is using - # middleware that could alter the response code, the HTTP status code reported on - # the transaction may not reflect the altered value. + # middleware that could alter the response code, the HTTP status code reported + # on the transaction may not reflect the altered value. # disable_middleware_instrumentation: false - # If true, disables agent middleware for Roda. This middleware is responsible for - # advanced feature support such as page load timing and error collection. + # If true, disables agent middleware for Roda. This middleware is responsible + # for advanced feature support such as page load timing and error collection. # disable_roda_auto_middleware: false - # If true, disables the collection of sampler metrics. Sampler metrics are metrics - # that are not event-based (such as CPU time or memory usage). + # If true, disables the collection of sampler metrics. Sampler metrics are + # metrics that are not event-based (such as CPU time or memory usage). # disable_samplers: false # If true, disables Sequel instrumentation. @@ -287,8 +287,8 @@ common: &default_settings # Distributed tracing lets you see the path that a request takes through your # distributed system. Enabling distributed tracing changes the behavior of some - # New Relic features, so carefully consult the transition guide before you enable - # this feature. + # New Relic features, so carefully consult the transition guide before you + # enable this feature. # distributed_tracing.enabled: true # If true, the agent captures Elasticsearch queries in transaction traces. @@ -319,8 +319,8 @@ common: &default_settings # error_collector.expected_classes: [] # A map of error classes to a list of messages. When an error of one of the - # classes specified here occurs, if its error message contains one of the strings - # corresponding to it here, that error will be treated as expected. + # classes specified here occurs, if its error message contains one of the + # strings corresponding to it here, that error will be treated as expected. # This option can't be set via environment variable. # error_collector.expected_messages: {} @@ -334,8 +334,8 @@ common: &default_settings # error_collector.ignore_classes: ["ActionController::RoutingError", "Sinatra::NotFound"] # A map of error classes to a list of messages. When an error of one of the - # classes specified here occurs, if its error message contains one of the strings - # corresponding to it here, that error will be ignored. + # classes specified here occurs, if its error message contains one of the + # strings corresponding to it here, that error will be ignored. # This option can't be set via environment variable. # error_collector.ignore_messages: {} @@ -343,9 +343,9 @@ common: &default_settings # associated with these status codes, where applicable, will be ignored. # error_collector.ignore_status_codes: "" - # Defines the maximum number of frames in an error backtrace. Backtraces over this - # amount are truncated in the middle, preserving the beginning and the end of the - # stack trace. + # Defines the maximum number of frames in an error backtrace. Backtraces over + # this amount are truncated in the middle, preserving the beginning and the end + # of the stack trace. # error_collector.max_backtrace_frames: 50 # Defines the maximum number of TransactionError events reported per harvest @@ -357,18 +357,19 @@ common: &default_settings # exclude_newrelic_header: false # The exit handler that sends all cached data to the collector before shutting - # down is forcibly installed. This is true even when it detects scenarios where it - # generally should not be. The known use case for this option is when Sinatra runs - # as an embedded service within another framework. The agent detects the Sinatra - # app and skips the at_exit handler as a result. Sinatra classically runs the - # entire application in an at_exit block and would otherwise misbehave if the - # agent's at_exit handler was also installed in those circumstances. Note: - # send_data_on_exit should also be set to true in tandem with this setting. + # down is forcibly installed. This is true even when it detects scenarios where + # it generally should not be. The known use case for this option is when Sinatra + # runs as an embedded service within another framework. The agent detects the + # Sinatra app and skips the at_exit handler as a result. Sinatra classically + # runs the entire application in an at_exit block and would otherwise misbehave + # if the agent's at_exit handler was also installed in those circumstances. + # Note: send_data_on_exit should also be set to true in tandem with this + # setting. # force_install_exit_handler: false - # Ordinarily the agent reports dyno names with a trailing dot and process ID (for - # example, worker.3). You can remove this trailing data by specifying the prefixes - # you want to report without trailing data (for example, worker). + # Ordinarily the agent reports dyno names with a trailing dot and process ID + # (for example, worker.3). You can remove this trailing data by specifying the + # prefixes you want to report without trailing data (for example, worker). # heroku.dyno_name_prefixes_to_shorten: ["scheduler", "run"] # If true, the agent uses Heroku dyno names as the hostname. @@ -395,8 +396,8 @@ common: &default_settings # Configures the TCP/IP port for the trace observer Host # infinite_tracing.trace_observer.port: 443 - # Controls auto-instrumentation of ActiveSupport::BroadcastLogger at start up. May - # be one of: auto, prepend, chain, disabled. Used in Rails versions >= 7.1. + # Controls auto-instrumentation of ActiveSupport::BroadcastLogger at start up. + # May be one of: auto, prepend, chain, disabled. Used in Rails versions >= 7.1. # instrumentation.active_support_broadcast_logger: auto # Controls auto-instrumentation of ActiveSupport::Logger at start up. May be one @@ -407,20 +408,20 @@ common: &default_settings # prepend, chain, disabled. # instrumentation.async_http: auto - # Controls auto-instrumentation of the aws-sdk-sqs library at start-up. May be one - # of: auto, prepend, chain, disabled. + # Controls auto-instrumentation of the aws-sdk-sqs library at start-up. May be + # one of: auto, prepend, chain, disabled. # instrumentation.aws_sqs: auto # Controls auto-instrumentation of bunny at start-up. May be one of: auto, # prepend, chain, disabled. # instrumentation.bunny: auto - # Controls auto-instrumentation of the concurrent-ruby library at start-up. May be - # one of: auto, prepend, chain, disabled. + # Controls auto-instrumentation of the concurrent-ruby library at start-up. May + # be one of: auto, prepend, chain, disabled. # instrumentation.concurrent_ruby: auto - # Controls auto-instrumentation of Curb at start-up. May be one of: auto, prepend, - # chain, disabled. + # Controls auto-instrumentation of Curb at start-up. May be one of: auto, + # prepend, chain, disabled. # instrumentation.curb: auto # Controls auto-instrumentation of Delayed Job at start-up. May be one of: auto, @@ -435,8 +436,8 @@ common: &default_settings # one of: auto, prepend, chain, disabled. # instrumentation.elasticsearch: auto - # Controls auto-instrumentation of ethon at start up. May be one of auto, prepend, - # chain, disabled + # Controls auto-instrumentation of ethon at start up. May be one of auto, + # prepend, chain, disabled # instrumentation.ethon: auto # Controls auto-instrumentation of Excon at start-up. May be one of: enabled, @@ -454,18 +455,18 @@ common: &default_settings # Specifies a list of hostname patterns separated by commas that will match gRPC # hostnames that traffic is to be ignored by New Relic for. New Relic's gRPC # client instrumentation will ignore traffic streamed to a host matching any of - # these patterns, and New Relic's gRPC server instrumentation will ignore traffic - # for a server running on a host whose hostname matches any of these patterns. By - # default, no traffic is ignored when gRPC instrumentation is itself enabled. For - # example, "private.com$,exception.*" + # these patterns, and New Relic's gRPC server instrumentation will ignore + # traffic for a server running on a host whose hostname matches any of these + # patterns. By default, no traffic is ignored when gRPC instrumentation is + # itself enabled. For example, "private.com$,exception.*" # instrumentation.grpc.host_denylist: [] - # Controls auto-instrumentation of gRPC clients at start-up. May be one of: auto, - # prepend, chain, disabled. + # Controls auto-instrumentation of gRPC clients at start-up. May be one of: + # auto, prepend, chain, disabled. # instrumentation.grpc_client: auto - # Controls auto-instrumentation of gRPC servers at start-up. May be one of: auto, - # prepend, chain, disabled. + # Controls auto-instrumentation of gRPC servers at start-up. May be one of: + # auto, prepend, chain, disabled. # instrumentation.grpc_server: auto # Controls auto-instrumentation of HTTPClient at start-up. May be one of: auto, @@ -476,24 +477,28 @@ common: &default_settings # prepend, chain, disabled. # instrumentation.httprb: auto - # Controls auto-instrumentation of httpx at start up. May be one of auto, prepend, - # chain, disabled + # Controls auto-instrumentation of httpx at start up. May be one of auto, + # prepend, chain, disabled # instrumentation.httpx: auto # Controls auto-instrumentation of Ruby standard library Logger at start-up. May # be one of: auto, prepend, chain, disabled. # instrumentation.logger: auto - # Controls auto-instrumentation of dalli gem for Memcache at start-up. May be one - # of: auto, prepend, chain, disabled. + # Controls auto-instrumentation of the LogStasher library at start-up. May be + # one of: auto, prepend, chain, disabled. + # instrumentation.logstasher: auto + + # Controls auto-instrumentation of dalli gem for Memcache at start-up. May be + # one of: auto, prepend, chain, disabled. # instrumentation.memcache: auto # Controls auto-instrumentation of memcache-client gem for Memcache at start-up. # May be one of: auto, prepend, chain, disabled. # instrumentation.memcache_client: auto - # Controls auto-instrumentation of memcached gem for Memcache at start-up. May be - # one of: auto, prepend, chain, disabled. + # Controls auto-instrumentation of memcached gem for Memcache at start-up. May + # be one of: auto, prepend, chain, disabled. # instrumentation.memcached: auto # Controls auto-instrumentation of Mongo at start-up. May be one of: enabled, @@ -504,13 +509,13 @@ common: &default_settings # prepend, chain, disabled. # instrumentation.net_http: auto - # Controls auto-instrumentation of Puma::Rack. When enabled, the agent hooks into - # the to_app method in Puma::Rack::Builder to find gems to instrument during - # application startup. May be one of: auto, prepend, chain, disabled. + # Controls auto-instrumentation of Puma::Rack. When enabled, the agent hooks + # into the to_app method in Puma::Rack::Builder to find gems to instrument + # during application startup. May be one of: auto, prepend, chain, disabled. # instrumentation.puma_rack: auto - # Controls auto-instrumentation of Puma::Rack::URLMap at start-up. May be one of: - # auto, prepend, chain, disabled. + # Controls auto-instrumentation of Puma::Rack::URLMap at start-up. May be one + # of: auto, prepend, chain, disabled. # instrumentation.puma_rack_urlmap: auto # Controls auto-instrumentation of Rack. When enabled, the agent hooks into the @@ -518,12 +523,12 @@ common: &default_settings # startup. May be one of: auto, prepend, chain, disabled. # instrumentation.rack: auto - # Controls auto-instrumentation of Rack::URLMap at start-up. May be one of: auto, - # prepend, chain, disabled. + # Controls auto-instrumentation of Rack::URLMap at start-up. May be one of: + # auto, prepend, chain, disabled. # instrumentation.rack_urlmap: auto - # Controls auto-instrumentation of rake at start-up. May be one of: auto, prepend, - # chain, disabled. + # Controls auto-instrumentation of rake at start-up. May be one of: auto, + # prepend, chain, disabled. # instrumentation.rake: auto # Controls auto-instrumentation of Redis at start-up. May be one of: auto, @@ -534,12 +539,13 @@ common: &default_settings # prepend, chain, disabled. # instrumentation.resque: auto - # Controls auto-instrumentation of Roda at start-up. May be one of: auto, prepend, - # chain, disabled. + # Controls auto-instrumentation of Roda at start-up. May be one of: auto, + # prepend, chain, disabled. # instrumentation.roda: auto - # Controls auto-instrumentation of the ruby-openai gem at start-up. May be one of: - # auto, prepend, chain, disabled. Defaults to disabled in high security mode. + # Controls auto-instrumentation of the ruby-openai gem at start-up. May be one + # of: auto, prepend, chain, disabled. Defaults to disabled in high security + # mode. # instrumentation.ruby_openai: auto # Controls auto-instrumentation of Sinatra at start-up. May be one of: auto, @@ -550,10 +556,11 @@ common: &default_settings # disabled. # instrumentation.stripe: enabled - # Controls auto-instrumentation of the Thread class at start-up to allow the agent - # to correctly nest spans inside of an asynchronous transaction. This does not - # enable the agent to automatically trace all threads created (see - # instrumentation.thread.tracing). May be one of: auto, prepend, chain, disabled. + # Controls auto-instrumentation of the Thread class at start-up to allow the + # agent to correctly nest spans inside of an asynchronous transaction. This does + # not enable the agent to automatically trace all threads created (see + # instrumentation.thread.tracing). May be one of: auto, prepend, chain, + # disabled. # instrumentation.thread: auto # Controls auto-instrumentation of the Thread class at start-up to automatically @@ -568,8 +575,8 @@ common: &default_settings # prepend, chain, disabled. # instrumentation.typhoeus: auto - # Controls auto-instrumentation of ViewComponent at startup. May be one of: auto, - # prepend, chain, disabled. + # Controls auto-instrumentation of ViewComponent at startup. May be one of: + # auto, prepend, chain, disabled. # instrumentation.view_component: auto # A dictionary of label names and values that will be applied to the data sent @@ -628,8 +635,8 @@ common: &default_settings # Specify an Array of Rake tasks to automatically instrument. This configuration # option converts the Array to a RegEx list. If you'd like to allow all tasks by - # default, use rake.tasks: [.+]. No rake tasks will be instrumented unless they're - # added to this list. For more information, visit the New Relic Rake + # default, use rake.tasks: [.+]. No rake tasks will be instrumented unless + # they're added to this list. For more information, visit the New Relic Rake # Instrumentation docs. # rake.tasks: [] @@ -638,6 +645,60 @@ common: &default_settings # ignoring specific transactions. # rules.ignore_url_regexes: [] + # BEGIN security agent + # + # NOTE: At this time, the security agent is intended for use only within + # a dedicated security testing environment with data that can tolerate + # modification or deletion. The security agent is available as a + # separate Ruby gem, newrelic_security. It is recommended that this + # separate gem only be introduced to a security testing environment + # by leveraging Bundler grouping like so: + # + # # Gemfile + # gem 'newrelic_rpm' # New Relic APM observability agent + # gem 'newrelic-infinite_tracing' # New Relic Infinite Tracing + # + # group :security do + # gem 'newrelic_security', require: false # New Relic security agent + # end + # + # NOTE: All "security.*" configuration parameters are related only to the + # security agent, and all other configuration parameters that may + # have "security" in the name some where are related to the APM agent. + # + + # If true, the security agent is loaded (a Ruby 'require' is performed) + # security.agent.enabled: false + + # The port the application is listening on. This setting is mandatory for + # Passenger servers. Other servers should be detected by default. + # security.application_info.port: nil + + # If true, enables deserialization detection + # security.detection.deserialization.enabled: true + + # If true, enables RCI (remote code injection) detection + # security.detection.rci.enabled: true + + # If true, enables RXSS (reflected cross-site scripting) detection + # security.detection.rxss.enabled: true + + # If true, the security agent is started (the agent runs in its event loop) + # security.enabled: false + + # Defines the mode for the security agent to operate in. Currently only IAST is + # supported + # security.mode: IAST + + # Defines the request body limit to process in security events (in KB). The + # default value is 300, for 300KB. + # security.request.body_limit: 300 + + # Defines the endpoint URL for posting security-related data + # security.validator_service_url: wss://csec.nr-data.net + + # END security agent + # Applies Language Agent Security Policy settings. # security_policies_token: "" @@ -646,29 +707,29 @@ common: &default_settings # send_data_on_exit: true # If true, the agent will operate in a streamlined mode suitable for use with - # short-lived serverless functions. NOTE: Only AWS Lambda functions are supported - # currently and this option is not intended for use without New Relic's Ruby - # Lambda layer offering. + # short-lived serverless functions. NOTE: Only AWS Lambda functions are + # supported currently and this option is not intended for use without New + # Relic's Ruby Lambda layer offering. # serverless_mode.enabled: false # An array of strings that will collectively serve as a denylist for filtering # which Sidekiq job arguments get reported to New Relic. To capture any Sidekiq # arguments, 'job.sidekiq.args.*' must be added to the separate # :'attributes.include' configuration option. Each string in this array will be - # turned into a regular expression via Regexp.new to permit advanced matching. For - # job argument hashes, if either a key or value matches the pair will be excluded. - # All matching job argument array elements and job argument scalars will be - # excluded. + # turned into a regular expression via Regexp.new to permit advanced matching. + # For job argument hashes, if either a key or value matches the pair will be + # excluded. All matching job argument array elements and job argument scalars + # will be excluded. # sidekiq.args.exclude: [] # An array of strings that will collectively serve as an allowlist for filtering # which Sidekiq job arguments get reported to New Relic. To capture any Sidekiq # arguments, 'job.sidekiq.args.*' must be added to the separate # :'attributes.include' configuration option. Each string in this array will be - # turned into a regular expression via Regexp.new to permit advanced matching. For - # job argument hashes, if either a key or value matches the pair will be included. - # All matching job argument array elements and job argument scalars will be - # included. + # turned into a regular expression via Regexp.new to permit advanced matching. + # For job argument hashes, if either a key or value matches the pair will be + # included. All matching job argument array elements and job argument scalars + # will be included. # sidekiq.args.include: [] # If true, the agent collects slow SQL queries. @@ -679,16 +740,16 @@ common: &default_settings # the default setting for explain plans in slow SQL as well. # slow_sql.explain_enabled: true - # Specify a threshold in seconds. The agent collects slow SQL queries and explain - # plans that exceed this threshold. + # Specify a threshold in seconds. The agent collects slow SQL queries and + # explain plans that exceed this threshold. # slow_sql.explain_threshold: 0.5 - # Defines an obfuscation level for slow SQL queries. Valid options are obfuscated, - # raw, or none. + # Defines an obfuscation level for slow SQL queries. Valid options are + # obfuscated, raw, or none. # slow_sql.record_sql: obfuscated - # Generate a longer sql_id for slow SQL traces. sql_id is used for aggregation of - # similar queries. + # Generate a longer sql_id for slow SQL traces. sql_id is used for aggregation + # of similar queries. # slow_sql.use_longer_sql_id: false # If true, the agent captures attributes on span events. @@ -703,8 +764,8 @@ common: &default_settings # If true, enables span event sampling. # span_events.enabled: true - # * Defines the maximum number of span events reported from a single harvest. Any - # Integer between 1 and 10000 is valid.' + # * Defines the maximum number of span events reported from a single harvest. + # Any Integer between 1 and 10000 is valid.' # * When configuring the agent for AI monitoring, set to max value 10000.This # ensures the agent captures the maximum amount of distributed traces. # span_events.max_samples_stored: 2000 @@ -714,30 +775,31 @@ common: &default_settings # span_events.queue_size: 10000 # Specify a list of exceptions you do not want the agent to strip when - # strip_exception_messages is true. Separate exceptions with a comma. For example, - # "ImportantException,PreserveMessageException". + # strip_exception_messages is true. Separate exceptions with a comma. For + # example, "ImportantException,PreserveMessageException". # strip_exception_messages.allowed_classes: "" # If true, the agent strips messages from all exceptions except those in the # allowlist. Enabled automatically in high security mode. # strip_exception_messages.enabled: false - # An array of strings to specify which keys and/or values inside a Stripe event's - # user_data hash should + # An array of strings to specify which keys and/or values inside a Stripe + # event's user_data hash should # not be reported to New Relic. Each string in this array will be turned into a # regular expression via - # Regexp.new to permit advanced matching. For each hash pair, if either the key or - # value is matched the - # pair will not be reported. By default, no user_data is reported, so this option - # should only be used if + # Regexp.new to permit advanced matching. For each hash pair, if either the key + # or value is matched the + # pair will not be reported. By default, no user_data is reported, so this + # option should only be used if # the stripe.user_data.include option is being used. # stripe.user_data.exclude: [] - # An array of strings to specify which keys inside a Stripe event's user_data hash - # should be reported - # to New Relic. Each string in this array will be turned into a regular expression - # via Regexp.new to - # permit advanced matching. Setting the value to ["."] will report all user_data. + # An array of strings to specify which keys inside a Stripe event's user_data + # hash should be reported + # to New Relic. Each string in this array will be turned into a regular + # expression via Regexp.new to + # permit advanced matching. Setting the value to ["."] will report all + # user_data. # stripe.user_data.include: [] # When set to true, forces a synchronous connection to the New Relic collector @@ -758,8 +820,8 @@ common: &default_settings # If true, the agent captures attributes from transaction events. # transaction_events.attributes.enabled: true - # Prefix of attributes to exclude from transaction events. Allows * as wildcard at - # end. + # Prefix of attributes to exclude from transaction events. Allows * as wildcard + # at end. # transaction_events.attributes.exclude: [] # Prefix of attributes to include in transaction events. Allows * as wildcard at @@ -769,25 +831,26 @@ common: &default_settings # If true, enables transaction event sampling. # transaction_events.enabled: true - # Defines the maximum number of transaction events reported from a single harvest. + # Defines the maximum number of transaction events reported from a single + # harvest. # transaction_events.max_samples_stored: 1200 # If true, the agent captures attributes on transaction segments. # transaction_segments.attributes.enabled: true - # Prefix of attributes to exclude from transaction segments. Allows * as wildcard - # at end. + # Prefix of attributes to exclude from transaction segments. Allows * as + # wildcard at end. # transaction_segments.attributes.exclude: [] - # Prefix of attributes to include on transaction segments. Allows * as wildcard at - # end. + # Prefix of attributes to include on transaction segments. Allows * as wildcard + # at end. # transaction_segments.attributes.include: [] # If true, the agent captures attributes from transaction traces. # transaction_tracer.attributes.enabled: true - # Prefix of attributes to exclude from transaction traces. Allows * as wildcard at - # end. + # Prefix of attributes to exclude from transaction traces. Allows * as wildcard + # at end. # transaction_tracer.attributes.exclude: [] # Prefix of attributes to include in transaction traces. Allows * as wildcard at diff --git a/test/agent_helper.rb b/test/agent_helper.rb index dbcabd1955..f53f4b7af3 100644 --- a/test/agent_helper.rb +++ b/test/agent_helper.rb @@ -1043,3 +1043,12 @@ def first_call_for(subject) def ruby_version_float RUBY_VERSION.split('.')[0..1].join('.').to_f end + +def rails_version + @rails_version ||= Gem::Version.new(Rails::VERSION::STRING) +end + +def rails_version_at_least?(version_string) + version_string += '.0' until version_string.count('.') >= 2 # '7' => '7.0.0' + rails_version >= Gem::Version.new(version_string) +end diff --git a/test/environments/rails71/Gemfile b/test/environments/rails71/Gemfile new file mode 100644 index 0000000000..d6090d7871 --- /dev/null +++ b/test/environments/rails71/Gemfile @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'rails', '~> 7.1.3' +gem 'bootsnap', '>= 1.4.4', require: false + +gem 'minitest', '5.2.3' +gem 'minitest-stub-const', '~> 0.6' +gem 'mocha', '~> 1.16', require: false + +platforms :ruby, :rbx do + gem 'mysql2', '>= 0.5.4' + gem 'sqlite3', '~> 1.4' +end + +gem 'newrelic_rpm', path: '../../..' + +group :development do + if ENV['ENABLE_PRY'] + gem 'pry', '~> 0.9.12' + gem 'pry-nav' + end +end + +gem 'simplecov' if ENV['VERBOSE_TEST_OUTPUT'] +gem 'warning' + +if RUBY_VERSION.split('.')[0..1].join('.').to_f >= 3.4 + gem 'base64' + gem 'bigdecimal' + gem 'mutex_m' + gem 'ostruct' +end diff --git a/test/environments/rails71/Rakefile b/test/environments/rails71/Rakefile new file mode 100644 index 0000000000..8c74a5a9a9 --- /dev/null +++ b/test/environments/rails71/Rakefile @@ -0,0 +1,17 @@ +# This file is distributed under new relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/license for complete details. +# frozen_string_literal: true + +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative '../../warning_test_helper' + +require_relative 'config/application' + +Rails.application.load_tasks + +require 'tasks/all' + +Rake::Task['default'].clear +task :default => [:'test:newrelic'] diff --git a/test/environments/rails71/app/assets/config/manifest.js b/test/environments/rails71/app/assets/config/manifest.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/environments/rails71/app/controllers/application_controller.rb b/test/environments/rails71/app/controllers/application_controller.rb new file mode 100644 index 0000000000..612314dd94 --- /dev/null +++ b/test/environments/rails71/app/controllers/application_controller.rb @@ -0,0 +1,6 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +class ApplicationController < ActionController::Base +end diff --git a/test/environments/rails71/app/controllers/no_method_controller.rb b/test/environments/rails71/app/controllers/no_method_controller.rb new file mode 100644 index 0000000000..70a729db3f --- /dev/null +++ b/test/environments/rails71/app/controllers/no_method_controller.rb @@ -0,0 +1,7 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require_relative 'application_controller' + +class NoMethodController < ApplicationController; end diff --git a/test/environments/rails71/config/application.rb b/test/environments/rails71/config/application.rb new file mode 100644 index 0000000000..df9629d77f --- /dev/null +++ b/test/environments/rails71/config/application.rb @@ -0,0 +1,20 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require_relative 'boot' + +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module RpmTestApp + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults(7.1) + config.eager_load = false + config.filter_parameters += [:password] + end +end diff --git a/test/environments/rails71/config/boot.rb b/test/environments/rails71/config/boot.rb new file mode 100644 index 0000000000..a8d25cb53d --- /dev/null +++ b/test/environments/rails71/config/boot.rb @@ -0,0 +1,8 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/test/environments/rails71/config/database.yml b/test/environments/rails71/config/database.yml new file mode 100644 index 0000000000..5e490dbd99 --- /dev/null +++ b/test/environments/rails71/config/database.yml @@ -0,0 +1,30 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +mysql: &mysql + adapter: mysql2 + username: root + password: <%= ENV['MYSQL_PASSWORD'] %> + host: localhost + database: <%= db = "#{ENV['RUBY_VERSION']}#{ENV['BRANCH']}"; db.empty? ? "rails_blog" : db %> + +sqlite3: &sqlite3 +<% if defined?(JRuby) %> + adapter: jdbcsqlite3 +<% else %> + adapter: sqlite3 +<% end %> + database: db/all.sqlite3 + pool: 5 + timeout: 5000 + host: localhost + +development: + <<: *sqlite3 + +test: + <<: *sqlite3 + +production: + <<: *sqlite3 diff --git a/test/environments/rails71/config/environment.rb b/test/environments/rails71/config/environment.rb new file mode 100644 index 0000000000..d0c0782949 --- /dev/null +++ b/test/environments/rails71/config/environment.rb @@ -0,0 +1,9 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/test/environments/rails71/config/initializers/test.rb b/test/environments/rails71/config/initializers/test.rb new file mode 100644 index 0000000000..9981aac1a3 --- /dev/null +++ b/test/environments/rails71/config/initializers/test.rb @@ -0,0 +1,20 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require 'new_relic/agent/method_tracer' + +class Bloodhound < ActiveRecord::Base + include ::NewRelic::Agent::MethodTracer + + def sniff + puts 'When a bloodhound sniffs a scent article (a piece of clothing or item touched only by the subject), ' \ + "air rushes through its nasal cavity and chemical vapors — or odors — lodge in the mucus and bombard the dog's " \ + 'scent receptors. ' \ + 'Source: https://www.pbs.org/wnet/nature/underdogs-the-bloodhounds-amazing-sense-of-smell/350/' + end + + add_method_tracer :sniff +end + +Rails.application.config.active_record.timestamped_migrations = false diff --git a/test/environments/rails71/db/schema.rb b/test/environments/rails71/db/schema.rb new file mode 100644 index 0000000000..2d964da716 --- /dev/null +++ b/test/environments/rails71/db/schema.rb @@ -0,0 +1,5 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +# File is required to exist by Rails diff --git a/test/environments/rails72/Gemfile b/test/environments/rails72/Gemfile new file mode 100644 index 0000000000..24c6e38afe --- /dev/null +++ b/test/environments/rails72/Gemfile @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gem 'rails', '~> 7.2.0.beta2' +gem 'bootsnap', '>= 1.4.4', require: false + +gem 'minitest', '5.2.3' +gem 'minitest-stub-const', '~> 0.6' +gem 'mocha', '~> 1.16', require: false + +platforms :ruby, :rbx do + gem 'mysql2', '>= 0.5.4' + gem 'sqlite3', '~> 1.4' +end + +gem 'newrelic_rpm', path: '../../..' + +group :development do + if ENV['ENABLE_PRY'] + gem 'pry', '~> 0.9.12' + gem 'pry-nav' + end +end + +gem 'simplecov' if ENV['VERBOSE_TEST_OUTPUT'] +gem 'warning' + +if RUBY_VERSION.split('.')[0..1].join('.').to_f >= 3.4 + gem 'base64' + gem 'bigdecimal' + gem 'mutex_m' + gem 'ostruct' +end diff --git a/test/environments/rails72/Rakefile b/test/environments/rails72/Rakefile new file mode 100644 index 0000000000..8c74a5a9a9 --- /dev/null +++ b/test/environments/rails72/Rakefile @@ -0,0 +1,17 @@ +# This file is distributed under new relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/license for complete details. +# frozen_string_literal: true + +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative '../../warning_test_helper' + +require_relative 'config/application' + +Rails.application.load_tasks + +require 'tasks/all' + +Rake::Task['default'].clear +task :default => [:'test:newrelic'] diff --git a/test/environments/rails72/app/assets/config/manifest.js b/test/environments/rails72/app/assets/config/manifest.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/environments/rails72/app/controllers/application_controller.rb b/test/environments/rails72/app/controllers/application_controller.rb new file mode 100644 index 0000000000..612314dd94 --- /dev/null +++ b/test/environments/rails72/app/controllers/application_controller.rb @@ -0,0 +1,6 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +class ApplicationController < ActionController::Base +end diff --git a/test/environments/rails72/app/controllers/no_method_controller.rb b/test/environments/rails72/app/controllers/no_method_controller.rb new file mode 100644 index 0000000000..70a729db3f --- /dev/null +++ b/test/environments/rails72/app/controllers/no_method_controller.rb @@ -0,0 +1,7 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require_relative 'application_controller' + +class NoMethodController < ApplicationController; end diff --git a/test/environments/rails72/config/application.rb b/test/environments/rails72/config/application.rb new file mode 100644 index 0000000000..a5d6c01a28 --- /dev/null +++ b/test/environments/rails72/config/application.rb @@ -0,0 +1,20 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require_relative 'boot' + +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module RpmTestApp + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults(7.2) + config.eager_load = false + config.filter_parameters += [:password] + end +end diff --git a/test/environments/rails72/config/boot.rb b/test/environments/rails72/config/boot.rb new file mode 100644 index 0000000000..a8d25cb53d --- /dev/null +++ b/test/environments/rails72/config/boot.rb @@ -0,0 +1,8 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/test/environments/rails72/config/database.yml b/test/environments/rails72/config/database.yml new file mode 100644 index 0000000000..5e490dbd99 --- /dev/null +++ b/test/environments/rails72/config/database.yml @@ -0,0 +1,30 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +mysql: &mysql + adapter: mysql2 + username: root + password: <%= ENV['MYSQL_PASSWORD'] %> + host: localhost + database: <%= db = "#{ENV['RUBY_VERSION']}#{ENV['BRANCH']}"; db.empty? ? "rails_blog" : db %> + +sqlite3: &sqlite3 +<% if defined?(JRuby) %> + adapter: jdbcsqlite3 +<% else %> + adapter: sqlite3 +<% end %> + database: db/all.sqlite3 + pool: 5 + timeout: 5000 + host: localhost + +development: + <<: *sqlite3 + +test: + <<: *sqlite3 + +production: + <<: *sqlite3 diff --git a/test/environments/rails72/config/environment.rb b/test/environments/rails72/config/environment.rb new file mode 100644 index 0000000000..d0c0782949 --- /dev/null +++ b/test/environments/rails72/config/environment.rb @@ -0,0 +1,9 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/test/environments/rails72/config/initializers/test.rb b/test/environments/rails72/config/initializers/test.rb new file mode 100644 index 0000000000..9981aac1a3 --- /dev/null +++ b/test/environments/rails72/config/initializers/test.rb @@ -0,0 +1,20 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require 'new_relic/agent/method_tracer' + +class Bloodhound < ActiveRecord::Base + include ::NewRelic::Agent::MethodTracer + + def sniff + puts 'When a bloodhound sniffs a scent article (a piece of clothing or item touched only by the subject), ' \ + "air rushes through its nasal cavity and chemical vapors — or odors — lodge in the mucus and bombard the dog's " \ + 'scent receptors. ' \ + 'Source: https://www.pbs.org/wnet/nature/underdogs-the-bloodhounds-amazing-sense-of-smell/350/' + end + + add_method_tracer :sniff +end + +Rails.application.config.active_record.timestamped_migrations = false diff --git a/test/environments/rails72/db/schema.rb b/test/environments/rails72/db/schema.rb new file mode 100644 index 0000000000..2d964da716 --- /dev/null +++ b/test/environments/rails72/db/schema.rb @@ -0,0 +1,5 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +# File is required to exist by Rails diff --git a/test/environments/railsedge/Gemfile b/test/environments/railsedge/Gemfile index 5bc73b6cde..70e0119b67 100644 --- a/test/environments/railsedge/Gemfile +++ b/test/environments/railsedge/Gemfile @@ -25,3 +25,10 @@ end gem 'simplecov' if ENV['VERBOSE_TEST_OUTPUT'] gem 'warning' + +if RUBY_VERSION.split('.')[0..1].join('.').to_f >= 3.4 + gem 'base64' + gem 'bigdecimal' + gem 'mutex_m' + gem 'ostruct' +end diff --git a/test/environments/railsedge/config/application.rb b/test/environments/railsedge/config/application.rb index 2565b8762e..930b300484 100644 --- a/test/environments/railsedge/config/application.rb +++ b/test/environments/railsedge/config/application.rb @@ -13,7 +13,7 @@ module RpmTestApp class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults(7.0) # TODO: keep this number up to date (and keep this TODO) + config.load_defaults(8.0) # TODO: keep this number up to date (and keep this TODO) config.eager_load = false config.filter_parameters += [:password] end diff --git a/test/helpers/misc.rb b/test/helpers/misc.rb index 47717db285..2f0c0b3094 100644 --- a/test/helpers/misc.rb +++ b/test/helpers/misc.rb @@ -131,6 +131,12 @@ def skip_unless_ci_cron skip 'This test only runs as part of the CI cron workflow' end +def skip_unless_special_ci + return if ENV['SPECIAL_CI'] + + skip 'This test only runs as part of the special CI workflow' +end + def agent_root @agent_root ||= File.expand_path('../../..', __FILE__).freeze end diff --git a/test/multiverse/suites/active_record/active_record_test.rb b/test/multiverse/suites/active_record/active_record_test.rb index 4fc0f0e1b9..6cc148c27d 100644 --- a/test/multiverse/suites/active_record/active_record_test.rb +++ b/test/multiverse/suites/active_record/active_record_test.rb @@ -8,35 +8,6 @@ class ActiveRecordInstrumentationTest < Minitest::Test include MultiverseHelpers setup_and_teardown_agent - module VersionHelpers - def active_record_major_version - if defined?(::ActiveRecord::VERSION::MAJOR) - ::ActiveRecord::VERSION::MAJOR.to_i - else - 2 - end - end - - def active_record_minor_version - if defined?(::ActiveRecord::VERSION::MINOR) - ::ActiveRecord::VERSION::MINOR.to_i - else - 1 - end - end - - def active_record_version - if defined?(::ActiveRecord::VERSION::MINOR) - Gem::Version.new(::ActiveRecord::VERSION::STRING) - else - Gem::Version.new('2.1.0') # Can't tell between 2.1 and 2.2. Meh. - end - end - end - - include VersionHelpers - extend VersionHelpers - def after_setup super NewRelic::Agent.drop_buffered_data @@ -51,31 +22,23 @@ def test_metrics_for_calculation_methods Order.sum(:id) end - if active_record_major_version >= 3 - assert_activerecord_metrics(Order, 'select', :call_count => 5) - else - assert_generic_rollup_metrics('select') - end + assert_activerecord_metrics(Order, 'select', :call_count => 5) end - if active_record_version >= Gem::Version.new('3.2.0') - def test_metrics_for_pluck - in_web_transaction do - Order.pluck(:id) - end - - assert_activerecord_metrics(Order, 'select') + def test_metrics_for_pluck + in_web_transaction do + Order.pluck(:id) end - end - if active_record_version >= Gem::Version.new('4.0.0') - def test_metrics_for_ids - in_web_transaction do - Order.ids - end + assert_activerecord_metrics(Order, 'select') + end - assert_activerecord_metrics(Order, 'select') + def test_metrics_for_ids + in_web_transaction do + Order.ids end + + assert_activerecord_metrics(Order, 'select') end def test_metrics_for_create @@ -98,11 +61,7 @@ def test_metrics_for_create_via_association def test_metrics_for_find in_web_transaction do - if active_record_major_version >= 4 - Order.where(:name => 'foo').load - else - Order.find_all_by_name('foo') - end + Order.where(:name => 'foo').load end assert_activerecord_metrics(Order, 'find') @@ -120,38 +79,19 @@ def test_metrics_for_find_via_association def test_metrics_for_find_all in_web_transaction do - case - when active_record_major_version >= 4 - Order.all.load - when active_record_major_version >= 3 - Order.all - else - Order.find(:all) - end + Order.all.load end assert_activerecord_metrics(Order, 'find') end def test_metrics_for_find_via_named_scope - major_version = active_record_major_version - minor_version = active_record_minor_version Order.class_eval do - if major_version >= 4 - scope(:jeffs, lambda { where(:name => 'Jeff') }) - elsif major_version == 3 && minor_version >= 1 - scope(:jeffs, :conditions => {:name => 'Jeff'}) - else - named_scope(:jeffs, :conditions => {:name => 'Jeff'}) - end + scope(:jeffs, lambda { where(:name => 'Jeff') }) end in_web_transaction do - if active_record_major_version >= 4 - Order.jeffs.load - else - Order.jeffs.find(:all) - end + Order.jeffs.load end assert_activerecord_metrics(Order, 'find') @@ -162,12 +102,7 @@ def test_metrics_for_exists Order.exists?(['name=?', 'jeff']) end - if active_record_major_version == 3 && [0, 1].include?(active_record_minor_version) - # Bugginess in Rails 3.0 and 3.1 doesn't let us get ActiveRecord/find - assert_generic_rollup_metrics('select') - else - assert_activerecord_metrics(Order, 'find') - end + assert_activerecord_metrics(Order, 'find') end def test_metrics_for_update_all @@ -192,11 +127,7 @@ def test_metrics_for_delete_all Order.delete_all end - if active_record_major_version >= 3 - assert_activerecord_metrics(Order, 'delete') - else - assert_generic_rollup_metrics('delete') - end + assert_activerecord_metrics(Order, 'delete') end def test_metrics_for_relation_delete @@ -205,32 +136,25 @@ def test_metrics_for_relation_delete Order.delete(order.id) end - if active_record_major_version >= 3 - assert_activerecord_metrics(Order, 'delete') - else - assert_generic_rollup_metrics('delete') - end + assert_activerecord_metrics(Order, 'delete') end - # delete and touch did not exist in AR 2.2 - if active_record_version >= Gem::Version.new('3.0.0') - def test_metrics_for_delete - in_web_transaction do - order = Order.create('name' => 'burt') - order.delete - end - - assert_activerecord_metrics(Order, 'delete') + def test_metrics_for_delete + in_web_transaction do + order = Order.create('name' => 'burt') + order.delete end - def test_metrics_for_touch - in_web_transaction do - order = Order.create('name' => 'wendy') - order.touch - end + assert_activerecord_metrics(Order, 'delete') + end - assert_activerecord_metrics(Order, 'update') + def test_metrics_for_touch + in_web_transaction do + order = Order.create('name' => 'wendy') + order.touch end + + assert_activerecord_metrics(Order, 'update') end def test_metrics_for_relation_update @@ -252,11 +176,7 @@ def test_create_via_association_equal u.groups = groups end - if active_record_major_version >= 3 - assert_activerecord_metrics(Group, 'create') - else - assert_generic_rollup_metrics('insert') - end + assert_activerecord_metrics(Group, 'create') end # Can be Mysql2::Error or ActiveRecord::RecordNotUnique @@ -305,11 +225,7 @@ def test_create_via_association_shovel u.groups << Group.new(:name => 'radiohead') end - if active_record_major_version >= 3 - assert_activerecord_metrics(Group, 'create') - else - assert_generic_rollup_metrics('insert') - end + assert_activerecord_metrics(Group, 'create') end def test_create_via_association_create @@ -318,11 +234,7 @@ def test_create_via_association_create u.groups.create(:name => 'radiohead') end - if active_record_major_version >= 3 - assert_activerecord_metrics(Group, 'create') - else - assert_generic_rollup_metrics('insert') - end + assert_activerecord_metrics(Group, 'create') end def test_create_via_association_create_bang @@ -331,11 +243,7 @@ def test_create_via_association_create_bang u.groups.create!(:name => 'radiohead') end - if active_record_major_version >= 3 - assert_activerecord_metrics(Group, 'create') - else - assert_generic_rollup_metrics('insert') - end + assert_activerecord_metrics(Group, 'create') end def test_destroy_via_dependent_destroy @@ -349,25 +257,22 @@ def test_destroy_via_dependent_destroy assert_activerecord_metrics(Alias, 'delete') end - # update & update! didn't become public until 4.0 - if active_record_version >= Gem::Version.new('4.0.0') - def test_metrics_for_update - in_web_transaction do - order = Order.create(:name => 'wendy') - order.update(:name => 'walter') - end - - assert_activerecord_metrics(Order, 'update') + def test_metrics_for_update + in_web_transaction do + order = Order.create(:name => 'wendy') + order.update(:name => 'walter') end - def test_metrics_for_update_bang - in_web_transaction do - order = Order.create(:name => 'wendy') - order.update!(:name => 'walter') - end + assert_activerecord_metrics(Order, 'update') + end - assert_activerecord_metrics(Order, 'update') + def test_metrics_for_update_bang + in_web_transaction do + order = Order.create(:name => 'wendy') + order.update!(:name => 'walter') end + + assert_activerecord_metrics(Order, 'update') end def test_metrics_for_update_attribute @@ -601,6 +506,8 @@ def test_with_database_metric_name ## helpers + private + def adapter adapter_string = Order.configurations[RAILS_ENV]['adapter'] adapter_string.downcase.to_sym @@ -619,12 +526,10 @@ def current_product end def operation_for(op) - is_5_2 = active_record_version >= Gem::Version.new('5.2.0.beta1') - if op == 'create' - active_record_major_version >= 3 && !is_5_2 ? 'insert' : 'create' - elsif op == 'delete' - active_record_major_version >= 3 && !is_5_2 ? 'delete' : 'destroy' + active_record_version_at_least?(5.2) ? 'create' : 'insert' + elsif op == 'delete' && !active_record_version_at_least?(7) + active_record_version_at_least?(5.2) ? 'destroy' : 'delete' else op end @@ -648,4 +553,14 @@ def assert_generic_rollup_metrics(operation) 'Datastore/all' ]) end + + def active_record_version + Gem::Version.new(::ActiveRecord::VERSION::STRING) + end + + def active_record_version_at_least?(version_string) + version_string = version_string.to_s + version_string += '.0' until version_string.count('.') >= 2 # '7' => '7.0.0' + active_record_version >= Gem::Version.new(version_string) + end end diff --git a/test/multiverse/suites/active_record_pg/Envfile b/test/multiverse/suites/active_record_pg/Envfile index 3947564ad8..68d718c94d 100644 --- a/test/multiverse/suites/active_record_pg/Envfile +++ b/test/multiverse/suites/active_record_pg/Envfile @@ -22,7 +22,7 @@ ACTIVERECORD_VERSIONS = [ ['5.0.0', 2.4, 2.7] ] -# unshift_rails_edge(ACTIVERECORD_VERSIONS) +unshift_rails_edge(ACTIVERECORD_VERSIONS) def gem_list(activerecord_version = nil) <<~RB diff --git a/test/multiverse/suites/active_record_pg/active_record_test.rb b/test/multiverse/suites/active_record_pg/active_record_test.rb index c670380fd8..470a267c06 100644 --- a/test/multiverse/suites/active_record_pg/active_record_test.rb +++ b/test/multiverse/suites/active_record_pg/active_record_test.rb @@ -8,31 +8,6 @@ class ActiveRecordInstrumentationTest < Minitest::Test include MultiverseHelpers setup_and_teardown_agent - module VersionHelpers - def active_record_major_version - if defined?(::ActiveRecord::VERSION::MAJOR) - ::ActiveRecord::VERSION::MAJOR.to_i - else - 2 - end - end - - def active_record_minor_version - if defined?(::ActiveRecord::VERSION::MINOR) - ::ActiveRecord::VERSION::MINOR.to_i - else - 1 - end - end - - def active_record_version - Gem::Version.new(::ActiveRecord::VERSION::STRING) - end - end - - include VersionHelpers - extend VersionHelpers - def after_setup super NewRelic::Agent.drop_buffered_data @@ -47,7 +22,7 @@ def test_metrics_for_calculation_methods Order.sum(:id) end - if active_record_major_version >= 7 + if active_record_version_at_least?(7) assert_activerecord_metrics(Order, 'find') else assert_activerecord_metrics(Order, 'select', call_count: '>= 5') @@ -59,7 +34,7 @@ def test_metrics_for_pluck Order.pluck(:id) end - if active_record_major_version >= 7 + if active_record_version_at_least?(7) assert_activerecord_metrics(Order, 'pluck') else assert_activerecord_metrics(Order, 'select') @@ -71,12 +46,10 @@ def test_metrics_for_ids Order.ids end - if active_record_major_version >= 7 - if active_record_minor_version >= 1 - assert_activerecord_metrics(Order, 'ids') - else - assert_activerecord_metrics(Order, 'pluck') - end + if active_record_version_at_least?(7.1) + assert_activerecord_metrics(Order, 'ids') + elsif active_record_version_at_least?(7) + assert_activerecord_metrics(Order, 'pluck') else assert_activerecord_metrics(Order, 'select') end @@ -102,11 +75,7 @@ def test_metrics_for_create_via_association def test_metrics_for_find in_web_transaction do - if active_record_major_version >= 4 - Order.where(:name => 'foo').load - else - Order.find_all_by_name('foo') - end + Order.where(:name => 'foo').load end assert_activerecord_metrics(Order, 'find') @@ -124,35 +93,19 @@ def test_metrics_for_find_via_association def test_metrics_for_find_all in_web_transaction do - if active_record_major_version >= 4 - Order.all.load - else - Order.all - end + Order.all.load end assert_activerecord_metrics(Order, 'find') end def test_metrics_for_find_via_named_scope - major_version = active_record_major_version - minor_version = active_record_minor_version Order.class_eval do - if major_version >= 4 - scope(:jeffs, lambda { where(:name => 'Jeff') }) - elsif major_version == 3 && minor_version >= 1 - scope(:jeffs, :conditions => {:name => 'Jeff'}) - else - named_scope(:jeffs, :conditions => {:name => 'Jeff'}) - end + scope(:jeffs, lambda { where(:name => 'Jeff') }) end in_web_transaction do - if active_record_major_version >= 4 - Order.jeffs.load - else - Order.jeffs.find(:all) - end + Order.jeffs.load end assert_activerecord_metrics(Order, 'find') @@ -163,7 +116,7 @@ def test_metrics_for_exists Order.exists?(['name=?', 'jeff']) end - if active_record_major_version >= 6 + if active_record_version_at_least?(6) assert_activerecord_metrics(Order, 'exists?') else assert_activerecord_metrics(Order, 'find') @@ -210,7 +163,7 @@ def test_metrics_for_delete order.delete end - if active_record_major_version >= 7 + if active_record_version_at_least?(7) assert_activerecord_metrics(Order, 'destroy') else assert_activerecord_metrics(Order, 'delete') @@ -322,7 +275,7 @@ def test_destroy_via_dependent_destroy u.destroy end - if active_record_major_version >= 7 + if active_record_version_at_least?(7) assert_activerecord_metrics(User, 'destroy') assert_activerecord_metrics(Alias, 'destroy') else @@ -331,25 +284,22 @@ def test_destroy_via_dependent_destroy end end - # update & update! didn't become public until 4.0 - if active_record_version >= Gem::Version.new('4.0.0') - def test_metrics_for_update - in_web_transaction do - order = Order.create(:name => 'wendy') - order.update(:name => 'walter') - end - - assert_activerecord_metrics(Order, 'update') + def test_metrics_for_update + in_web_transaction do + order = Order.create(:name => 'wendy') + order.update(:name => 'walter') end - def test_metrics_for_update_bang - in_web_transaction do - order = Order.create(:name => 'wendy') - order.update!(:name => 'walter') - end + assert_activerecord_metrics(Order, 'update') + end - assert_activerecord_metrics(Order, 'update') + def test_metrics_for_update_bang + in_web_transaction do + order = Order.create(:name => 'wendy') + order.update!(:name => 'walter') end + + assert_activerecord_metrics(Order, 'update') end def test_metrics_for_update_attribute @@ -398,7 +348,7 @@ def test_metrics_for_destroy order.destroy end - if active_record_major_version >= 7 + if active_record_version_at_least?(7) assert_activerecord_metrics(Order, 'destroy') else assert_activerecord_metrics(Order, 'delete') @@ -586,7 +536,7 @@ def test_with_database_metric_name end def test_metrics_for_async_find_by_sql - skip_unless_active_record_7_1_or_above + skip unless active_record_version_at_least?(7.1) in_web_transaction do Order.async_find_by_sql('SELECT * FROM orders') @@ -596,7 +546,7 @@ def test_metrics_for_async_find_by_sql end def test_metrics_for_async_count_by_sql - skip_unless_active_record_7_1_or_above + skip unless active_record_version_at_least?(7.1) in_web_transaction do Order.create(:name => 'wendy') @@ -607,7 +557,7 @@ def test_metrics_for_async_count_by_sql end def test_metrics_for_async_pluck - skip_unless_active_record_7_1_or_above + skip unless active_record_version_at_least?(7.1) in_web_transaction do Order.async_pluck(:id) @@ -617,7 +567,7 @@ def test_metrics_for_async_pluck end def test_metrics_for_async_calculation_methods - skip_unless_active_record_7_1_or_above + skip unless active_record_version_at_least?(7.1) in_web_transaction do Order.async_count @@ -631,6 +581,7 @@ def test_metrics_for_async_calculation_methods end ## helpers + private def adapter # ActiveRecord::Base.configurations[NewRelic::Control.instance.env]['adapter'] @@ -654,13 +605,10 @@ def current_product end def operation_for(op) - is_greater_than_5_2 = active_record_version >= Gem::Version.new('5.2.0.beta1') - is_less_than_7_0 = active_record_version < Gem::Version.new('7.0.0.alpha1') - if op == 'create' - !is_greater_than_5_2 ? 'insert' : 'create' - elsif op == 'delete' && is_less_than_7_0 - !is_greater_than_5_2 ? 'delete' : 'destroy' + active_record_version_at_least?(5.2) ? 'create' : 'insert' + elsif op == 'delete' && !active_record_version_at_least?(7) + active_record_version_at_least?(5.2) ? 'destroy' : 'delete' else op end @@ -685,7 +633,13 @@ def assert_generic_rollup_metrics(operation) ]) end - def skip_unless_active_record_7_1_or_above - skip unless active_record_major_version >= 7 && active_record_minor_version >= 1 + def active_record_version + Gem::Version.new(::ActiveRecord::VERSION::STRING) + end + + def active_record_version_at_least?(version_string) + version_string = version_string.to_s + version_string += '.0' until version_string.count('.') >= 2 # '7' => '7.0.0' + active_record_version >= Gem::Version.new(version_string) end end diff --git a/test/multiverse/suites/active_support_broadcast_logger/Envfile b/test/multiverse/suites/active_support_broadcast_logger/Envfile index 9a6b79c4fc..9a274d0c4f 100644 --- a/test/multiverse/suites/active_support_broadcast_logger/Envfile +++ b/test/multiverse/suites/active_support_broadcast_logger/Envfile @@ -11,7 +11,7 @@ ACTIVE_SUPPORT_VERSIONS = [ ['7.1.0', 2.7] ] -# unshift_rails_edge(ACTIVE_SUPPORT_VERSIONS) +unshift_rails_edge(ACTIVE_SUPPORT_VERSIONS) def gem_list(activesupport_version = nil) <<-RB diff --git a/test/multiverse/suites/bunny/bunny_test.rb b/test/multiverse/suites/bunny/bunny_test.rb index 86f5bb77d4..52cb4fbbc9 100644 --- a/test/multiverse/suites/bunny/bunny_test.rb +++ b/test/multiverse/suites/bunny/bunny_test.rb @@ -15,6 +15,9 @@ class BunnyTest < Minitest::Test end def teardown + harvest_span_events! + mocha_teardown + NewRelic::Agent.instance.stats_engine.clear_stats @conn.close end @@ -333,6 +336,40 @@ def test_pop_returning_no_message_doesnt_error end end + def test_pop_adds_attributes_to_span + with_queue do |queue| + queue.publish('test_msg', routing_key: queue.name) + in_transaction('test_txn') do |txn| + txn.stubs(:sampled?).returns(true) + + queue.pop + end + spans = harvest_span_events! + + assert_equal @conn.hostname, spans[1][0][2]['server.address'] + assert_equal 5672, spans[1][0][2]['server.port'] + assert_equal 'Default', spans[1][0][2]['messaging.destination_publish.name'] + assert_equal queue.name, spans[1][0][2]['messaging.destination.name'] + assert_equal queue.name, spans[1][0][2]['messaging.rabbitmq.destination.routing_key'] + assert_equal queue.name, spans[1][0][2]['message.queueName'] + end + end + + def test_publish_adds_attributes_to_span + with_queue do |queue| + in_transaction('test_txn') do |txn| + txn.stubs(:sampled?).returns(true) + queue.publish('test_msg', routing_key: queue.name) + end + spans = harvest_span_events! + + assert_equal @conn.hostname, spans[1][0][2]['server.address'] + assert_equal 5672, spans[1][0][2]['server.port'] + assert_equal 'Default', spans[1][0][2]['messaging.destination.name'] + assert_equal queue.name, spans[1][0][2]['messaging.rabbitmq.destination.routing_key'] + end + end + def test_pop_returning_a_good_message_send_to_an_exchange_we_havent_accessed_doesnt_error NewRelic::Agent.stubs(:logger).returns(NewRelic::Agent::MemoryLogger.new) diff --git a/test/multiverse/suites/elasticsearch/elasticsearch_instrumentation_test.rb b/test/multiverse/suites/elasticsearch/elasticsearch_instrumentation_test.rb index 1427f0dd6c..33ea640617 100644 --- a/test/multiverse/suites/elasticsearch/elasticsearch_instrumentation_test.rb +++ b/test/multiverse/suites/elasticsearch/elasticsearch_instrumentation_test.rb @@ -102,6 +102,15 @@ def test_segment_database_name assert_equal 'docker-cluster', @segment.database_name end + def test_cluster_name_doesnt_try_again_if_defined_but_nil + original = @client.instance_variable_get(:@transport).instance_variable_get(:@nr_cluster_name) + @client.instance_variable_get(:@transport).instance_variable_set(:@nr_cluster_name, nil) + search + @client.instance_variable_get(:@transport).instance_variable_set(:@nr_cluster_name, original) + + assert_nil @segment.database_name + end + def test_nosql_statement_recorded_params_obfuscated with_config(:'elasticsearch.obfuscate_queries' => true) do txn = in_transaction do diff --git a/test/multiverse/suites/httpx/httpx_instrumentation_test.rb b/test/multiverse/suites/httpx/httpx_instrumentation_test.rb index 1190e924ee..ddefbd430f 100644 --- a/test/multiverse/suites/httpx/httpx_instrumentation_test.rb +++ b/test/multiverse/suites/httpx/httpx_instrumentation_test.rb @@ -33,7 +33,15 @@ def test_finish_with_error request = Minitest::Mock.new request.expect :response, :the_response 2.times { request.expect :hash, 1138 } - responses = {request => ::HTTPX::ErrorResponse.new(request, StandardError.new, {})} + + error = if Gem::Version.new(::HTTPX::VERSION) >= Gem::Version.new('1.3.0') + request.expect :options, ::HTTPX::Options.new({}) + ::HTTPX::ErrorResponse.new(request, StandardError.new) + else + ::HTTPX::ErrorResponse.new(request, StandardError.new, {}) + end + responses = {request => error} + segment = Minitest::Mock.new def segment.notice_error(_error); end def segment.process_response_headers(_wrappep); end diff --git a/test/multiverse/suites/logstasher/Envfile b/test/multiverse/suites/logstasher/Envfile new file mode 100644 index 0000000000..ae515679de --- /dev/null +++ b/test/multiverse/suites/logstasher/Envfile @@ -0,0 +1,19 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +instrumentation_methods :chain, :prepend + +logstasher_versions = [ + [nil, 2.7] +] + +# Lock down activesupport version due to a logstasher test incompatiability with 7.1. +def gem_list(logstasher_versions = nil) + <<~RB + gem 'logstasher'#{logstasher_versions} + gem 'activesupport', '< 7.1' + RB +end + +create_gemfiles(logstasher_versions) diff --git a/test/multiverse/suites/logstasher/config/newrelic.yml b/test/multiverse/suites/logstasher/config/newrelic.yml new file mode 100644 index 0000000000..3c27a58cce --- /dev/null +++ b/test/multiverse/suites/logstasher/config/newrelic.yml @@ -0,0 +1,22 @@ +--- +development: + error_collector: + enabled: true + apdex_t: 0.5 + monitor_mode: true + license_key: bootstrap_newrelic_admin_license_key_000 + instrumentation: + logstasher: <%= $instrumentation_method %> + app_name: test + log_level: debug + host: 127.0.0.1 + api_host: 127.0.0.1 + transaction_trace: + record_sql: obfuscated + enabled: true + stack_trace_threshold: 0.5 + transaction_threshold: 1.0 + capture_params: false + application_logging: + forwarding: + enabled: true diff --git a/test/multiverse/suites/logstasher/logstasher_instrumentation_test.rb b/test/multiverse/suites/logstasher/logstasher_instrumentation_test.rb new file mode 100644 index 0000000000..9c1df1f06f --- /dev/null +++ b/test/multiverse/suites/logstasher/logstasher_instrumentation_test.rb @@ -0,0 +1,131 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require 'ostruct' + +class LogStasherInstrumentationTest < Minitest::Test + include MultiverseHelpers + + def setup + @written = StringIO.new + + # Give LogStasher's setup method a new place to write logs to, as well as + # skip a config check, controller_monkey_patch, that otherwise causes an error + LogStasher.setup(OpenStruct.new(logger_path: @written, controller_monkey_patch: false)) + + # required for build_logstasher_event method + LogStasher.field_renaming = {} + @aggregator = NewRelic::Agent.agent.log_event_aggregator + + NewRelic::Agent.instance.stats_engine.reset! + NewRelic::Agent.instance.log_event_aggregator.reset! + end + + def teardown + NewRelic::Agent.instance.stats_engine.reset! + NewRelic::Agent.instance.log_event_aggregator.reset! + end + + def json_log_hash + { + :identifier => 'dinosaurs/_dinosaur.html.erb', + :name => 'render_partial.action_view', + :request_id => '01234-abcde-56789-fghij', + 'source' => '127.0.0.1', + 'tags' => [], + '@timestamp' => '2024-06-24T23:55:59.497Z' + } + end + + def test_level_is_recorded + in_transaction do + LogStasher.build_logstash_event({'level' => :info, 'message' => 'hi there'}, ['log']) + end + _, events = @aggregator.harvest! + + assert_equal 'INFO', events[0][1]['level'] + assert_metrics_recorded(%w[Logging/lines/INFO]) + end + + def test_logs_without_levels_are_unknown + in_transaction do + LogStasher.build_logstash_event(json_log_hash, ['log']) + end + _, events = @aggregator.harvest! + + assert_equal 'UNKNOWN', events[0][1]['level'] + assert_metrics_recorded(%w[Logging/lines/UNKNOWN]) + end + + def test_logs_without_messages_are_not_added + in_transaction do + LogStasher.build_logstash_event(json_log_hash, ['log']) + end + _, events = @aggregator.harvest! + + refute events[0][1]['message'] + end + + def test_attributes_added_to_payload + in_transaction do + LogStasher.build_logstash_event(json_log_hash, ['log']) + end + _, events = @aggregator.harvest! + + assert events[0][1]['attributes'].key?(:identifier) + assert events[0][1]['attributes'].key?(:name) + assert events[0][1]['attributes'].key?('source') + end + + def test_log_decorating_enabled_records_linking_metadata + with_config(:'application_logging.local_decorating.enabled' => true) do + in_transaction do + LogStasher.warn('yikes') + end + end + logfile = JSON.parse(@written.string) + + assert logfile.key?('entity.name') + assert logfile.key?('entity.type') + assert logfile.key?('hostname') + assert logfile.key?('trace.id') + assert logfile.key?('span.id') + end + + def test_log_decorating_disabled_doesnt_records_linking_metadata + with_config(:'application_logging.local_decorating.enabled' => false) do + in_transaction do + LogStasher.warn('yikes') + end + end + logfile = JSON.parse(@written.string) + + refute logfile.key?('entity.name') + refute logfile.key?('entity.type') + refute logfile.key?('hostname') + refute logfile.key?('trace.id') + refute logfile.key?('span.id') + end + + def test_no_instrumentation_when_disabled + with_config(:'instrumentation.logstasher' => 'disabled') do + LogStasher.warn('yikes') + end + _, events = @aggregator.harvest! + + assert_empty(events) + end + + def test_enabled_returns_false_when_disabled + with_config(:'instrumentation.logstasher' => 'disabled') do + refute_predicate NewRelic::Agent::Instrumentation::LogStasher, :enabled? + end + end + + def test_enabled_returns_true_when_enabled + with_config(:'instrumentation.logstasher' => 'auto') do + assert_predicate NewRelic::Agent::Instrumentation::LogStasher, :enabled? + end + end +end diff --git a/test/multiverse/suites/rails/Envfile b/test/multiverse/suites/rails/Envfile index adcf97a305..d26fcad892 100644 --- a/test/multiverse/suites/rails/Envfile +++ b/test/multiverse/suites/rails/Envfile @@ -4,6 +4,7 @@ RAILS_VERSIONS = [ [nil, 3.1], + ['7.2.0.beta3', 3.1], ['7.1.0', 2.7], ['7.0.4', 2.7], ['6.1.7', 2.5], @@ -14,7 +15,7 @@ RAILS_VERSIONS = [ ['4.2.11', 2.4, 2.4] ] -# unshift_rails_edge(RAILS_VERSIONS) +unshift_rails_edge(RAILS_VERSIONS) def haml_rails(rails_version = nil) if rails_version && ( diff --git a/test/multiverse/suites/rails/action_controller_live_rum_test.rb b/test/multiverse/suites/rails/action_controller_live_rum_test.rb index ed21ace9d8..053c3c20e0 100644 --- a/test/multiverse/suites/rails/action_controller_live_rum_test.rb +++ b/test/multiverse/suites/rails/action_controller_live_rum_test.rb @@ -42,6 +42,9 @@ def test_excludes_rum_instrumentation_when_streaming_with_action_controller_live end def test_excludes_rum_instrumentation_when_streaming_with_action_stream_true + # Rails 8 stopped using the 'chunked' header value and defers all streaming responsibilities to Rack + skip if rails_version_at_least?('8.0.0.alpha') + get('/undead/brain_stream', env: {'HTTP_VERSION' => 'HTTP/1.1'}) assert_predicate(response, :ok?, 'Expected ActionController streaming response to be OK') diff --git a/test/multiverse/suites/rails/action_controller_other_test.rb b/test/multiverse/suites/rails/action_controller_other_test.rb index 672469d6b8..799ed069a8 100644 --- a/test/multiverse/suites/rails/action_controller_other_test.rb +++ b/test/multiverse/suites/rails/action_controller_other_test.rb @@ -3,10 +3,11 @@ # frozen_string_literal: true require './app' - if defined?(ActionController::Live) class DataController < ApplicationController + CACHE_KEY = :the_key + # send_file def send_test_file send_file(Rails.root + '../../../../README.md') @@ -35,6 +36,26 @@ def do_a_redirect def not_allowed params.permit(:only_this) end + + # exist_fragment? + def exist_fragment + fragment_exist?(CACHE_KEY) + end + + # expire_fragment + def expire_test_fragment + expire_fragment(CACHE_KEY) + end + + # read_fragment + def read_test_fragment + read_fragment(CACHE_KEY) + end + + # write_fragment + def write_test_fragment + write_fragment(CACHE_KEY, 'fragment') + end end class ActionControllerDataTest < ActionDispatch::IntegrationTest @@ -57,7 +78,9 @@ def test_send_data end def test_send_stream - skip if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new('7.2.0') + # rails/rails@ed68af0 now defers all streaming behavior over to Rack (v3+) itself, so test + # only versions >= 7.2 and < 8.0 + skip unless rails_version_at_least?('7.2') && !rails_version_at_least?('8.0.0.alpha') get('/data/send_test_stream') assert_metrics_recorded(['Controller/data/send_test_stream', 'Ruby/ActionController/send_stream']) @@ -79,7 +102,7 @@ def test_redirect_to get('/data/do_a_redirect') # payload does not include the request in rails < 6.1 - rails61 = Gem::Version.new(Rails::VERSION::STRING) >= Gem::Version.new('6.1.0') + rails61 = rails_version_at_least?('6.1') segment_name = if rails61 'Ruby/ActionController/data/redirect_to' @@ -96,8 +119,8 @@ def test_redirect_to end def test_unpermitted_parameters - skip if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new('6.0.0') # unpermitted parameters is only available in rails 6.0+ + skip unless rails_version_at_least?('6') get('/data/not_allowed', params: {this_is_a_param: 1}) @@ -121,6 +144,66 @@ def test_unpermitted_parameters assert_metrics_recorded(['Controller/data/not_allowed', segment_name]) end + def test_exist_fragment? + skip unless rails_version_at_least?('6') + + get('/data/exist_fragment') + + node = find_node_with_name_matching(last_transaction_trace, /ActionController/) + + assert node, 'Could not find an >>ActionController<< metric while testing >>exist_fragment?<<' + child_metric_partial_name = rails_version_at_least?('6.1') ? 'FileStore/exist' : 'ActiveSupport/exist?' + child = node.children.detect { |n| n.metric_name.include?(child_metric_partial_name) } + + assert child, 'Could not find a >>Filestore/exist<< child of the ActionController node!' + confirm_key_exists_in_params(child) + end + + def test_expire_fragment + skip unless rails_version_at_least?('6') + + get('/data/expire_test_fragment') + + node = find_node_with_name_matching(last_transaction_trace, /ActionController/) + + assert node, 'Could not find an >>ActionController<< metric while testing >>expire_fragment<<' + child_metric_partial_name = rails_version_at_least?('6.1') ? 'FileStore/delete' : 'ActiveSupport/delete' + child = node.children.detect { |n| n.metric_name.include?(child_metric_partial_name) } + + assert child, 'Could not find a >>Filestore/delete<< child of the ActionController node!' + confirm_key_exists_in_params(child) + end + + def test_read_fragment + skip unless rails_version_at_least?('6') + + get('/data/read_test_fragment') + + node = find_node_with_name_matching(last_transaction_trace, /ActionController/) + + assert node, 'Could not find an >>ActionController<< metric while testing >>read_fragment<<' + child_metric_partial_name = rails_version_at_least?('6.1') ? 'FileStore/read' : 'ActiveSupport/read' + child = node.children.detect { |n| n.metric_name.include?(child_metric_partial_name) } + + assert child, 'Could not find a >>Filestore/read<< child of the ActionController node!' + confirm_key_exists_in_params(child) + end + + def test_write_fragment + skip unless rails_version_at_least?('6') + + get('/data/write_test_fragment') + + node = find_node_with_name_matching(last_transaction_trace, /ActionController/) + + assert node, 'Could not find an >>ActionController<< metric while testing >>write_fragment<<' + child_metric_partial_name = rails_version_at_least?('6.1') ? 'FileStore/write' : 'ActiveSupport/write' + child = node.children.detect { |n| n.metric_name.include?(child_metric_partial_name) } + + assert child, 'Could not find a >>Filestore/write<< child of the ActionController node!' + confirm_key_exists_in_params(child) + end + class TestClassActionController; end def test_no_metric_naming_error @@ -142,5 +225,18 @@ def controller_class NewRelic::Agent::Instrumentation::ActionControllerOtherSubscriber.new.controller_name_for_metric(payload) end end + + private + + def confirm_key_exists_in_params(node) + assertion_failure = "Expected to find the cache key >>#{DataController::CACHE_KEY}<< in the node params!" + # Rails v7.2+ stores the URI string, so look for the key on the end of it + if rails_version_at_least?('7.2.0.beta1') + assert_match(/#{CGI.escape("/#{DataController::CACHE_KEY}")}/, node.params[:key], assertion_failure) + # Rails < v7.2 stores the params in an array, so confirm it includes the key + else + assert_includes node.params[:key], DataController::CACHE_KEY, assertion_failure + end + end end end diff --git a/test/multiverse/suites/rails_prepend/Envfile b/test/multiverse/suites/rails_prepend/Envfile index c474d0b6e5..18884f4cb1 100644 --- a/test/multiverse/suites/rails_prepend/Envfile +++ b/test/multiverse/suites/rails_prepend/Envfile @@ -14,7 +14,7 @@ RAILS_VERSIONS = [ ['4.2.0', 2.4, 2.4] ] -# unshift_rails_edge(RAILS_VERSIONS) +unshift_rails_edge(RAILS_VERSIONS) def gem_list(rails_version = nil) # earlier thor errors, uncertain if they persist diff --git a/test/multiverse/suites/redis/Envfile b/test/multiverse/suites/redis/Envfile index 8969c4e42f..04d39db434 100644 --- a/test/multiverse/suites/redis/Envfile +++ b/test/multiverse/suites/redis/Envfile @@ -14,8 +14,19 @@ def gem_list(redis_version = nil) <<~RB gem 'rack' gem 'redis'#{redis_version} - + RB end create_gemfiles(REDIS_VERSIONS) + +# We do not spin up a full Redis cluster in the testing environment, which +# limits our ability to run unit tests on the redis-clustering behavior. +# Since the testing capability is limited, only test the latest version of the +# redis-clustering gem, which itself requires Ruby v2.7+. +if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7.0') + gemfile <<~RB + gem 'rack' + gem 'redis-clustering' + RB +end diff --git a/test/multiverse/suites/redis/redis_instrumentation_test.rb b/test/multiverse/suites/redis/redis_instrumentation_test.rb index bc88722d41..0d5a6a7420 100644 --- a/test/multiverse/suites/redis/redis_instrumentation_test.rb +++ b/test/multiverse/suites/redis/redis_instrumentation_test.rb @@ -59,6 +59,8 @@ def test_records_metrics_for_connect end def test_records_connect_tt_node_within_call_that_triggered_it + skip_for_redis_clustering_gem + in_transaction do redis = Redis.new redis.get('foo') @@ -277,6 +279,8 @@ def test_records_instance_parameters_on_tt_node_for_get end def test_records_hostname_on_tt_node_for_get_with_unix_domain_socket + skip_for_redis_clustering_gem + redis = Redis.new redis.send(client).stubs(:path).returns('/tmp/redis.sock') @@ -309,6 +313,8 @@ def test_records_instance_parameters_on_tt_node_for_multi end def test_records_hostname_on_tt_node_for_multi_with_unix_domain_socket + skip_for_redis_clustering_gem + redis = Redis.new redis.send(client).stubs(:path).returns('/tmp/redis.sock') @@ -327,6 +333,8 @@ def test_records_hostname_on_tt_node_for_multi_with_unix_domain_socket end def test_records_unknown_unknown_metric_when_error_gathering_instance_data + skip_for_redis_clustering_gem + redis = Redis.new redis.send(client).stubs(:path).raises(StandardError.new) in_transaction do @@ -347,6 +355,8 @@ def simulate_read_error end def test_noticed_error_at_segment_and_txn_on_error + skip_for_redis_clustering_gem + txn = nil begin in_transaction do |redis_txn| @@ -362,6 +372,8 @@ def test_noticed_error_at_segment_and_txn_on_error end def test_noticed_error_only_at_segment_on_error + skip_for_redis_clustering_gem + txn = nil in_transaction do |redis_txn| begin @@ -472,4 +484,8 @@ def either_hostname [NewRelic::Agent::Hostname.get, 'redis'] end end + + def skip_for_redis_clustering_gem + skip 'Incompatible with redis-clustering' if defined?(Redis::Cluster::Client) + end end diff --git a/test/new_relic/agent/instrumentation/rails/active_job_subscriber.rb b/test/new_relic/agent/instrumentation/rails/active_job_subscriber.rb index 8cdfbe51ae..dbb637f7fd 100644 --- a/test/new_relic/agent/instrumentation/rails/active_job_subscriber.rb +++ b/test/new_relic/agent/instrumentation/rails/active_job_subscriber.rb @@ -107,7 +107,12 @@ def validate_transaction(txn, methods = []) segment = segments.detect { |s| s.name == "Ruby/ActiveJob/default/#{method}" } assert segment - assert_equal 'ActiveJob::QueueAdapters::AsyncAdapter', segment.params[:adapter].class.name + + if defined?(Rails) && Rails.respond_to?(:version) && Gem::Version.new(Rails.version) >= Gem::Version.new('7.1') + assert_match(/ActiveJob::QueueAdapters::(?:Test|Async)Adapter/, segment.params[:adapter].class.name) + else + assert_equal 'ActiveJob::QueueAdapters::AsyncAdapter', segment.params[:adapter].class.name + end end end end diff --git a/test/new_relic/agent/local_log_decorator_test.rb b/test/new_relic/agent/local_log_decorator_test.rb index 6846f85c20..a3208fedd2 100644 --- a/test/new_relic/agent/local_log_decorator_test.rb +++ b/test/new_relic/agent/local_log_decorator_test.rb @@ -45,9 +45,10 @@ def test_does_not_decorate_if_local_decoration_disabled end end - def test_does_not_decorate_if_instrumentation_logger_disabled + def test_does_not_decorate_if_instrumentation_logger_and_logastasher_disabled with_config( :'instrumentation.logger' => 'disabled', + :'instrumentation.logstasher' => 'disabled', :'application_logging.enabled' => true, :'application_logging.local_decorating.enabled' => true ) do @@ -99,6 +100,34 @@ def test_safe_without_entity_name assert_includes decorated_message, '||' end end + + def test_decorates_json_log_hashes + canned_hostname = 'blazkowicz' + hash = {'dennis' => 'gnasher'} + + in_transaction do |txn| + expected = hash.merge({'entity.name' => @enabled_config[:app_name], + 'entity.type' => 'SERVICE', + 'hostname' => canned_hostname, + 'entity.guid' => @enabled_config[:entity_guid], + 'trace.id' => txn.trace_id, + 'span.id' => txn.segments.first.guid}) + + NewRelic::Agent::Hostname.stub(:get, canned_hostname) do + LocalLogDecorator.decorate(hash) + end + + assert_equal expected, hash, "Expected hash to be decorated. Wanted >>#{expected}<<, got >>#{hash}<<" + end + end + + def test_returns_early_with_frozen_hashes + hash = {'dennis' => 'gnasher'}.freeze + expected = hash.dup + LocalLogDecorator.decorate(hash) + + assert_equal expected, hash, 'Expected no errors and no hash modifications for a frozen hash' + end end end end diff --git a/test/new_relic/agent/log_event_aggregator_test.rb b/test/new_relic/agent/log_event_aggregator_test.rb index 78b4ff1ee2..d707a84f99 100644 --- a/test/new_relic/agent/log_event_aggregator_test.rb +++ b/test/new_relic/agent/log_event_aggregator_test.rb @@ -15,6 +15,7 @@ def setup @enabled_config = { :'instrumentation.logger' => 'auto', + :'instrumentation.logstasher' => 'auto', LogEventAggregator::OVERALL_ENABLED_KEY => true, LogEventAggregator::FORWARDING_ENABLED_KEY => true } @@ -57,6 +58,7 @@ def test_records_enabled_metrics_on_startup assert_metrics_recorded_exclusive({ 'Supportability/Logging/Ruby/Logger/enabled' => {:call_count => 1}, + 'Supportability/Logging/Ruby/LogStasher/enabled' => {:call_count => 1}, 'Supportability/Logging/Metrics/Ruby/enabled' => {:call_count => 1}, 'Supportability/Logging/Forwarding/Ruby/enabled' => {:call_count => 1}, 'Supportability/Logging/LocalDecorating/Ruby/enabled' => {:call_count => 1} @@ -76,6 +78,7 @@ def test_records_disabled_metrics_on_startup assert_metrics_recorded_exclusive({ 'Supportability/Logging/Ruby/Logger/disabled' => {:call_count => 1}, + 'Supportability/Logging/Ruby/LogStasher/disabled' => {:call_count => 1}, 'Supportability/Logging/Metrics/Ruby/disabled' => {:call_count => 1}, 'Supportability/Logging/Forwarding/Ruby/disabled' => {:call_count => 1}, 'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1} @@ -337,6 +340,7 @@ def test_high_security_mode 'Logging/lines' => {:call_count => 9}, 'Logging/lines/DEBUG' => {:call_count => 9}, 'Supportability/Logging/Ruby/Logger/enabled' => {:call_count => 1}, + 'Supportability/Logging/Ruby/LogStasher/enabled' => {:call_count => 1}, 'Supportability/Logging/Metrics/Ruby/enabled' => {:call_count => 1}, 'Supportability/Logging/Forwarding/Ruby/enabled' => {:call_count => 1}, 'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1} @@ -359,6 +363,7 @@ def test_overall_disabled # All settings should report as disabled regardless of config option assert_metrics_recorded_exclusive({ 'Supportability/Logging/Ruby/Logger/disabled' => {:call_count => 1}, + 'Supportability/Logging/Ruby/LogStasher/disabled' => {:call_count => 1}, 'Supportability/Logging/Metrics/Ruby/disabled' => {:call_count => 1}, 'Supportability/Logging/Forwarding/Ruby/disabled' => {:call_count => 1}, 'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1} @@ -384,6 +389,7 @@ def test_overall_disabled_in_high_security_mode assert_metrics_recorded_exclusive({ 'Supportability/Logging/Ruby/Logger/disabled' => {:call_count => 1}, + 'Supportability/Logging/Ruby/LogStasher/disabled' => {:call_count => 1}, 'Supportability/Logging/Metrics/Ruby/disabled' => {:call_count => 1}, 'Supportability/Logging/Forwarding/Ruby/disabled' => {:call_count => 1}, 'Supportability/Logging/LocalDecorating/Ruby/disabled' => {:call_count => 1} @@ -521,5 +527,171 @@ def test_records_log_events_not_within_default_severities assert_equal(log_message, events.first.last['message']) end end + + def test_record_json_sets_severity_when_given_level + @aggregator.record_logstasher_event({'level' => :warn, 'message' => 'yikes!'}) + _, events = @aggregator.harvest! + + assert_equal 'WARN', events[0][1]['level'] + assert_metrics_recorded([ + 'Logging/lines/WARN' + ]) + end + + def test_record_json_sets_severity_unknown_when_no_level + @aggregator.record_logstasher_event({'message' => 'hello there'}) + _, events = @aggregator.harvest! + + assert_equal 'UNKNOWN', events[0][1]['level'] + assert_metrics_recorded([ + 'Logging/lines/UNKNOWN' + ]) + end + + def test_record_json_does_not_record_if_message_is_nil + @aggregator.record_logstasher_event({'level' => :info, 'message' => nil}) + _, events = @aggregator.harvest! + + assert_empty events + end + + def test_record_json_does_not_record_if_message_empty_string + @aggregator.record_logstasher_event({'level' => :info, 'message' => ''}) + _, events = @aggregator.harvest! + + assert_empty events + end + + def test_record_json_returns_message_when_avaliable + @aggregator.record_logstasher_event({'level' => :warn, 'message' => 'A trex is near'}) + _, events = @aggregator.harvest! + + assert_equal 'A trex is near', events[0][1]['message'] + end + + def test_create_logstasher_event_do_not_message_when_not_given + @aggregator.record_logstasher_event({'level' => :info}) + _, events = @aggregator.harvest! + + refute events[0][1]['message'] + end + + def test_add_logstasher_event_attributes_records_attributes + @aggregator.record_logstasher_event({'source' => '127.0.0.1', 'tags' => ['log']}) + _, events = @aggregator.harvest! + + assert_includes(events[0][1]['attributes'], 'source') + assert_includes(events[0][1]['attributes'], 'tags') + end + + def test_add_add_logstasher_event_attributes_deletes_already_recorded_attributes + @aggregator.record_logstasher_event({'message' => 'bye', 'level' => :info, '@timestamp' => 'now', 'include_me' => 'random attribute'}) + _, events = @aggregator.harvest! + + # The event's 'attributes' hash doesn't include already recorded attributes + refute_includes(events[0][1]['attributes'], 'message') + refute_includes(events[0][1]['attributes'], 'level') + refute_includes(events[0][1]['attributes'], '@timestamp') + assert events[0][1]['attributes']['include_me'] + + # The event still includes necessary attributes + assert events[0][1]['message'] + assert events[0][1]['level'] + assert events[0][1]['timestamp'] + end + + def test_logstasher_forwarding_disabled_and_high_security_enabled + with_config(:'application_logging.forwarding.enabled' => false) do + # Refresh the high security setting on this notification + NewRelic::Agent.config.notify_server_source_added + + @aggregator.record_logstasher_event({'message' => 'high security enabled', 'level' => :info}) + _, events = @aggregator.harvest! + + assert_empty events + end + end + + def test_records_customer_metrics_when_enabled_logstasher + with_config(LogEventAggregator::METRICS_ENABLED_KEY => true) do + 2.times { @aggregator.record_logstasher_event({'message' => 'metrics enabled', 'level' => :debug}) } + @aggregator.harvest! + end + + assert_metrics_recorded({ + 'Logging/lines' => {:call_count => 2}, + 'Logging/lines/DEBUG' => {:call_count => 2} + }) + end + + def test_doesnt_record_customer_metrics_when_overall_disabled_and_metrics_enabled_logstasher + with_config( + LogEventAggregator::OVERALL_ENABLED_KEY => false, + LogEventAggregator::METRICS_ENABLED_KEY => true + ) do + NewRelic::Agent.config.notify_server_source_added + + @aggregator.record_logstasher_event({'message' => 'overall disabled, metrics enabled', 'level' => :debug}) + @aggregator.harvest! + end + + assert_metrics_not_recorded([ + 'Logging/lines', + 'Logging/lines/DEBUG' + ]) + end + + def test_doesnt_record_customer_metrics_when_disabled_logstasher + with_config(LogEventAggregator::METRICS_ENABLED_KEY => false) do + @aggregator.record_logstasher_event({'message' => 'metrics disabled', 'level' => :warn}) + @aggregator.harvest! + end + + assert_metrics_not_recorded([ + 'Logging/lines', + 'Logging/lines/WARN' + ]) + end + + def test_high_security_mode_logstasher + with_config(CAPACITY_KEY => 5, :high_security => true) do + # Refresh the high security setting on this notification + NewRelic::Agent.config.notify_server_source_added + + @aggregator.record_logstasher_event({'message' => 'high security enabled', 'level' => :info}) + _, events = @aggregator.harvest! + + assert_empty events + end + end + + def test_does_not_record_logstasher_events_with_a_severity_below_config + with_config(LogEventAggregator::LOG_LEVEL_KEY => 'info') do + assert_equal :INFO, @aggregator.send(:configured_log_level_constant) + + @aggregator.record_logstasher_event({'message' => 'severity below config', 'level' => :debug}) + _, events = @aggregator.harvest! + + assert_empty events + end + end + + def test_record_logstasher_exits_if_forwarding_disabled + with_config(LogEventAggregator::FORWARDING_ENABLED_KEY => false) do + @aggregator.record_logstasher_event({'message' => 'forwarding disabled', 'level' => :info}) + _, results = @aggregator.harvest! + + assert_empty results + end + end + + def test_nil_when_record_logstasher_errors + @aggregator.stub(:severity_too_low?, -> { raise 'kaboom' }) do + @aggregator.record_logstasher_event({'level' => :warn, 'message' => 'A trex is near'}) + _, results = @aggregator.harvest! + + assert_empty results + end + end end end diff --git a/test/new_relic/control/security_interface_test.rb b/test/new_relic/control/security_interface_test.rb new file mode 100644 index 0000000000..8eee07c86e --- /dev/null +++ b/test/new_relic/control/security_interface_test.rb @@ -0,0 +1,147 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require_relative '../../test_helper' +require 'new_relic/control/security_interface' + +class NewRelic::Control::SecurityInterfaceTest < Minitest::Test + def setup + reset_supportability_metrics + NewRelic::Agent.config.reset_to_defaults + %i[@agent_started @wait].each do |variable| + instance = NewRelic::Control::SecurityInterface.instance + instance.remove_instance_variable(variable) if instance.instance_variable_defined?(variable) + end + end + + # For testing purposes, clear out the supportability metrics that have already been recorded. + def reset_supportability_metrics + NewRelic::Agent.instance_variable_get(:@metrics_already_recorded)&.clear + end + + def assert_supportability_metrics_enabled + assert_metrics_recorded 'Supportability/Ruby/SecurityAgent/Agent/Enabled/enabled' + end + + def test_initialization_short_circuits_when_the_security_agent_is_disabled + logger = MiniTest::Mock.new + with_config('security.agent.enabled' => false, 'high_security' => false) do + NewRelic::Agent.stub :logger, logger do + logger.expect :info, nil, [/Security is completely disabled/] + logger.expect :info, nil, [/high_security = false/] + logger.expect :info, nil, [/security.agent.enabled = false/] + + NewRelic::Control::SecurityInterface.instance.init_agent + end + + refute_predicate NewRelic::Control::SecurityInterface.instance, :agent_started? + assert_metrics_recorded 'Supportability/Ruby/SecurityAgent/Agent/Enabled/disabled' + end + logger.verify + end + + def test_initialization_short_circuits_when_high_security_mode_is_enabled + logger = MiniTest::Mock.new + with_config('security.agent.enabled' => true, 'high_security' => true) do + NewRelic::Agent.stub :logger, logger do + logger.expect :info, nil, [/Security is completely disabled/] + logger.expect :info, nil, [/high_security = true/] + logger.expect :info, nil, [/security.agent.enabled = true/] + + NewRelic::Control::SecurityInterface.instance.init_agent + end + + refute_predicate NewRelic::Control::SecurityInterface.instance, :agent_started? + assert_supportability_metrics_enabled + end + logger.verify + end + + def test_initialization_short_circuits_if_the_agent_has_already_been_started + reached = false + with_config('security.agent.enabled' => true) do + NewRelic::Agent.stub :config, -> { reached = true } do + NewRelic::Control::SecurityInterface.instance.instance_variable_set(:@agent_started, true) + NewRelic::Control::SecurityInterface.instance.init_agent + end + end + + refute reached, 'Expected init_agent to short circuit but it reached code within the method instead!' + end + + def test_initialization_short_circuits_if_the_agent_has_been_told_to_wait + reached = false + with_config('security.agent.enabled' => true) do + NewRelic::Agent.stub :config, -> { reached = true } do + NewRelic::Control::SecurityInterface.instance.instance_variable_set(:@wait, true) + NewRelic::Control::SecurityInterface.instance.init_agent + end + end + + refute reached, 'Expected init_agent to short circuit but it reached code within the method instead!' + end + + def test_initialization_requires_the_security_agent + skip_unless_minitest5_or_above + + required = false + logger = MiniTest::Mock.new + with_config('security.agent.enabled' => true) do + NewRelic::Agent.stub :logger, logger do + logger.expect :info, nil, [/Invoking New Relic security/] + + NewRelic::Control::SecurityInterface.instance.stub :require, proc { |_gem| required = true }, %w[newrelic_security] do + NewRelic::Control::SecurityInterface.instance.init_agent + end + end + end + logger.verify + + assert required, 'Expected init_agent to perform a require statement' + assert_predicate NewRelic::Control::SecurityInterface.instance, :agent_started? + assert_supportability_metrics_enabled + end + + def test_initialization_anticipates_a_load_error + skip_unless_minitest5_or_above + + logger = MiniTest::Mock.new + with_config('security.agent.enabled' => true) do + NewRelic::Agent.stub :logger, logger do + logger.expect :info, nil, [/Invoking New Relic security/] + logger.expect :info, nil, [/security agent not found/] + + error_proc = proc { |_gem| raise LoadError.new } + NewRelic::Control::SecurityInterface.instance.stub :require, error_proc, %w[newrelic_security] do + NewRelic::Control::SecurityInterface.instance.init_agent + end + end + logger.verify + + refute_predicate NewRelic::Control::SecurityInterface.instance, :agent_started? + assert_supportability_metrics_enabled + end + end + + def test_initialization_handles_errors + skip_unless_minitest5_or_above + + logger = MiniTest::Mock.new + with_config('security.agent.enabled' => true) do + NewRelic::Agent.stub :logger, logger do + logger.expect :info, nil, [/Invoking New Relic security/] + logger.expect :error, nil, [/Exception in New Relic security module loading/] + + error_proc = proc { |_gem| raise StandardError } + NewRelic::Control::SecurityInterface.instance.stub :require, error_proc, %w[newrelic_security] do + NewRelic::Control::SecurityInterface.instance.init_agent + end + end + end + logger.verify + + refute_predicate NewRelic::Control::SecurityInterface.instance, :agent_started? + assert_supportability_metrics_enabled + end +end diff --git a/test/new_relic/healthy_urls_test.rb b/test/new_relic/healthy_urls_test.rb index a444e9c3e9..d42340fcd5 100644 --- a/test/new_relic/healthy_urls_test.rb +++ b/test/new_relic/healthy_urls_test.rb @@ -70,8 +70,7 @@ class HealthyUrlsTest < Minitest::Test DEBUG = false def test_all_urls - skip_unless_ci_cron - skip_unless_newest_ruby + skip_unless_special_ci urls = gather_urls errors = urls.each_with_object({}) do |(url, _files), hash|