diff --git a/.github/versions.yml b/.github/versions.yml index 5b002e5028..467f91ad92 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.190.0 - :sha: a6e6f86333f0a2523ece813039b8b4be04560854 + :tag: v1.191.0 + :sha: 52753b7da854d5c07df37391a986c76ab4615999 actions/checkout: :tag: v4.1.7 :sha: 692973e3d937129bcbf40652eb9f2f61becf3332 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 358f20170f..0be439be24 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@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 - - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + - uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.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.4] + ruby-version: [2.4.10, 3.3.5] 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@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: ${{ matrix.ruby-version }} @@ -62,7 +62,7 @@ jobs: "2.4.10": { "rails": "norails,rails42,rails52" }, - "3.3.4": { + "3.3.5": { "rails": "norails,rails61,rails72" } } @@ -136,6 +136,33 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 10 + zookeeper: + image: bitnami/zookeeper + ports: + - 2181:2181 + env: + ALLOW_ANONYMOUS_LOGIN: yes + options: >- + --health-cmd "echo mntr | nc -w 2 -q 2 localhost 2181" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + kafka: + image: bitnami/kafka + ports: + - 9092:9092 + options: >- + --health-cmd "kafka-broker-api-versions.sh --version" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper:2181 + ALLOW_PLAINTEXT_LISTENER: yes + KAFKA_LISTENERS: INSIDE://0.0.0.0:9093,OUTSIDE://0.0.0.0:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://localhost:9092 memcached: image: memcached:latest ports: @@ -190,7 +217,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.4] + ruby-version: [2.4.10, 3.3.5] steps: - name: Configure git @@ -204,7 +231,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@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: ${{ matrix.ruby-version }} @@ -290,14 +317,14 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: [2.7.8, 3.3.4] + ruby-version: [2.7.8, 3.3.5] steps: - name: Configure git run: 'git config --global init.defaultBranch main' - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 - name: Install Ruby ${{ matrix.ruby-version }} - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: ${{ matrix.ruby-version }} @@ -337,7 +364,7 @@ jobs: - name: Configure git run: 'git config --global init.defaultBranch main' - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 - - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + - uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: '3.3' - run: bundle diff --git a/.github/workflows/ci_cron.yml b/.github/workflows/ci_cron.yml index 4dc51bcbc5..02dd0a08aa 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@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 - - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + - uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.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.5, 3.3.4, 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.5, 3.3.5, 3.4.0-preview1] steps: - name: Configure git @@ -50,9 +50,9 @@ 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@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: - ruby-version: ${{ matrix.ruby-version }} + ruby-version: ${{ matrix.ruby-version }} - name: Set up mini-envs for ruby version uses: ./.github/actions/variable-mapper @@ -76,12 +76,12 @@ jobs: "rails": "norails,rails61,rails60,rails70,rails71" }, "3.1.6": { - "rails": "norails,rails61,rails70,rails71,rails72,railsedge" + "rails": "norails,rails61,rails70,rails71,rails72" }, "3.2.5": { "rails": "norails,rails61,rails70,rails71,rails72,railsedge" }, - "3.3.4": { + "3.3.5": { "rails": "norails,rails61,rails70,rails71,rails72,railsedge" }, "3.4.0-preview1": { @@ -149,6 +149,33 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 10 + zookeeper: + image: bitnami/zookeeper + ports: + - 2181:2181 + env: + ALLOW_ANONYMOUS_LOGIN: yes + options: >- + --health-cmd "echo mntr | nc -w 2 -q 2 localhost 2181" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + kafka: + image: bitnami/kafka + ports: + - 9092:9092 + options: >- + --health-cmd "kafka-broker-api-versions.sh --version" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper:2181 + ALLOW_PLAINTEXT_LISTENER: yes + KAFKA_LISTENERS: INSIDE://0.0.0.0:9093,OUTSIDE://0.0.0.0:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:9093,OUTSIDE://localhost:9092 memcached: image: memcached:latest ports: @@ -203,7 +230,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.5, 3.3.4, 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.5, 3.3.5, 3.4.0-preview1] steps: - name: Configure git run: 'git config --global init.defaultBranch main' @@ -216,7 +243,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@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: ${{ matrix.ruby-version }} @@ -281,14 +308,14 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: [2.7.8, 3.0.7, 3.1.6, 3.2.5, 3.3.4, 3.4.0-preview1] + ruby-version: [2.7.8, 3.0.7, 3.1.6, 3.2.5, 3.3.5, 3.4.0-preview1] steps: - name: Configure git run: 'git config --global init.defaultBranch main' - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 - name: Install Ruby ${{ matrix.ruby-version }} - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: ${{ matrix.ruby-version }} diff --git a/.github/workflows/ci_jruby.yml b/.github/workflows/ci_jruby.yml index 6c759c65a6..4f48127c64 100644 --- a/.github/workflows/ci_jruby.yml +++ b/.github/workflows/ci_jruby.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 - name: Install JRuby - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: jruby-9.4.8.0 @@ -49,7 +49,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 - name: Install JRuby - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: jruby-9.4.8.0 diff --git a/.github/workflows/ci_special.yml b/.github/workflows/ci_special.yml index 58eb075c2c..46e3e0c9f3 100644 --- a/.github/workflows/ci_special.yml +++ b/.github/workflows/ci_special.yml @@ -21,7 +21,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 3.4.0-preview1 - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: 3.4.0-preview1 diff --git a/.github/workflows/config_docs.yml b/.github/workflows/config_docs.yml index aa50dd699f..22aee84aec 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@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: 3.3 diff --git a/.github/workflows/performance_tests.yml b/.github/workflows/performance_tests.yml index c1872b5c45..1aceb9e60d 100644 --- a/.github/workflows/performance_tests.yml +++ b/.github/workflows/performance_tests.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 with: ref: 'main' - - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + - uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: '3.3' - run: bundle diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 1d30de8b93..8ceb0aced4 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@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: 3.3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9232fcb6f1..a96f4a10c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: with: fetch-depth: 0 - - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + - uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: 3.3 diff --git a/.github/workflows/release_notes.yml b/.github/workflows/release_notes.yml index febddf5cde..ed77a6748e 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@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + - uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: 3.3 - name: Checkout code diff --git a/.github/workflows/release_pr.yml b/.github/workflows/release_pr.yml index 7aa891d6c3..ec1015ecbf 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@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: 3.3 diff --git a/.github/workflows/slack_notifications.yml b/.github/workflows/slack_notifications.yml index 2fa184bf7d..0dd7074859 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@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + - uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: 3.3 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 @@ -32,6 +32,7 @@ jobs: rack rails rake + rdkafka redis resque roda @@ -46,7 +47,7 @@ jobs: cve_notifications: runs-on: ubuntu-22.04 steps: - - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # tag v1.190.0 + - uses: ruby/setup-ruby@52753b7da854d5c07df37391a986c76ab4615999 # tag v1.191.0 with: ruby-version: 3.3 - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # tag v4.1.7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5946f5fc..8cec4b666f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,98 @@ # New Relic Ruby Agent Release Notes +## v9.14.0 + +Version 9.14.0 adds Apache Kafka instrumentation for the rdkafka and ruby-kafka gems, introduces a configuration-based, automatic way to add custom instrumentation method tracers, correctly captures MIME type for AcionDispatch 7.0+ requests, properly handles Boolean coercion for `newrelic.yml` configuration, fixes a JRuby bug in the configuration manager, fixes a bug related to `Bundler.rubygems.installed_specs`, and fixes a bug to make the agent compatible with ViewComponent v3.15.0+. + +- **Feature: Add Apache Kafka instrumentation for the rdkafka and ruby-kafka gems** + + The agent now has instrumentation for both the rdkafka and ruby-kafka gems. The agent will record transactions and message broker segments for produce and consume calls made using these gems. [PR#2824](https://github.com/newrelic/newrelic-ruby-agent/pull/2824) [PR#2842](https://github.com/newrelic/newrelic-ruby-agent/pull/2842) + +- **Feature: Add a configuration option to permit custom method tracers to be defined automatically** + + A new `:automatic_custom_instrumentation_method_list` configuration parameter has been added to permit the user to define a list of fully qualified (namespaced) Ruby methods for the agent to automatically add custom instrumentation for without requiring any code modifications to be made to the classes that define the methods. + + The list should be an array of `CLASS#METHOD` (for instance methods) and/or `CLASS.METHOD` (for class methods) strings. + + Use fully qualified class names (using the `::` delimiter) that include any module or class namespacing. + + Here is some Ruby source code that defines a `render_png` instance method for an `Image` class and a `notify` class method for a `User` class, both within a `MyCompany` module namespace: + + ``` + module MyCompany + class Image + def render_png + # code to render a PNG + end + end + + class User + def self.notify + # code to notify users + end + end + end + ``` + + Given that source code, the `newrelic.yml` config file might request instrumentation for both of these methods like so: + + ``` + automatic_custom_instrumentation_method_list: + - MyCompany::Image#render_png + - MyCompany::User.notify + ``` + + That configuration example uses YAML array syntax to specify both methods. Alternatively, a comma-delimited string can be used instead: + + ``` + automatic_custom_instrumentation_method_list: 'MyCompany::Image#render_png, MyCompany::User.notify' + ``` + + Whitespace around the comma(s) in the list is optional. When configuring the agent with a list of methods via the `NEW_RELIC_AUTOMATIC_CUSTOM_INSTRUMENTATION_METHOD_LIST` environment variable, this comma-delimited string format should be used: + + ``` + export NEW_RELIC_AUTOMATIC_CUSTOM_INSTRUMENTATION_METHOD_LIST='MyCompany::Image#render_png, MyCompany::User.notify' + ``` + + [PR#2851](https://github.com/newrelic/newrelic-ruby-agent/pull/2851) + +- **Feature: Collect just MIME type for AcionDispatch 7.0+ requests** + + Rails 7.0 [introduced changes](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#actiondispatch-request-content-type-now-returns-content-type-header-as-it-is) to the behavior of `ActionDispatch::Request#content_type`, adding extra request-related details the agent wasn't expecting to collect. Additionally, the agent's use of `content_type ` was triggering deprecation warnings. The agent now uses `ActionDispatch::Request#media_type` to capture the MIME type. Thanks to [@internethostage](https://github.com/internethostage) for letting us know about this change. [Issue#2500](https://github.com/newrelic/newrelic-ruby-agent/issues/2500) [PR#2855](https://github.com/newrelic/newrelic-ruby-agent/pull/2855) + +- **Bugfix: Corrected Boolean coercion for `newrelic.yml` configuration** + + Previously, any String assigned to New Relic configurations expecting a Boolean value were evaluated as `true`. This could lead to unexpected behavior. For example, setting `application_logging.enabled: 'false'` in `newrelic.yml` would incorrectly evaluate to `application_logging.enabled: true` due to the truthy nature of Strings. + + Now, the agent strictly interprets Boolean configuration values. It recognizes both actual Boolean values and certain Strings/Symbols: + - `'true'`, `'yes'`, or `'on'` (evaluates to `true`) + - `'false'`, `'no'`, or `'off'` (evaluates to `false`) + + Any other inputs will revert to the setting's default configuration value. [PR#2847](https://github.com/newrelic/newrelic-ruby-agent/pull/2847) + +- **Bugfix: JRuby not saving configuration values correctly in configuration manager** + + Previously, a change made to fix a different JRuby bug caused the agent to not save configuration values correctly in the configuration manager when running on JRuby. This has been fixed. [PR#2848](https://github.com/newrelic/newrelic-ruby-agent/pull/2848) + +- **Bugfix: Update condition to verify Bundler.rubygems.installed_specs is available** + + To address a recent Bundler deprecation warning, we started using `Bundler.rubygems.installed_specs` instead of `Bundler.rubygems.all_specs` in environments that seemed appropriate. We discovered the version constraint we used was too low. Now, rather than check the version, we check for the method using `respond_to?`. [PR#2853](https://github.com/newrelic/newrelic-ruby-agent/pull/2853) + +- **Bugfix: Support view_component v3.15.0+** + + Previously the agent had been making use of a private API to obtain a component identifier value. This private API was dropped in v3.15.0 of view_component, resulting in errors from the New Relic Ruby agent's continued attempts to use it. Many thanks to community member [@navidemad](https://github.com/navidemad) for bringing this issue to our attention and supplying a bugfix with [PR#2870](https://github.com/newrelic/newrelic-ruby-agent/pull/2870). + ## v9.13.0 Version 9.13.0 enhances support for AWS Lambda functions, adds experimental OpenSearch instrumentation, updates framework detection, silences a Bundler deprecation warning, fixes Falcon dispatcher detection, fixes a bug with Redis instrumentation installation, and addresses a JRuby-specific concurrency issue. - **Feature: Enhance AWS Lambda function instrumentation** -When utilized via the latest [New Relic Ruby layer for AWS Lambda](https://layers.newrelic-external.com/), the agent now offers enhanced support for AWS Lambda function instrumentation. -* The agent's instrumentation for AWS Lambda functions now supports distributed tracing. -* Web-triggered invocations are now identified as being "web"-based when an API Gateway call is involved, with support for both API Gateway versions 1.0 and 2.0. -* Web-based calls have the HTTP method, URI, and status code recorded. -* The agent now recognizes and reports on 12 separate AWS resources that are capable of triggering a Lambda function invocation: ALB, API Gateway V1, API Gateway V2, CloudFront, CloudWatch Scheduler, DynamoStreams, Firehose, Kinesis, S3, SES, SNS, and SQS. +When utilized via the latest [New Relic Ruby layer for AWS Lambda](https://layers.newrelic-external.com/), the agent now offers enhanced support for AWS Lambda function instrumentation. +* The agent's instrumentation for AWS Lambda functions now supports distributed tracing. +* Web-triggered invocations are now identified as being "web"-based when an API Gateway call is involved, with support for both API Gateway versions 1.0 and 2.0. +* Web-based calls have the HTTP method, URI, and status code recorded. +* The agent now recognizes and reports on 12 separate AWS resources that are capable of triggering a Lambda function invocation: ALB, API Gateway V1, API Gateway V2, CloudFront, CloudWatch Scheduler, DynamoStreams, Firehose, Kinesis, S3, SES, SNS, and SQS. * The type of the triggering resource and its ARN will be recorded for each resource, and for many of them, extra resource-specific attributes will be recorded as well. For example, Lambda function invocations triggered by S3 bucket activity will now result in the S3 bucket name being recorded. [PR#2811](https://github.com/newrelic/newrelic-ruby-agent/pull/2811) diff --git a/lib/new_relic/agent.rb b/lib/new_relic/agent.rb index 6865341fe3..888ce963ba 100644 --- a/lib/new_relic/agent.rb +++ b/lib/new_relic/agent.rb @@ -85,6 +85,10 @@ class LicenseException < StandardError; end # An exception that forces an agent to stop reporting until its mongrel is restarted. class ForceDisconnectException < StandardError; end + # Error handling for the automated custom instrumentation tracer logic + class AutomaticTracerParseException < StandardError; end + class AutomaticTracerTraceException < StandardError; end + # An exception that forces an agent to restart. class ForceRestartException < StandardError def message @@ -109,6 +113,9 @@ class SerializationError < StandardError; end # placeholder name used when we cannot determine a transaction's name UNKNOWN_METRIC = '(unknown)'.freeze LLM_FEEDBACK_MESSAGE = 'LlmFeedbackMessage' + # give the observed app time to load the code that automatic tracers have + # been configured for + AUTOMATIC_TRACER_MAX_ATTEMPTS = 60 # 60 = try about twice a second for 30 seconds attr_reader :error_group_callback attr_reader :llm_token_count_callback @@ -163,6 +170,92 @@ def add_or_defer_method_tracer(receiver, method_name, metric_name, options) end end + # @api private + def self.add_automatic_method_tracers(arr) + return unless arr + return arr if arr.respond_to?(:empty?) && arr.empty? + + arr = arr.split(/\s*,\s*/) if arr.is_a?(String) + + add_tracers_once_methods_are_defined(arr.dup) + + arr + end + + # spawn a thread that will attempt to establish a tracer for each of the + # configured methods. the thread will continue to keep trying with each + # tracer until one of the following happens: + # - the tracer is successfully established + # - the configured method string couldn't be parsed + # - establishing a tracer for a successfully parsed string failed + # - the maximum number of attempts has been reached + # the thread will only be spawned once per agent initialization, to account + # for configuration reloading scenarios. + # + # @api private + def self.add_tracers_once_methods_are_defined(notations) + # this class method can be invoked multiple times at agent startup, so + # we return asap here instead of using a traditional memoization of + # waiting for the method's body to finish being executed + if defined?(@add_tracers_once_methods_are_defined) + return + else + @add_tracers_once_methods_are_defined = true + end + + Thread.new do + AUTOMATIC_TRACER_MAX_ATTEMPTS.times do + notations.delete_if { |notation| prep_tracer_for(notation) } + + break if notations.empty? + + sleep 0.5 + end + end + end + + # returns `true` if the notation string has either been successfully + # processed or raised an error during processing. returns `false` if the + # string seems good but the (customer) code to be traced has not yet been + # loaded into the Ruby VM + # + # @api private + def self.prep_tracer_for(fully_qualified_method_notation) + delimiters = fully_qualified_method_notation.scan(/\.|#/) + raise AutomaticTracerParseException.new("Expected exactly one '.' or '#' delimiter.") unless delimiters.size == 1 + + delimiter = delimiters.first + namespace, method_name = fully_qualified_method_notation.split(delimiter) + unless namespace && !namespace.empty? + raise AutomaticTracerParseException.new("Nothing found to the left of the #{delimiter} delimiter.") + end + unless method_name && !method_name.empty? + raise AutomaticTracerParseException.new("Nothing found to the right of the #{delimiter} delimiter.") + end + + begin + klass = ::NewRelic::LanguageSupport.constantize(namespace) + return false unless klass + + klass_to_trace = delimiter.eql?('.') ? klass.singleton_class : klass + add_or_defer_method_tracer(klass_to_trace, method_name, nil, {}) + rescue StandardError => e + raise AutomaticTracerTraceException.new("#{e.class} - #{e.message}") + end + + true + rescue AutomaticTracerParseException => e + NewRelic::Agent.logger.error('Unable to parse out a usable method name to trace. Expected a valid, fully ' \ + "qualified method notation. Got: '#{fully_qualified_method_notation}'. " \ + "Error: #{e.message}") + true + rescue AutomaticTracerTraceException => e + NewRelic::Agent.logger.error('Unable to automatically apply a tracer to method ' \ + "'#{fully_qualified_method_notation}'. Error: #{e.message}") + true + end + + # @api private def add_deferred_method_tracers_now @tracer_lock.synchronize do @tracer_queue.each do |receiver, method_name, metric_name, options| diff --git a/lib/new_relic/agent/configuration/default_source.rb b/lib/new_relic/agent/configuration/default_source.rb index d1e6211cd8..246af67829 100644 --- a/lib/new_relic/agent/configuration/default_source.rb +++ b/lib/new_relic/agent/configuration/default_source.rb @@ -35,6 +35,15 @@ def self.===(o) end class DefaultSource + BOOLEAN_MAP = { + 'true' => true, + 'yes' => true, + 'on' => true, + 'false' => false, + 'no' => false, + 'off' => false + }.freeze + attr_reader :defaults extend Forwardable @@ -64,6 +73,12 @@ def self.allowlist_for(key) value_from_defaults(key, :allowlist) end + def self.boolean_for(key, value) + string_value = (value.respond_to?(:call) ? value.call : value).to_s + + BOOLEAN_MAP.fetch(string_value, nil) + end + def self.default_for(key) value_from_defaults(key, :default) end @@ -1137,6 +1152,56 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) :allowed_from_server => false, :description => 'If `false`, custom attributes will not be sent on events.' }, + :automatic_custom_instrumentation_method_list => { + :default => NewRelic::EMPTY_ARRAY, + :public => true, + :type => Array, + :allowed_from_server => false, + :transform => proc { |arr| NewRelic::Agent.add_automatic_method_tracers(arr) }, + :description => <<~DESCRIPTION + An array of `CLASS#METHOD` (for instance methods) and/or `CLASS.METHOD` (for class methods) strings representing Ruby methods for the agent to automatically add custom instrumentation to without the need for altering any of the source code that defines the methods. + + Use fully qualified class names (using the `::` delimiter) that include any module or class namespacing. + + Here is some Ruby source code that defines a `render_png` instance method for an `Image` class and a `notify` class method for a `User` class, both within a `MyCompany` module namespace: + + ``` + module MyCompany + class Image + def render_png + # code to render a PNG + end + end + + class User + def self.notify + # code to notify users + end + end + end + ``` + + Given that source code, the `newrelic.yml` config file might request instrumentation for both of these methods like so: + + ``` + automatic_custom_instrumentation_method_list: + - MyCompany::Image#render_png + - MyCompany::User.notify + ``` + + That configuration example uses YAML array syntax to specify both methods. Alternatively, a comma-delimited string can be used instead: + + ``` + automatic_custom_instrumentation_method_list: 'MyCompany::Image#render_png, MyCompany::User.notify' + ``` + + Whitespace around the comma(s) in the list is optional. When configuring the agent with a list of methods via the `NEW_RELIC_AUTOMATIC_CUSTOM_INSTRUMENTATION_METHOD_LIST` environment variable, this comma-delimited string format should be used: + + ``` + export NEW_RELIC_AUTOMATIC_CUSTOM_INSTRUMENTATION_METHOD_LIST='MyCompany::Image#render_png, MyCompany::User.notify' + ``` + DESCRIPTION + }, # Custom events :'custom_insights_events.enabled' => { :default => true, @@ -1463,6 +1528,14 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) :allowed_from_server => false, :description => 'Controls auto-instrumentation of bunny at start-up. May be one of: `auto`, `prepend`, `chain`, `disabled`.' }, + :'instrumentation.ruby_kafka' => { + :default => 'auto', + :public => true, + :type => String, + :dynamic_name => true, + :allowed_from_server => false, + :description => 'Controls auto-instrumentation of the ruby-kafka library at start-up. May be one of `auto`, `prepend`, `chain`, `disabled`.' + }, :'instrumentation.opensearch' => { :default => 'auto', :documentation_default => 'auto', @@ -1472,6 +1545,14 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) :allowed_from_server => false, :description => 'Controls auto-instrumentation of the opensearch-ruby library at start-up. May be one of `auto`, `prepend`, `chain`, `disabled`.' }, + :'instrumentation.rdkafka' => { + :default => 'auto', + :public => true, + :type => String, + :dynamic_name => true, + :allowed_from_server => false, + :description => 'Controls auto-instrumentation of the rdkafka library at start-up. May be one of `auto`, `prepend`, `chain`, `disabled`.' + }, :'instrumentation.aws_sqs' => { :default => 'auto', :public => true, @@ -2202,7 +2283,7 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) :description => 'Enable or disable debugging version of JavaScript agent loader for browser monitoring instrumentation.' }, :'browser_monitoring.ssl_for_http' => { - :default => nil, + :default => false, :allow_nil => true, :public => false, :type => Boolean, diff --git a/lib/new_relic/agent/configuration/environment_source.rb b/lib/new_relic/agent/configuration/environment_source.rb index e8c24079d3..fe6cb791a1 100644 --- a/lib/new_relic/agent/configuration/environment_source.rb +++ b/lib/new_relic/agent/configuration/environment_source.rb @@ -92,7 +92,11 @@ def set_key_by_type(config_key, environment_key) elsif type == Symbol self[config_key] = value.to_sym elsif type == Array - self[config_key] = value.split(/\s*,\s*/) + self[config_key] = if DEFAULTS[config_key].key?(:transform) + DEFAULTS[config_key][:transform].call(value) + else + value.split(/\s*,\s*/) + end elsif type == NewRelic::Agent::Configuration::Boolean if /false|off|no/i.match?(value) self[config_key] = false diff --git a/lib/new_relic/agent/configuration/manager.rb b/lib/new_relic/agent/configuration/manager.rb index 40249f8a8f..f1c5b6bb24 100644 --- a/lib/new_relic/agent/configuration/manager.rb +++ b/lib/new_relic/agent/configuration/manager.rb @@ -142,6 +142,9 @@ def evaluate_and_apply_transformations(key, value) default = enforce_allowlist(key, evaluated) return default if default + boolean = enforce_boolean(key, value) + return boolean if [true, false].include?(boolean) + apply_transformations(key, evaluated) end @@ -167,6 +170,18 @@ def enforce_allowlist(key, value) default end + def enforce_boolean(key, value) + type = default_source.value_from_defaults(key, :type) + return unless type == Boolean + + bool_value = default_source.boolean_for(key, value) + return bool_value unless bool_value.nil? + + default = default_source.default_for(key) + NewRelic::Agent.logger.warn "Invalid value '#{value}' for #{key}, applying default value of '#{default}'" + default + end + def transform_from_default(key) default_source.transform_for(key) end @@ -388,7 +403,7 @@ def reset_cache # modified. The hash really only needs to be modified for the benefit # of the security agent, so if JRuby is in play and the security agent # is not, don't attempt to modify the hash at all and return early. - return @cache if NewRelic::LanguageSupport.jruby? && !Agent.config[:'security.agent.enabled'] + return new_cache if NewRelic::LanguageSupport.jruby? && !Agent.config[:'security.agent.enabled'] @lock.synchronize do preserved = @cache.dup.select { |_k, v| DEPENDENCY_DETECTION_VALUES.include?(v) } diff --git a/lib/new_relic/agent/database/obfuscation_helpers.rb b/lib/new_relic/agent/database/obfuscation_helpers.rb index 12ae7fd043..ed2a68a74f 100644 --- a/lib/new_relic/agent/database/obfuscation_helpers.rb +++ b/lib/new_relic/agent/database/obfuscation_helpers.rb @@ -7,17 +7,17 @@ module Agent module Database module ObfuscationHelpers COMPONENTS_REGEX_MAP = { - :single_quotes => /'(?:[^']|'')*?(?:\\'.*|'(?!'))/, - :double_quotes => /"(?:[^"]|"")*?(?:\\".*|"(?!"))/, - :dollar_quotes => /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/, - :uuids => /\{?(?:[0-9a-fA-F]\-*){32}\}?/, - :numeric_literals => /-?\b(?:[0-9]+\.)?[0-9]+([eE][+-]?[0-9]+)?\b/, - :boolean_literals => /\b(?:true|false|null)\b/i, - :hexadecimal_literals => /0x[0-9a-fA-F]+/, - :comments => /(?:#|--).*?(?=\r|\n|$)/i, - :multi_line_comments => /\/\*(?:[^\/]|\/[^*])*?(?:\*\/|\/\*.*)/, - :oracle_quoted_strings => /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'\<.*?(?:\>'|$)|q'\(.*?(?:\)'|$)/ - } + single_quotes: /'(?:[^']|'')*?(?:\\'.*|'(?!'))/, + double_quotes: /"(?:[^"]|"")*?(?:\\".*|"(?!"))/, + dollar_quotes: /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/, + uuids: /\{?(?:[0-9a-fA-F]-*){32}\}?/, + numeric_literals: /-?\b(?:[0-9]+\.)?[0-9]+([eE][+-]?[0-9]+)?\b/, + boolean_literals: /\b(?:true|false|null)\b/i, + hexadecimal_literals: /0x[0-9a-fA-F]+/, + comments: /(?:#|--).*?(?=\r|\n|$)/i, + multi_line_comments: %r{/\*.*?\*/}m, + oracle_quoted_strings: /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'<.*?(?:>'|$)|q'\(.*?(?:\)'|$)/ + }.freeze DIALECT_COMPONENTS = { :fallback => COMPONENTS_REGEX_MAP.keys, diff --git a/lib/new_relic/agent/instrumentation/dynamodb/instrumentation.rb b/lib/new_relic/agent/instrumentation/dynamodb/instrumentation.rb index b2584aa22c..cf869660f4 100644 --- a/lib/new_relic/agent/instrumentation/dynamodb/instrumentation.rb +++ b/lib/new_relic/agent/instrumentation/dynamodb/instrumentation.rb @@ -31,8 +31,9 @@ def instrument_method_with_new_relic(method_name, *args) collection: args[0][:table_name] ) - arn = get_arn(args[0]) - segment&.add_agent_attribute('cloud.resource_id', arn) if arn + # TODO: Update this when it has been decided how to handle account id for ARN + # arn = get_arn(args[0]) + # segment&.add_agent_attribute('cloud.resource_id', arn) if arn @nr_captured_request = nil # clear request just in case begin diff --git a/lib/new_relic/agent/instrumentation/grape.rb b/lib/new_relic/agent/instrumentation/grape.rb index 0167794b3c..1424ffa06b 100644 --- a/lib/new_relic/agent/instrumentation/grape.rb +++ b/lib/new_relic/agent/instrumentation/grape.rb @@ -20,7 +20,7 @@ depends_on do begin if defined?(Bundler) && - ((Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('2.0.0') && Bundler.rubygems.installed_specs.map(&:name).include?('newrelic-grape')) || + ((Bundler.rubygems.respond_to?(:installed_specs) && Bundler.rubygems.installed_specs.map(&:name).include?('newrelic-grape')) || Bundler.rubygems.all_specs.map(&:name).include?('newrelic-grape')) NewRelic::Agent.logger.info('Not installing New Relic supported Grape instrumentation because the third party newrelic-grape gem is present') false diff --git a/lib/new_relic/agent/instrumentation/rdkafka.rb b/lib/new_relic/agent/instrumentation/rdkafka.rb new file mode 100644 index 0000000000..51b8dae73d --- /dev/null +++ b/lib/new_relic/agent/instrumentation/rdkafka.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 + +DependencyDetection.defer do + named :rdkafka + + depends_on do + defined?(Rdkafka) + end + + executes do + NewRelic::Agent.logger.info('Installing rdkafka instrumentation') + + require_relative 'rdkafka/instrumentation' + require_relative 'rdkafka/chain' + require_relative 'rdkafka/prepend' + + if use_prepend? + prepend_instrument Rdkafka::Config, NewRelic::Agent::Instrumentation::RdkafkaConfig::Prepend + prepend_instrument Rdkafka::Producer, NewRelic::Agent::Instrumentation::RdkafkaProducer::Prepend + prepend_instrument Rdkafka::Consumer, NewRelic::Agent::Instrumentation::RdkafkaConsumer::Prepend + else + chain_instrument NewRelic::Agent::Instrumentation::Rdkafka::Chain + end + end +end diff --git a/lib/new_relic/agent/instrumentation/rdkafka/chain.rb b/lib/new_relic/agent/instrumentation/rdkafka/chain.rb new file mode 100644 index 0000000000..fc9b826aa8 --- /dev/null +++ b/lib/new_relic/agent/instrumentation/rdkafka/chain.rb @@ -0,0 +1,71 @@ +# 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 'instrumentation' + +module NewRelic::Agent::Instrumentation + module Rdkafka::Chain + def self.instrument! + ::Rdkafka::Producer.class_eval do + include NewRelic::Agent::Instrumentation::Rdkafka + + alias_method(:produce_without_new_relic, :produce) + + def produce(**kwargs) + produce_with_new_relic(kwargs) do |headers| + kwargs[:headers] = headers + produce_without_new_relic(**kwargs) + end + end + end + + ::Rdkafka::Consumer.class_eval do + include NewRelic::Agent::Instrumentation::Rdkafka + + alias_method(:each_without_new_relic, :each) + + def each(**kwargs) + each_without_new_relic(**kwargs) do |message| + each_with_new_relic(message) do + yield(message) + end + end + end + end + + ::Rdkafka::Config.class_eval do + include NewRelic::Agent::Instrumentation::RdkafkaConfig + + alias_method(:producer_without_new_relic, :producer) + alias_method(:consumer_without_new_relic, :consumer) + + if Gem::Version.new(::Rdkafka::VERSION) >= Gem::Version.new('0.16.0') + def producer(**kwargs) + producer_without_new_relic(**kwargs).tap do |producer| + set_nr_config(producer) + end + end + + def consumer(**kwargs) + consumer_without_new_relic(**kwargs).tap do |consumer| + set_nr_config(consumer) + end + end + else + def producer + producer_without_new_relic.tap do |producer| + set_nr_config(producer) + end + end + + def consumer + consumer_without_new_relic.tap do |consumer| + set_nr_config(consumer) + end + end + end + end + end + end +end diff --git a/lib/new_relic/agent/instrumentation/rdkafka/instrumentation.rb b/lib/new_relic/agent/instrumentation/rdkafka/instrumentation.rb new file mode 100644 index 0000000000..a91c3f9e1d --- /dev/null +++ b/lib/new_relic/agent/instrumentation/rdkafka/instrumentation.rb @@ -0,0 +1,70 @@ +# 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/messaging' + +module NewRelic::Agent::Instrumentation + module Rdkafka + MESSAGING_LIBRARY = 'Kafka' + PRODUCE = 'Produce' + CONSUME = 'Consume' + + INSTRUMENTATION_NAME = 'Rdkafka' + + def produce_with_new_relic(*args) + NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME) + + topic_name = args[0][:topic] + segment = NewRelic::Agent::Tracer.start_message_broker_segment( + action: :produce, + library: MESSAGING_LIBRARY, + destination_type: :topic, + destination_name: topic_name + ) + create_kafka_metrics(action: PRODUCE, topic: topic_name) + + headers = args[0][:headers] || {} + ::NewRelic::Agent::DistributedTracing.insert_distributed_trace_headers(headers) + + NewRelic::Agent::Tracer.capture_segment_error(segment) { yield(headers) } + ensure + segment&.finish + end + + def each_with_new_relic(message) + NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME) + + headers = message&.headers || {} + topic_name = message&.topic + + NewRelic::Agent::Messaging.wrap_message_broker_consume_transaction( + library: MESSAGING_LIBRARY, + destination_type: :topic, + destination_name: topic_name, + headers: headers, + action: :consume + ) do + create_kafka_metrics(action: CONSUME, topic: topic_name) + yield + end + end + + def create_kafka_metrics(action:, topic:) + hosts = [] + # both 'bootstrap.servers' and 'metadata.broker.list' are valid ways to specify the Kafka server + hosts << @nr_config[:'bootstrap.servers'] if @nr_config[:'bootstrap.servers'] + hosts << @nr_config[:'metadata.broker.list'] if @nr_config[:'metadata.broker.list'] + hosts.each do |host| + NewRelic::Agent.record_metric("MessageBroker/Kafka/Nodes/#{host}/#{action}/#{topic}", 1) + NewRelic::Agent.record_metric("MessageBroker/Kafka/Nodes/#{host}", 1) + end + end + end + + module RdkafkaConfig + def set_nr_config(producer_or_consumer) + producer_or_consumer.instance_variable_set(:@nr_config, self) + end + end +end diff --git a/lib/new_relic/agent/instrumentation/rdkafka/prepend.rb b/lib/new_relic/agent/instrumentation/rdkafka/prepend.rb new file mode 100644 index 0000000000..3397c2436c --- /dev/null +++ b/lib/new_relic/agent/instrumentation/rdkafka/prepend.rb @@ -0,0 +1,66 @@ +# 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 'instrumentation' + +module NewRelic::Agent::Instrumentation + module RdkafkaProducer + module Prepend + include NewRelic::Agent::Instrumentation::Rdkafka + + def produce(**kwargs) + produce_with_new_relic(kwargs) do |headers| + kwargs[:headers] = headers + super + end + end + end + end + + module RdkafkaConsumer + module Prepend + include NewRelic::Agent::Instrumentation::Rdkafka + + def each + super do |message| + each_with_new_relic(message) do + yield(message) + end + end + end + end + end + + module RdkafkaConfig + module Prepend + include NewRelic::Agent::Instrumentation::RdkafkaConfig + + if defined?(::Rdkafka) && Gem::Version.new(::Rdkafka::VERSION) >= Gem::Version.new('0.16.0') + def producer(**kwargs) + super.tap do |producer| + set_nr_config(producer) + end + end + + def consumer(**kwargs) + super.tap do |consumer| + set_nr_config(consumer) + end + end + else # older versions + def producer + super.tap do |producer| + set_nr_config(producer) + end + end + + def consumer + super.tap do |consumer| + set_nr_config(consumer) + end + end + end + end + end +end diff --git a/lib/new_relic/agent/instrumentation/ruby_kafka.rb b/lib/new_relic/agent/instrumentation/ruby_kafka.rb new file mode 100644 index 0000000000..6ab2c88654 --- /dev/null +++ b/lib/new_relic/agent/instrumentation/ruby_kafka.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 'ruby_kafka/instrumentation' +require_relative 'ruby_kafka/chain' +require_relative 'ruby_kafka/prepend' + +DependencyDetection.defer do + named :'ruby_kafka' + + depends_on do + defined?(Kafka) + end + + executes do + NewRelic::Agent.logger.info('Installing ruby-kafka instrumentation') + + if use_prepend? + prepend_instrument Kafka::Producer, NewRelic::Agent::Instrumentation::RubyKafkaProducer::Prepend + prepend_instrument Kafka::Consumer, NewRelic::Agent::Instrumentation::RubyKafkaConsumer::Prepend + prepend_instrument Kafka::Client, NewRelic::Agent::Instrumentation::RubyKafkaClient::Prepend + else + chain_instrument NewRelic::Agent::Instrumentation::RubyKafka::Chain + end + end +end diff --git a/lib/new_relic/agent/instrumentation/ruby_kafka/chain.rb b/lib/new_relic/agent/instrumentation/ruby_kafka/chain.rb new file mode 100644 index 0000000000..896245a709 --- /dev/null +++ b/lib/new_relic/agent/instrumentation/ruby_kafka/chain.rb @@ -0,0 +1,55 @@ +# 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 RubyKafka::Chain + def self.instrument! + ::Kafka::Producer.class_eval do + include NewRelic::Agent::Instrumentation::RubyKafka + + alias_method(:produce_without_new_relic, :produce) + + def produce(value, **kwargs) + produce_with_new_relic(value, **kwargs) do |headers| + kwargs[:headers] = headers + produce_without_new_relic(value, **kwargs) + end + end + end + + ::Kafka::Consumer.class_eval do + include NewRelic::Agent::Instrumentation::RubyKafka + + alias_method(:each_message_without_new_relic, :each_message) + + def each_message(*args) + each_message_without_new_relic(*args) do |message| + each_message_with_new_relic(message) do + yield(message) + end + end + end + end + + ::Kafka::Client.class_eval do + include NewRelic::Agent::Instrumentation::RubyKafkaConfig + + alias_method(:producer_without_new_relic, :producer) + alias_method(:consumer_without_new_relic, :consumer) + + def producer(**kwargs) + producer_without_new_relic(**kwargs).tap do |producer| + set_nr_config(producer) + end + end + + def consumer(**kwargs) + consumer_without_new_relic(**kwargs).tap do |consumer| + set_nr_config(consumer) + end + end + end + end + end +end diff --git a/lib/new_relic/agent/instrumentation/ruby_kafka/instrumentation.rb b/lib/new_relic/agent/instrumentation/ruby_kafka/instrumentation.rb new file mode 100644 index 0000000000..389fa3cb86 --- /dev/null +++ b/lib/new_relic/agent/instrumentation/ruby_kafka/instrumentation.rb @@ -0,0 +1,67 @@ +# 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/messaging' + +module NewRelic::Agent::Instrumentation + module RubyKafka + MESSAGING_LIBRARY = 'Kafka' + PRODUCE = 'Produce' + CONSUME = 'Consume' + + INSTRUMENTATION_NAME = 'ruby-kafka' + + def produce_with_new_relic(value, **kwargs) + NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME) + + topic_name = kwargs[:topic] + segment = NewRelic::Agent::Tracer.start_message_broker_segment( + action: :produce, + library: MESSAGING_LIBRARY, + destination_type: :topic, + destination_name: topic_name + ) + create_kafka_metrics(action: PRODUCE, topic: topic_name) + + headers = kwargs[:headers] || {} + ::NewRelic::Agent::DistributedTracing.insert_distributed_trace_headers(headers) + + NewRelic::Agent::Tracer.capture_segment_error(segment) { yield(headers) } + ensure + segment&.finish + end + + def each_message_with_new_relic(message) + NewRelic::Agent.record_instrumentation_invocation(INSTRUMENTATION_NAME) + + headers = message&.headers || {} + topic_name = message&.topic + + NewRelic::Agent::Messaging.wrap_message_broker_consume_transaction( + library: MESSAGING_LIBRARY, + destination_type: :topic, + destination_name: topic_name, + headers: headers, + action: :consume + ) do + create_kafka_metrics(action: CONSUME, topic: topic_name) + yield + end + end + + def create_kafka_metrics(action:, topic:) + @nr_config.each do |seed_broker| + host = "#{seed_broker&.host}:#{seed_broker&.port}" + NewRelic::Agent.record_metric("MessageBroker/Kafka/Nodes/#{host}/#{action}/#{topic}", 1) + NewRelic::Agent.record_metric("MessageBroker/Kafka/Nodes/#{host}", 1) + end + end + end + + module RubyKafkaConfig + def set_nr_config(producer_or_consumer) + producer_or_consumer.instance_variable_set(:@nr_config, @seed_brokers) + end + end +end diff --git a/lib/new_relic/agent/instrumentation/ruby_kafka/prepend.rb b/lib/new_relic/agent/instrumentation/ruby_kafka/prepend.rb new file mode 100644 index 0000000000..66cec78a33 --- /dev/null +++ b/lib/new_relic/agent/instrumentation/ruby_kafka/prepend.rb @@ -0,0 +1,50 @@ +# 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 RubyKafkaProducer + module Prepend + include NewRelic::Agent::Instrumentation::RubyKafka + + def produce(value, **kwargs) + produce_with_new_relic(value, **kwargs) do |headers| + kwargs[:headers] = headers + super + end + end + end + end + + module RubyKafkaConsumer + module Prepend + include NewRelic::Agent::Instrumentation::RubyKafka + + def each_message(*args) + super do |message| + each_message_with_new_relic(message) do + yield(message) + end + end + end + end + end + + module RubyKafkaClient + module Prepend + include NewRelic::Agent::Instrumentation::RubyKafkaConfig + + def producer(**kwargs) + super.tap do |producer| + set_nr_config(producer) + end + end + + def consumer(**kwargs) + super.tap do |consumer| + set_nr_config(consumer) + end + end + end + end +end diff --git a/lib/new_relic/agent/instrumentation/view_component/instrumentation.rb b/lib/new_relic/agent/instrumentation/view_component/instrumentation.rb index 71488ca9d0..3005214c36 100644 --- a/lib/new_relic/agent/instrumentation/view_component/instrumentation.rb +++ b/lib/new_relic/agent/instrumentation/view_component/instrumentation.rb @@ -11,7 +11,10 @@ def render_in_with_tracing(*args) begin segment = NewRelic::Agent::Tracer.start_segment( - name: metric_name(self.class.identifier, self.class.name) + name: metric_name( + self.class.respond_to?(:identifier) ? self.class.identifier : nil, + self.class.name + ) ) yield rescue => e diff --git a/lib/new_relic/agent/javascript_instrumentor.rb b/lib/new_relic/agent/javascript_instrumentor.rb index e753d2f25c..3e1742c5db 100644 --- a/lib/new_relic/agent/javascript_instrumentor.rb +++ b/lib/new_relic/agent/javascript_instrumentor.rb @@ -164,9 +164,8 @@ def data_for_js_agent(transaction) def add_ssl_for_http(data) ssl_for_http = NewRelic::Agent.config[:'browser_monitoring.ssl_for_http'] - unless ssl_for_http.nil? - data[SSL_FOR_HTTP_KEY] = ssl_for_http - end + + data[SSL_FOR_HTTP_KEY] = ssl_for_http if ssl_for_http end def add_attributes(data, txn) diff --git a/lib/new_relic/agent/messaging.rb b/lib/new_relic/agent/messaging.rb index 01bc9b38c3..67bd3b1d9b 100644 --- a/lib/new_relic/agent/messaging.rb +++ b/lib/new_relic/agent/messaging.rb @@ -117,7 +117,8 @@ def wrap_message_broker_consume_transaction(library:, queue_name: nil, exchange_type: nil, reply_to: nil, - correlation_id: nil) + correlation_id: nil, + action: nil) state = Tracer.state return yield if state.current_transaction @@ -125,12 +126,12 @@ def wrap_message_broker_consume_transaction(library:, txn = nil begin - txn_name = transaction_name(library, destination_type, destination_name) + txn_name = transaction_name(library, destination_type, destination_name, action) txn = Tracer.start_transaction(name: txn_name, category: :message) - if headers - txn.distributed_tracer.consume_message_headers(headers, state, RABBITMQ_TRANSPORT_TYPE) + NewRelic::Agent::DistributedTracing::accept_distributed_trace_headers(headers, library) # to handle the new w3c headers + txn.distributed_tracer.consume_message_headers(headers, state, library) # to do the expected old things CrossAppTracing.reject_messaging_cat_headers(headers).each do |k, v| txn.add_agent_attribute(:"message.headers.#{k}", v, AttributeFilter::DST_NONE) unless v.nil? end @@ -327,12 +328,17 @@ def segment_parameters_enabled? NewRelic::Agent.config[:'message_tracer.segment_parameters.enabled'] end - def transaction_name(library, destination_type, destination_name) + def transaction_name(library, destination_type, destination_name, action = nil) transaction_name = Transaction::MESSAGE_PREFIX + library transaction_name << NewRelic::SLASH transaction_name << Transaction::MessageBrokerSegment::TYPES[destination_type] transaction_name << NewRelic::SLASH + if action == :consume + transaction_name << 'Consume' + transaction_name << NewRelic::SLASH + end + case destination_type when :queue transaction_name << Transaction::MessageBrokerSegment::NAMED diff --git a/lib/new_relic/agent/transaction/request_attributes.rb b/lib/new_relic/agent/transaction/request_attributes.rb index 60486ae94c..def6f07183 100644 --- a/lib/new_relic/agent/transaction/request_attributes.rb +++ b/lib/new_relic/agent/transaction/request_attributes.rb @@ -24,7 +24,7 @@ def initialize(request) @referer = referer_from_request(request) @accept = attribute_from_env(request, HTTP_ACCEPT_HEADER_KEY) @content_length = content_length_from_request(request) - @content_type = attribute_from_request(request, :content_type) + @content_type = content_type_attribute_from_request(request) @host = attribute_from_request(request, :host) @port = port_from_request(request) @user_agent = attribute_from_request(request, :user_agent) @@ -127,6 +127,18 @@ def attribute_from_request(request, attribute_method) end end + def content_type_attribute_from_request(request) + # Rails 7.0 changed the behavior of `content_type`. We want just the MIME type, so use `media_type` if available. + # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#actiondispatch-request-content-type-now-returns-content-type-header-as-it-is + content_type = if request.respond_to?(:media_type) + :media_type + elsif request.respond_to?(:content_type) + :content_type + end + + request.send(content_type) if content_type + end + def attribute_from_env(request, key) if env = attribute_from_request(request, :env) env[key] diff --git a/lib/new_relic/control/frameworks/rails4.rb b/lib/new_relic/control/frameworks/rails4.rb index 3b13d588c1..23d403dd69 100644 --- a/lib/new_relic/control/frameworks/rails4.rb +++ b/lib/new_relic/control/frameworks/rails4.rb @@ -9,7 +9,7 @@ class Control module Frameworks class Rails4 < NewRelic::Control::Frameworks::Rails3 def rails_gem_list - if Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('2.0.0') + if Bundler.rubygems.respond_to?(:installed_specs) Bundler.rubygems.installed_specs.map { |gem| "#{gem.name} (#{gem.version})" } else Bundler.rubygems.all_specs.map { |gem| "#{gem.name} (#{gem.version})" } diff --git a/lib/new_relic/environment_report.rb b/lib/new_relic/environment_report.rb index 7d2b487fc0..1661bebeb6 100644 --- a/lib/new_relic/environment_report.rb +++ b/lib/new_relic/environment_report.rb @@ -44,7 +44,7 @@ def self.registered_reporters=(logic) #################################### report_on('Gems') do begin - if Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('2.0.0') + if Bundler.rubygems.respond_to?(:installed_specs) Bundler.rubygems.installed_specs.map { |gem| "#{gem.name}(#{gem.version})" } else Bundler.rubygems.all_specs.map { |gem| "#{gem.name}(#{gem.version})" } diff --git a/lib/new_relic/language_support.rb b/lib/new_relic/language_support.rb index 438d899aa4..140d2455d2 100644 --- a/lib/new_relic/language_support.rb +++ b/lib/new_relic/language_support.rb @@ -90,7 +90,7 @@ def snakeize(string) def bundled_gem?(gem_name) return false unless defined?(Bundler) - if Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('2.0.0') + if Bundler.rubygems.respond_to?(:installed_specs) Bundler.rubygems.installed_specs.map(&:name).include?(gem_name) else Bundler.rubygems.all_specs.map(&:name).include?(gem_name) diff --git a/lib/new_relic/version.rb b/lib/new_relic/version.rb index e4889456a2..a64d74cf2e 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 = 13 + MINOR = 14 TINY = 0 STRING = "#{MAJOR}.#{MINOR}.#{TINY}" diff --git a/newrelic.yml b/newrelic.yml index cb2c78a591..a9f475437d 100644 --- a/newrelic.yml +++ b/newrelic.yml @@ -113,6 +113,53 @@ common: &default_settings # Specifies a path to the audit log file (including the filename). # audit_log.path: log/newrelic_audit.log + # An array of CLASS#METHOD (for instance methods) and/or CLASS.METHOD (for class + # methods) strings representing Ruby methods for the agent to automatically add + # custom instrumentation to without the need for altering any of the source code + # that defines the methods. + # + # Use fully qualified class names (using the :: delimiter) that include any + # module or class namespacing. + # + # Here is some Ruby source code that defines a render_png instance method for an + # Image class and a notify class method for a User class, both within a + # MyCompany module namespace: + # + # module MyCompany + # class Image + # def render_png + # # code to render a PNG + # end + # end + # + # class User + # def self.notify + # # code to notify users + # end + # end + # end + # + # Given that source code, the newrelic.yml config file might request + # instrumentation for both of these methods like so: + # + # automatic_custom_instrumentation_method_list: + # - MyCompany::Image#render_png + # - MyCompany::User.notify + # + # That configuration example uses YAML array syntax to specify both methods. + # Alternatively, a comma-delimited string can be used instead: + # + # automatic_custom_instrumentation_method_list: 'MyCompany::Image#render_png, MyCompany::User.notify' + # + # Whitespace around the comma(s) in the list is optional. When configuring the + # agent with a list of methods via the + # NEW_RELIC_AUTOMATIC_CUSTOM_INSTRUMENTATION_METHOD_LIST environment variable, + # this comma-delimited string format should be used: + # + # export NEW_RELIC_AUTOMATIC_CUSTOM_INSTRUMENTATION_METHOD_LIST='MyCompany::Image#render_png, MyCompany::User.notify' + # + # automatic_custom_instrumentation_method_list: [] + # Specify a list of constants that should prevent the agent from starting # automatically. Separate individual constants with a comma ,. For example, # "Rails::Console,UninstrumentedBackgroundJob". @@ -535,6 +582,10 @@ common: &default_settings # prepend, chain, disabled. # instrumentation.rake: auto + # Controls auto-instrumentation of the rdkafka library at start-up. May be one + # of auto, prepend, chain, disabled. + # instrumentation.rdkafka: auto + # Controls auto-instrumentation of Redis at start-up. May be one of: auto, # prepend, chain, disabled. # instrumentation.redis: auto @@ -547,6 +598,10 @@ common: &default_settings # prepend, chain, disabled. # instrumentation.roda: auto + # Controls auto-instrumentation of the ruby-kafka library at start-up. May be + # one of auto, prepend, chain, disabled. + # instrumentation.ruby_kafka: 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. diff --git a/test/environments/railsedge/Gemfile b/test/environments/railsedge/Gemfile index 366403f7c1..9446e5896b 100644 --- a/test/environments/railsedge/Gemfile +++ b/test/environments/railsedge/Gemfile @@ -11,7 +11,7 @@ gem 'mocha', '~> 1.16', require: false platforms :ruby, :rbx do gem 'mysql2', '>= 0.5.4' - gem 'sqlite3', '~> 2.0.4' + gem 'sqlite3', '>= 2.1' end gem 'newrelic_rpm', path: '../../..' diff --git a/test/helpers/misc.rb b/test/helpers/misc.rb index 2f0c0b3094..16a1d14945 100644 --- a/test/helpers/misc.rb +++ b/test/helpers/misc.rb @@ -131,8 +131,11 @@ def skip_unless_ci_cron skip 'This test only runs as part of the CI cron workflow' end +# If in a CI (non local dev) context, skip unless operating within the +# special CI context. When in a local dev context or in a special CI context, +# permit the test(s) to run. def skip_unless_special_ci - return if ENV['SPECIAL_CI'] + return if ENV.fetch('CI', nil).nil? || ENV.fetch('SPECIAL_CI', nil) skip 'This test only runs as part of the special CI workflow' end diff --git a/test/multiverse/lib/multiverse/envfile.rb b/test/multiverse/lib/multiverse/envfile.rb index 1c7d33b0db..5158996689 100644 --- a/test/multiverse/lib/multiverse/envfile.rb +++ b/test/multiverse/lib/multiverse/envfile.rb @@ -169,7 +169,7 @@ def unshift_rails_edge(gem_version_array = []) # NOTE: The Rails Edge version is not tested unless the Ruby version in # play is greater than or equal to (>=) the version number at the # end of the unshifted inner array - gem_version_array.unshift(["github: 'rails'", 3.1]) + gem_version_array.unshift(["github: 'rails'", 3.2]) end # are we running in a CI context intended for PR approvals? diff --git a/test/multiverse/suites/active_record_pg/Envfile b/test/multiverse/suites/active_record_pg/Envfile index 68d718c94d..de48cc5477 100644 --- a/test/multiverse/suites/active_record_pg/Envfile +++ b/test/multiverse/suites/active_record_pg/Envfile @@ -12,7 +12,8 @@ end serialize! ACTIVERECORD_VERSIONS = [ - [nil, 3.1], + [nil, 3.2], + ['7.2.0', 3.1], ['7.1.0', 2.7], ['7.0.0', 2.7], ['6.1.0', 2.5], @@ -28,7 +29,7 @@ def gem_list(activerecord_version = nil) <<~RB gem 'activerecord'#{activerecord_version} gem 'pg' - + gem 'rack' gem 'minitest', '~> 5.2.3' RB diff --git a/test/multiverse/suites/active_support_broadcast_logger/Envfile b/test/multiverse/suites/active_support_broadcast_logger/Envfile index 9a274d0c4f..cb5286d84c 100644 --- a/test/multiverse/suites/active_support_broadcast_logger/Envfile +++ b/test/multiverse/suites/active_support_broadcast_logger/Envfile @@ -7,7 +7,8 @@ instrumentation_methods :chain, :prepend # ActiveSupport::BroadcastLogger introduced in Rails 7.1. # Rails 7.1 is the latest version at the time of writing. ACTIVE_SUPPORT_VERSIONS = [ - [nil, 3.1], + [nil, 3.2], + ['7.2.0', 3.1], ['7.1.0', 2.7] ] diff --git a/test/multiverse/suites/dynamodb/dynamodb_instrumentation_test.rb b/test/multiverse/suites/dynamodb/dynamodb_instrumentation_test.rb index 72dfbc7ae0..9da2e08885 100644 --- a/test/multiverse/suites/dynamodb/dynamodb_instrumentation_test.rb +++ b/test/multiverse/suites/dynamodb/dynamodb_instrumentation_test.rb @@ -42,7 +42,8 @@ def test_all_attributes_added_to_segment assert_equal 'us-east-2', span[2]['aws.region'] assert_equal 'query', span[2]['aws.operation'] assert_equal '1234321', span[2]['aws.requestId'] - assert_equal 'test-arn', span[2]['cloud.resource_id'] + # TODO: Uncomment this when the ARN is added to the segment + # assert_equal 'test-arn', span[2]['cloud.resource_id'] end def test_create_table_table_name_operation diff --git a/test/multiverse/suites/rails/Envfile b/test/multiverse/suites/rails/Envfile index d5e9b485b6..f645b1b712 100644 --- a/test/multiverse/suites/rails/Envfile +++ b/test/multiverse/suites/rails/Envfile @@ -3,7 +3,7 @@ # frozen_string_literal: true RAILS_VERSIONS = [ - [nil, 3.1], + [nil, 3.2], ['7.2.0', 3.1], ['7.1.0', 2.7], ['7.0.4', 2.7], diff --git a/test/multiverse/suites/rails_prepend/Envfile b/test/multiverse/suites/rails_prepend/Envfile index 18884f4cb1..801a0e8ab2 100644 --- a/test/multiverse/suites/rails_prepend/Envfile +++ b/test/multiverse/suites/rails_prepend/Envfile @@ -3,7 +3,8 @@ # frozen_string_literal: true RAILS_VERSIONS = [ - [nil, 3.1], + [nil, 3.2], + ['7.2.0', 3.1], ['7.1.0', 2.7], ['7.0.0', 2.7], ['6.1.0', 2.5], diff --git a/test/multiverse/suites/rdkafka/Envfile b/test/multiverse/suites/rdkafka/Envfile new file mode 100644 index 0000000000..6c3d00b9eb --- /dev/null +++ b/test/multiverse/suites/rdkafka/Envfile @@ -0,0 +1,34 @@ +# 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 + +suite_condition("Does not run on JRuby") do + RUBY_PLATFORM != 'java' +end + +VERSIONS = [ + [nil, 2.5] +] + +def gem_list(sidekiq_version = nil) + <<-RB + gem 'rdkafka'#{sidekiq_version} + #{ffi} + RB +end + +def ffi + "gem 'ffi', '< 1.17.0'" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7') +end + +create_gemfiles(VERSIONS) + +# check karafka-rdkafka compatibility as well +# but we don't need to test it on every ruby version bc it should just be the same as rdkafka +if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('3.3.0') + gemfile <<~RB + gem 'karafka-rdkafka', require: 'rdkafka' + RB +end diff --git a/test/multiverse/suites/rdkafka/config/newrelic.yml b/test/multiverse/suites/rdkafka/config/newrelic.yml new file mode 100644 index 0000000000..bdc09e5915 --- /dev/null +++ b/test/multiverse/suites/rdkafka/config/newrelic.yml @@ -0,0 +1,19 @@ +--- +development: + error_collector: + enabled: true + apdex_t: 0.5 + monitor_mode: true + license_key: bootstrap_newrelic_admin_license_key_000 + instrumentation: + rdkafka: <%= $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 diff --git a/test/multiverse/suites/rdkafka/rdkafka_instrumentation_test.rb b/test/multiverse/suites/rdkafka/rdkafka_instrumentation_test.rb new file mode 100644 index 0000000000..78d3e60313 --- /dev/null +++ b/test/multiverse/suites/rdkafka/rdkafka_instrumentation_test.rb @@ -0,0 +1,130 @@ +# 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 RdkafkaInstrumentationTest < Minitest::Test + def setup + @topic = 'ruby-test-topic' + Time.now.to_i.to_s + Rdkafka::Config.logger = Logger.new(STDOUT, level: :error) + @stats_engine = NewRelic::Agent.instance.stats_engine + end + + def teardown + harvest_span_events! + harvest_transaction_events! + NewRelic::Agent.instance.stats_engine.clear_stats + mocha_teardown + end + + def test_produce_creates_span_metrics + in_transaction do |txn| + produce_message + end + + spans = harvest_span_events! + span = spans[1][0] + + assert_equal "MessageBroker/Kafka/Topic/Produce/Named/#{@topic}", span[0]['name'] + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}" + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}/Produce/#{@topic}" + end + + def test_consume_creates_span_metrics + produce_message + harvest_span_events! + + consumer = config.consumer + consumer.subscribe(@topic) + consumer.each do |message| + # get 1 message and leave + break + end + + spans = harvest_span_events! + span = spans[1][0] + + assert_equal "OtherTransaction/Message/Kafka/Topic/Consume/Named/#{@topic}", span[0]['name'] + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}" + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}/Consume/#{@topic}" + end + + def test_produce_with_different_host_key + producer = config('metadata.broker.list').producer + in_transaction do |txn| + produce_message(producer) + end + + spans = harvest_span_events! + span = spans[1][0] + + assert_equal "MessageBroker/Kafka/Topic/Produce/Named/#{@topic}", span[0]['name'] + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}" + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}/Produce/#{@topic}" + end + + def test_consume_with_different_host_key + produce_message + harvest_span_events! + + consumer = config('metadata.broker.list').consumer + consumer.subscribe(@topic) + consumer.each do |message| + # get 1 message and leave + break + end + + spans = harvest_span_events! + span = spans[1][0] + + assert_equal "OtherTransaction/Message/Kafka/Topic/Consume/Named/#{@topic}", span[0]['name'] + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}" + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}/Consume/#{@topic}" + end + + def test_rdkafka_distributed_tracing + NewRelic::Agent.agent.stub :connected?, true do + with_config(account_id: '190', primary_application_id: '46954', trusted_account_key: 'trust_this!') do + in_transaction('first_txn_for_dt') do |txn| + produce_message + end + end + first_txn = harvest_transaction_events![1] + + consumer = config.consumer + consumer.subscribe(@topic) + consumer.each do |message| + # get 1 message and leave + break + end + txn = harvest_transaction_events![1] + + assert_metrics_recorded 'Supportability/DistributedTrace/CreatePayload/Success' + assert_equal txn[0][0]['traceId'], first_txn[0][0]['traceId'] + assert_equal txn[0][0]['parentId'], first_txn[0][0]['guid'] + end + end + + def host + '127.0.0.1:9092' + end + + def config(host_key = 'bootstrap.servers') + config_vals ||= { + "#{host_key}": host, + "group.id": 'ruby-test', + 'auto.offset.reset': 'smallest' + } + Rdkafka::Config.new(config_vals) + end + + def produce_message(producer = config.producer) + delivery_handles = [] + delivery_handles << producer.produce( + topic: @topic, + payload: 'Payload 1', + key: 'Key 1' + ) + delivery_handles.each(&:wait) + producer.close + end +end diff --git a/test/multiverse/suites/ruby_kafka/Envfile b/test/multiverse/suites/ruby_kafka/Envfile new file mode 100644 index 0000000000..c8fa620dfa --- /dev/null +++ b/test/multiverse/suites/ruby_kafka/Envfile @@ -0,0 +1,14 @@ +# 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 + +suite_condition('Skip in CI on newer ruby versions') do + # will run locally OR on CI for ruby < 3.4.0 + !ENV['CI'] || RUBY_VERSION < '3.4.0' +end + +instrumentation_methods :chain, :prepend + +gemfile <<~RB + gem 'ruby-kafka' +RB diff --git a/test/multiverse/suites/ruby_kafka/config/newrelic.yml b/test/multiverse/suites/ruby_kafka/config/newrelic.yml new file mode 100644 index 0000000000..09a10a4f2d --- /dev/null +++ b/test/multiverse/suites/ruby_kafka/config/newrelic.yml @@ -0,0 +1,19 @@ +--- +development: + error_collector: + enabled: true + apdex_t: 0.5 + monitor_mode: true + license_key: bootstrap_newrelic_admin_license_key_000 + instrumentation: + ruby_kafka: <%= $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 diff --git a/test/multiverse/suites/ruby_kafka/ruby_kafka_instrumentation_test.rb b/test/multiverse/suites/ruby_kafka/ruby_kafka_instrumentation_test.rb new file mode 100644 index 0000000000..1e49a13370 --- /dev/null +++ b/test/multiverse/suites/ruby_kafka/ruby_kafka_instrumentation_test.rb @@ -0,0 +1,90 @@ +# 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 RubyKafkaInstrumentationTest < Minitest::Test + def setup + @topic = 'ruby-test-topic' + Time.now.to_i.to_s + @stats_engine = NewRelic::Agent.instance.stats_engine + end + + def teardown + harvest_span_events! + harvest_transaction_events! + NewRelic::Agent.instance.stats_engine.clear_stats + mocha_teardown + end + + def test_produce_creates_span_metrics + in_transaction do |txn| + produce_message + end + + spans = harvest_span_events! + span = spans[1][0] + + assert_equal "MessageBroker/Kafka/Topic/Produce/Named/#{@topic}", span[0]['name'] + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}" + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}/Produce/#{@topic}" + end + + def test_consume_creates_span_metrics + produce_message + harvest_span_events! + + consumer = config.consumer(group_id: 'ruby-test') + consumer.subscribe(@topic) + consumer.each_message do |message| + # get 1 message and leave + break + end + + spans = harvest_span_events! + span = spans[1][0] + + assert_equal "OtherTransaction/Message/Kafka/Topic/Consume/Named/#{@topic}", span[0]['name'] + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}" + assert_metrics_recorded "MessageBroker/Kafka/Nodes/#{host}/Consume/#{@topic}" + end + + def test_rdkafka_distributed_tracing + NewRelic::Agent.agent.stub :connected?, true do + with_config(account_id: '190', primary_application_id: '46954', trusted_account_key: 'trust_this!') do + in_transaction('first_txn_for_dt') do |txn| + produce_message + end + end + first_txn = harvest_transaction_events![1] + + consumer = config.consumer(group_id: 'ruby-test') + consumer.subscribe(@topic) + consumer.each_message do |message| + # get 1 message and leave + break + end + txn = harvest_transaction_events![1] + + assert_metrics_recorded 'Supportability/DistributedTrace/CreatePayload/Success' + assert_equal txn[0][0]['traceId'], first_txn[0][0]['traceId'] + assert_equal txn[0][0]['parentId'], first_txn[0][0]['guid'] + end + end + + def host + '127.0.0.1:9092' + end + + def config + Kafka.new([host], client_id: 'ruby-test') + end + + def produce_message(producer = config.producer) + producer.produce( + 'Payload 1', + topic: @topic, + key: 'Key 1' + ) + producer.deliver_messages + producer.shutdown + end +end diff --git a/test/multiverse/suites/sidekiq/sidekiq_instrumentation_test.rb b/test/multiverse/suites/sidekiq/sidekiq_instrumentation_test.rb index 60d7179dd6..9d75ffbf0f 100644 --- a/test/multiverse/suites/sidekiq/sidekiq_instrumentation_test.rb +++ b/test/multiverse/suites/sidekiq/sidekiq_instrumentation_test.rb @@ -53,4 +53,21 @@ def test_captures_sidekiq_internal_errors assert_equal 1, noticed.size assert_equal exception, noticed.first end + + # Sidekiq::Job::Setter#perform_inline is expected to light up all registered + # client and server middleware, and the lighting up of NR's server middleware + # will produce a segment + def test_works_with_perform_inline + # Sidekiq version 6.4.2 ends up invoking String#constantize, which is only + # delivered by ActiveSupport, which this test suite doesn't currently + # include. + skip 'This test requires Sidekiq v7+' unless Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('7.0.0') + + in_transaction do |txn| + NRDeadEndJob.perform_inline + segments = txn.segments.select { |s| s.name.eql?('Nested/OtherTransaction/SidekiqJob/NRDeadEndJob/perform') } + + assert_equal 1, segments.size, "Expected to find a single Sidekiq job segment, found #{segments.size}" + end + end end diff --git a/test/multiverse/suites/view_component/view_component_instrumentation_test.rb b/test/multiverse/suites/view_component/view_component_instrumentation_test.rb index b3cfbd908d..01e36d291c 100644 --- a/test/multiverse/suites/view_component/view_component_instrumentation_test.rb +++ b/test/multiverse/suites/view_component/view_component_instrumentation_test.rb @@ -57,4 +57,15 @@ def test_error_raised assert_equal(500, get('/view_components')) end end + + # Test metric name being built when the controller class doesn't respond to :identifier + # https://github.com/newrelic/newrelic-ruby-agent/pull/2870 + def test_the_metric_name_omits_the_identifier_when_absent + in_transaction do |txn| + FAKE_CLASS.render_in_with_tracing { 11 * 38 } + actual_name = txn.segments.last.name + + assert_equal 'View/component/DummyViewComponentInstrumentationClass', actual_name + end + end end diff --git a/test/new_relic/agent/configuration/default_source_test.rb b/test/new_relic/agent/configuration/default_source_test.rb index 43a2b50ebd..dd68ce405d 100644 --- a/test/new_relic/agent/configuration/default_source_test.rb +++ b/test/new_relic/agent/configuration/default_source_test.rb @@ -140,12 +140,6 @@ def test_config_search_path_in_warbler end end - def test_application_logging_enabled_default - with_config(:'application_logging.enabled' => :foo) do - assert_equal :foo, NewRelic::Agent.config['application_logging.enabled'] - end - end - def test_agent_attribute_settings_convert_comma_delimited_strings_into_an_arrays types = %w[transaction_tracer. transaction_events. error_collector. browser_monitoring.] types << '' @@ -291,6 +285,95 @@ def test_allowlist_blocks_invalid_values_and_uses_a_default end end + def test_automatic_custom_instrumentation_method_list_supports_an_array + key = :automatic_custom_instrumentation_method_list + list = %w[Beano::Roger#dodge Beano::Gnasher.gnash] + NewRelic::Agent.stub :add_tracers_once_methods_are_defined, nil do + with_config(key => list) do + assert_equal list, NewRelic::Agent.config[key], + "Expected '#{key}' to be configured with the unmodified original list" + end + end + end + + def test_automatic_custom_instrumentation_method_list_supports_a_comma_delmited_string + key = :automatic_custom_instrumentation_method_list + list = %w[Beano::Roger#dodge Beano::Gnasher.gnash] + NewRelic::Agent.stub :add_tracers_once_methods_are_defined, nil do + with_config(key => list.join(' ,')) do + assert_equal list, NewRelic::Agent.config[key], + "Expected '#{key}' to be configured with the given string converted into an array" + end + end + end + + def test_boolean_configs_accepts_yes_on_and_true_as_strings + key = :'send_data_on_exit' + config_array = %w[yes on true] + + config_array.each do |value| + with_config(key => value) do + assert NewRelic::Agent.config[key], "The '#{value}' value failed to evaluate as truthy!" + end + end + end + + def test_boolean_configs_accepts_yes_on_and_true_as_symbols + key = :'send_data_on_exit' + config_array = %i[yes on true] + + config_array.each do |value| + with_config(key => value) do + assert NewRelic::Agent.config[key], "The '#{value}' value failed to evaluate as truthy!" + end + end + end + + def test_boolean_configs_accepts_no_off_and_false_as_strings + key = :'send_data_on_exit' + + %w[no off false].each do |value| + with_config(key => value) do + refute NewRelic::Agent.config[key], "The '#{value}' value failed to evaluate as falsey!" + end + end + end + + def test_boolean_configs_accepts_no_off_and_false_as_strings_as_symbols + key = :'send_data_on_exit' + + %i[no off false].each do |value| + with_config(key => value) do + refute NewRelic::Agent.config[key], "The '#{value}' value failed to evaluate as falsey!" + end + end + end + + def test_enforce_boolean_uses_defult_on_invalid_value + key = :'send_data_on_exit' # default value is `true` + + with_config(key => 'invalid_value') do + assert NewRelic::Agent.config[key] + end + end + + def test_enforce_boolean_logs_warning_on_invalid_value + key = :'send_data_on_exit' + default = ::NewRelic::Agent::Configuration::DefaultSource.default_for(key) + + with_config(key => 'yikes!') do + expects_logging(:warn, includes("Invalid value 'yikes!' for #{key}, applying default value of '#{default}'")) + end + end + + def test_boolean_config_evaluates_proc_configs + key = :agent_enabled # default value is a proc + + with_config(key => 'off') do + refute NewRelic::Agent.config[key] + end + end + def get_config_value_class(value) type = value.class diff --git a/test/new_relic/agent/configuration/environment_source_test.rb b/test/new_relic/agent/configuration/environment_source_test.rb index 004e9250b0..056d889995 100644 --- a/test/new_relic/agent/configuration/environment_source_test.rb +++ b/test/new_relic/agent/configuration/environment_source_test.rb @@ -160,6 +160,20 @@ def test_set_key_by_type_converts_comma_lists_with_spaces_to_array assert_equal %w[hi bye], @environment_source[:'attributes.include'] end + def test_array_based_params_use_the_transform_proc_when_present + skip_unless_minitest5_or_above + + arr = %w[James Jessie Meowth] + + env_var = 'NEW_RELIC_AUTOMATIC_CUSTOM_INSTRUMENTATION_METHOD_LIST' + param = :automatic_custom_instrumentation_method_list + ENV.stub(:[], arr.join(','), [env_var]) do + @environment_source.set_key_by_type(param, env_var) + end + + assert_equal arr, @environment_source[param] + end + def test_set_key_with_new_relic_prefix assert_applied_string('NEW_RELIC_LICENSE_KEY', :license_key) end diff --git a/test/new_relic/agent/configuration/orphan_configuration_test.rb b/test/new_relic/agent/configuration/orphan_configuration_test.rb index f30041194b..d85827235d 100644 --- a/test/new_relic/agent/configuration/orphan_configuration_test.rb +++ b/test/new_relic/agent/configuration/orphan_configuration_test.rb @@ -8,6 +8,9 @@ class OrphanedConfigTest < Minitest::Test include NewRelic::TestHelpers::FileSearching include NewRelic::TestHelpers::ConfigScanning + # :automatic_custom_instrumentation_method_list - the tranform proc handles all processing, no other reference exists + IGNORED_KEYS = %i[automatic_custom_instrumentation_method_list] + def setup @default_keys = ::NewRelic::Agent::Configuration::DEFAULTS.keys end @@ -38,8 +41,16 @@ def test_all_default_source_config_keys_are_used_in_the_agent # This indicates that these keys are referenced and implemented in # an external gem, so we don't expect any explicit references to them # in the core gem's code. + # + + # Remove any of the following types of keys + # - "external" keys: these are expected to only be leveraged by "external" code bases (Infinite Tracing, CSEC) + # - "deprecated" keys: these are supported for a time and have their values set on new param names used in code + # - "ignored" keys: special cased params defined by a constant above @default_keys.delete_if do |key_name| - NewRelic::Agent::Configuration::DEFAULTS[key_name][:external] || NewRelic::Agent::Configuration::DEFAULTS[key_name][:deprecated] + NewRelic::Agent::Configuration::DEFAULTS[key_name][:external] || + NewRelic::Agent::Configuration::DEFAULTS[key_name][:deprecated] || + IGNORED_KEYS.include?(key_name) end assert_empty @default_keys diff --git a/test/new_relic/agent/transaction/request_attributes_test.rb b/test/new_relic/agent/transaction/request_attributes_test.rb index 5ed1baa593..20ccb7115e 100644 --- a/test/new_relic/agent/transaction/request_attributes_test.rb +++ b/test/new_relic/agent/transaction/request_attributes_test.rb @@ -127,13 +127,42 @@ def test_sets_content_length_from_request assert_equal 111, attrs.content_length end - def test_sets_content_type_from_request + def test_sets_content_type_from_request_content_type_attribute request = stub('request', :content_type => 'application/json') attrs = RequestAttributes.new(request) assert_equal 'application/json', attrs.content_type end + def test_sets_content_type_from_request_media_type_attribute + media_type = 'pool-party/alligator' + request = stub('request', media_type: media_type) + attrs = RequestAttributes.new(request) + + assert_equal media_type, attrs.content_type + end + + def test_sets_content_type_to_nil_if_media_type_is_available_with_a_nil_value + request = stub('request', media_type: nil) + attrs = RequestAttributes.new(request) + + assert_nil attrs.content_type + end + + def test_sets_content_type_to_nil_if_content_type_is_available_with_a_nil_value + request = stub('request', content_type: nil) + attrs = RequestAttributes.new(request) + + assert_nil attrs.content_type + end + + def test_sets_content_type_to_nil_if_neither_media_type_or_content_type_are_available + request = stub('request') + attrs = RequestAttributes.new(request) + + assert_nil attrs.content_type + end + def test_sets_host_from_request request = stub('request', :host => 'localhost') attrs = RequestAttributes.new(request) diff --git a/test/new_relic/agent_test.rb b/test/new_relic/agent_test.rb index e83c8df5f4..3bf5281dfa 100644 --- a/test/new_relic/agent_test.rb +++ b/test/new_relic/agent_test.rb @@ -12,6 +12,8 @@ module NewRelic class MainAgentTest < Minitest::Test include NewRelic::Agent::MethodTracer + class TesterClass; end + def setup NewRelic::Agent.drop_buffered_data NewRelic::Agent.reset_config @@ -698,6 +700,115 @@ def test_base_name_without_module_namespace assert_equal name, NewRelic::Agent.base_name(name) end + def test_add_automatic_method_tracers_short_circuits_with_a_nil_method_list + assert_nil NewRelic::Agent.add_automatic_method_tracers(nil) + end + + def test_add_automatic_method_tracers_returns_early_when_given_an_empty_method_list + arr = [] + + assert_equal arr, NewRelic::Agent.add_automatic_method_tracers(arr) + end + + # as of 2024-09-12, the agent doesn't natively permit a configuration + # parameter to accept either an array (definable only in the YAML file) or + # a comma-delimited string (definable in YAML or via an env var), so this + # method permits it by handling the `String#split` call itself. + def test_add_automatic_method_tracers_handles_a_comma_delimited_string + arr = %w[Astarion Gale Shadowheart] + # don't actually spawn the thread to add custom instrumentation + NewRelic::Agent.stub :add_tracers_once_methods_are_defined, nil do + assert_equal arr, NewRelic::Agent.add_automatic_method_tracers(arr.join(',')) + end + end + + # treat the configured list as immutable and operate on a dupe of the list + # when iterating over it and removing entries that have been processed + def test_add_automatic_method_tracers_processes_a_dupe_of_the_methods_array + arr = %w[Andy Barney] + dupe_object_id = nil + NewRelic::Agent.stub :add_tracers_once_methods_are_defined, proc { |a| dupe_object_id = a.object_id } do + result = NewRelic::Agent.add_automatic_method_tracers(arr) + + assert_same arr, result + assert dupe_object_id + refute_equal arr.object_id, dupe_object_id + end + end + + def test_prep_tracer_for_handles_a_non_delimited_notation + notation = 'StringWithoutADelimiter' + result = with_logger_expectation(:error, /Unable to parse out .*#{notation}.* Expected exactly/) do + NewRelic::Agent.prep_tracer_for(notation) + end + + assert result # `true`, we're not going to try processing this notation again + end + + def test_prep_tracer_for_handles_a_missing_namespace + notation = '.method_name' + result = with_logger_expectation(:error, /Unable to parse out .*#{notation}.* to the left of/) do + NewRelic::Agent.prep_tracer_for(notation) + end + + assert result # `true`, we're not going to try processing this notation again + end + + def test_prep_tracer_for_handles_a_missing_method_name + notation = 'namespace#' + result = with_logger_expectation(:error, /Unable to parse out .*#{notation}.* to the right of/) do + NewRelic::Agent.prep_tracer_for(notation) + end + + assert result # `true`, we're not going to try processing this notation again + end + + def test_prep_tracer_for_returns_false_if_the_notation_is_find_but_the_method_cannot_be_found_yet + notation = 'AGoodModuleNameSpace::AClass#a_method' # good syntax but undefined + result = NewRelic::Agent.prep_tracer_for(notation) + + refute result # `false`, we're going to try processing this notation again later + end + + def test_prep_tracer_handles_a_failed_tracer_add_attempt + skip_unless_minitest5_or_above + + notation = "Monk's.Blend" + NewRelic::LanguageSupport.stub :constantize, -> { raise 'kaboom' }, [notation] do + result = with_logger_expectation(:error, /Unable to automatically apply .*#{notation}/) do + NewRelic::Agent.prep_tracer_for(notation) + end + + assert result # `true`, we're not going to try processing this notation again + end + end + + def test_prep_tracer_for_traces_a_class_method + skip_unless_minitest5_or_above + + notation = 'A::Short.hike' + NewRelic::LanguageSupport.stub :constantize, TesterClass, [notation] do + NewRelic::Agent.stub :add_or_defer_method_tracer, nil, [TesterClass.singleton_class, 'hike', nil, {}] do + result = NewRelic::Agent.prep_tracer_for(notation) + + assert result # `true`, we're not going to try processing this notation again + end + end + end + + def test_prep_tracer_for_traces_an_instance_method + skip_unless_minitest5_or_above + + notation = 'A::Short#hike' + NewRelic::LanguageSupport.stub :constantize, TesterClass, [notation] do + NewRelic::Agent.stub :add_or_defer_method_tracer, nil, [TesterClass, 'hike', nil, {}] do + result = NewRelic::Agent.prep_tracer_for(notation) + + assert result # `true`, we're not going to try processing this notation again + end + end + end + private def with_unstarted_agent @@ -731,5 +842,13 @@ def fetch(k, d) NewRelic::Control.stubs(:instance).returns(control) control end + + def with_logger_expectation(log_level, expected_regex, &block) + logger = MiniTest::Mock.new + logger.expect log_level, nil, [expected_regex] + result = NewRelic::Agent.stub(:logger, logger) { yield } + logger.verify + result + end end end diff --git a/test/new_relic/healthy_urls_test.rb b/test/new_relic/healthy_urls_test.rb index ecedc22e16..925971e0d3 100644 --- a/test/new_relic/healthy_urls_test.rb +++ b/test/new_relic/healthy_urls_test.rb @@ -65,7 +65,7 @@ class HealthyUrlsTest < Minitest::Test FILE_PATTERN = /(?:^(?:#{FILENAMES.join('|')})$)|\.(?:#{EXTENSIONS.join('|')})$/.freeze IGNORED_FILE_PATTERN = %r{/(?:coverage|test)/}.freeze URL_PATTERN = %r{(https?://.*?)[^a-zA-Z0-9/\.\-_#]}.freeze - IGNORED_URL_PATTERN = %r{(?:\{|\(|\$|169\.254|\.\.\.|learn\.|metadata\.google|honeyryderchuck\.gitlab\.io/httpx|http://#)} + IGNORED_URL_PATTERN = %r{(?:\{|\(|\$|169\.254|\.\.\.|learn\.|metadata\.google|honeyryderchuck\.gitlab\.io/httpx|https?://#)} TIMEOUT = 5 DEBUG = false @@ -103,7 +103,7 @@ def gather_urls next unless line =~ URL_PATTERN url = Regexp.last_match(1).sub(%r{(?:/|\.)$}, '') - urls[url] << file if real_url?(url) + urls[url] << file if urls.key?(url) || real_url?(url) end end end diff --git a/test/script/run_tests b/test/script/run_tests index 9d9ad6aaaf..45e8e437a9 100755 --- a/test/script/run_tests +++ b/test/script/run_tests @@ -144,15 +144,48 @@ run_unit_tests() { fi } +# from a given space/newline delimited string of file paths, return the +# shortest path +# +# so if "bert agent" is invoked and all of these files match 'agent": +# +# new_relic/agent/rpm_agent_test.rb +# new_relic/agent/agent_test.rb +# new_relic/agent/distributed_tracing/trace_context_cross_agent_test.rb +# new_relic/agent/distributed_tracing/distributed_tracing_cross_agent_test.rb +# new_relic/agent/commands/agent_command_router_test.rb +# new_relic/agent/commands/agent_command_test.rb +# new_relic/agent/threading/agent_thread_test.rb +# new_relic/agent/agent_logger_test.rb +# new_relic/agent_test.rb +# new_relic/rack/agent_hooks_test.rb +# new_relic/rack/agent_middleware_test.rb +# +# then favor new_relic/agent_test.rb +shortest_test_file() { + files="$1" + shortest=1138000 + desired="" + for file in $files; do + len=${#file} + if (( "$len" < "$shortest" )); then + shortest=$len + desired=$file + fi + done + printf $desired +} + # If the first argument doesn't contain a slash or start with test_ # then assume it is a partial filename match. Find the file. find_test_file() { if [[ "$1" != */* && "$1" != test* ]]; then - file=$(find "test/$2" -name "*$1*" -print -quit) - if [[ "$file" == "" ]]; then + files=$(find "test/$2" -type f -name "*$1*" -print) + if [[ "$files" == "" ]]; then echo "Could not find a file match for '$1'" exit else + file=$(shortest_test_file "$files") echo "Testing against file '$file'..." TEST="$file" fi