diff --git a/.changesets/add-collector-attribute-options.md b/.changesets/add-collector-attribute-options.md
new file mode 100644
index 000000000..3e3c0c0ba
--- /dev/null
+++ b/.changesets/add-collector-attribute-options.md
@@ -0,0 +1,10 @@
+---
+bump: minor
+type: add
+---
+
+Add configuration options that map to OpenTelemetry resource attributes under collector mode: `service_name`, `filter_attributes`, `filter_function_parameters`, `filter_request_query_parameters`, `filter_request_payload`, `response_headers`, `send_function_parameters`, `send_request_query_parameters`, and `send_request_payload`. These tell the AppSignal Collector how to filter and forward telemetry data.
+
+When collector mode is active, existing configuration options (`name`, environment, `hostname`, `revision`, `ignore_actions`, `ignore_errors`, `ignore_namespaces`, `request_headers`, `filter_session_data`, `send_session_data`) are now passed to the collector as OpenTelemetry resource attributes.
+
+Setting any of these options without `collector_endpoint`, or `filter_parameters`/`filter_metadata`/`send_params` with `collector_endpoint`, now logs a warning at startup.
diff --git a/.changesets/add-collector-mode.md b/.changesets/add-collector-mode.md
new file mode 100644
index 000000000..b10465b5f
--- /dev/null
+++ b/.changesets/add-collector-mode.md
@@ -0,0 +1,19 @@
+---
+bump: minor
+type: add
+---
+
+Add a new `collector_endpoint` configuration option (`APPSIGNAL_COLLECTOR_ENDPOINT` environment variable) that puts the integration in _collector mode_. When set, AppSignal additionally configures an OpenTelemetry SDK that exports OTLP/HTTP protobuf traces, metrics, and logs to the configured endpoint. The existing AppSignal agent continues to run unchanged; no AppSignal-collected data flows through the OpenTelemetry SDK yet.
+
+Collector mode requires Ruby 3.1 or newer and the OpenTelemetry gems, which are optional and not installed by default. To use it, add them to your application's `Gemfile`:
+
+```ruby
+gem "opentelemetry-sdk", ">= 1.8.0"
+gem "opentelemetry-metrics-sdk", ">= 0.7.1"
+gem "opentelemetry-logs-sdk", ">= 0.2.0"
+gem "opentelemetry-exporter-otlp", ">= 0.30.0"
+gem "opentelemetry-exporter-otlp-metrics", ">= 0.4.0"
+gem "opentelemetry-exporter-otlp-logs", ">= 0.2.0"
+```
+
+If these gems are missing or older than the minimum versions, AppSignal logs a warning and falls back to the bundled agent.
diff --git a/.changesets/add-faraday-integration.md b/.changesets/add-faraday-integration.md
index a4e537141..0ac3fe85f 100644
--- a/.changesets/add-faraday-integration.md
+++ b/.changesets/add-faraday-integration.md
@@ -3,4 +3,4 @@ bump: minor
type: add
---
-Improve Faraday support. AppSignal now instruments Faraday requests automatically, without double-instrumenting the underlying HTTP client. Turn it off with the `instrument_faraday` option.
+Improve Faraday support. AppSignal now instruments Faraday requests automatically, without double-instrumenting the underlying HTTP client, and, in collector mode, propagates trace context to the called service so it joins the same distributed trace. Turn it off with the `instrument_faraday` option.
diff --git a/.changesets/add-que-distributed-tracing.md b/.changesets/add-que-distributed-tracing.md
new file mode 100644
index 000000000..db8ced36d
--- /dev/null
+++ b/.changesets/add-que-distributed-tracing.md
@@ -0,0 +1,6 @@
+---
+bump: minor
+type: add
+---
+
+Improve Que support. In collector mode, AppSignal now propagates trace context when enqueuing Que jobs, so each job links back to the trace that enqueued it. This covers single and bulk enqueues, on both Que 1 and Que 2.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 70baeb3b5..1ccb1178e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,7 +2,7 @@
# This is a generated file by the `rake build_matrix:github:generate` task.
# See `build_matrix.yml` for the build matrix.
# Generate this file with `rake build_matrix:github:generate`.
-# Generated job count: 234
+# Generated job count: 408
---
name: Ruby gem CI
'on':
@@ -123,7 +123,8 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &1
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
- name: Run tests without extension
run: "./script/bundler_wrapper exec rake test:failure"
@@ -132,8 +133,8 @@ jobs:
JRUBY_OPTS: ''
COV: '1'
BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
- ruby_4-0-0__capistrano2_ubuntu-latest:
- name: Ruby 4.0.0 - capistrano2
+ ruby_4-0-0__no_dependencies-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - no_dependencies-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -152,15 +153,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *1
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
- ruby_4-0-0__capistrano3_ubuntu-latest:
- name: Ruby 4.0.0 - capistrano3
+ BUNDLE_GEMFILE: gemfiles/no_dependencies-collector.gemfile
+ ruby_4-0-0__capistrano2_ubuntu-latest:
+ name: Ruby 4.0.0 - capistrano2
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -179,15 +179,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &2
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
- ruby_4-0-0__code_ownership_ubuntu-latest:
- name: Ruby 4.0.0 - code_ownership
+ BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
+ ruby_4-0-0__capistrano2-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - capistrano2-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -206,15 +207,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *2
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/code_ownership.gemfile
- ruby_4-0-0__dry-monitor_ubuntu-latest:
- name: Ruby 4.0.0 - dry-monitor
+ BUNDLE_GEMFILE: gemfiles/capistrano2-collector.gemfile
+ ruby_4-0-0__capistrano3_ubuntu-latest:
+ name: Ruby 4.0.0 - capistrano3
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -233,15 +233,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &3
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
- ruby_4-0-0__faraday-1_ubuntu-latest:
- name: Ruby 4.0.0 - faraday-1
+ BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
+ ruby_4-0-0__capistrano3-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - capistrano3-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -260,15 +261,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *3
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
- ruby_4-0-0__faraday-2_ubuntu-latest:
- name: Ruby 4.0.0 - faraday-2
+ BUNDLE_GEMFILE: gemfiles/capistrano3-collector.gemfile
+ ruby_4-0-0__code_ownership_ubuntu-latest:
+ name: Ruby 4.0.0 - code_ownership
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -287,15 +287,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &4
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
- ruby_4-0-0__grape_ubuntu-latest:
- name: Ruby 4.0.0 - grape
+ BUNDLE_GEMFILE: gemfiles/code_ownership.gemfile
+ ruby_4-0-0__code_ownership-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - code_ownership-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -314,15 +315,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *4
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/grape.gemfile
- ruby_4-0-0__http5_ubuntu-latest:
- name: Ruby 4.0.0 - http5
+ BUNDLE_GEMFILE: gemfiles/code_ownership-collector.gemfile
+ ruby_4-0-0__dry-monitor_ubuntu-latest:
+ name: Ruby 4.0.0 - dry-monitor
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -341,15 +341,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &5
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/http5.gemfile
- ruby_4-0-0__http6_ubuntu-latest:
- name: Ruby 4.0.0 - http6
+ BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
+ ruby_4-0-0__dry-monitor-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - dry-monitor-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -368,15 +369,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *5
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/http6.gemfile
- ruby_4-0-0__mongo_ubuntu-latest:
- name: Ruby 4.0.0 - mongo
+ BUNDLE_GEMFILE: gemfiles/dry-monitor-collector.gemfile
+ ruby_4-0-0__excon_ubuntu-latest:
+ name: Ruby 4.0.0 - excon
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -395,15 +395,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &6
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/mongo.gemfile
- ruby_4-0-0__ownership_ubuntu-latest:
- name: Ruby 4.0.0 - ownership
+ BUNDLE_GEMFILE: gemfiles/excon.gemfile
+ ruby_4-0-0__excon-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - excon-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -422,15 +423,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *6
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/ownership.gemfile
- ruby_4-0-0__psych-3_ubuntu-latest:
- name: Ruby 4.0.0 - psych-3
+ BUNDLE_GEMFILE: gemfiles/excon-collector.gemfile
+ ruby_4-0-0__faraday-1_ubuntu-latest:
+ name: Ruby 4.0.0 - faraday-1
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -449,15 +449,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &7
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
- ruby_4-0-0__psych-4_ubuntu-latest:
- name: Ruby 4.0.0 - psych-4
+ BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
+ ruby_4-0-0__faraday-1-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - faraday-1-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -476,15 +477,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *7
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
- ruby_4-0-0__que-1_ubuntu-latest:
- name: Ruby 4.0.0 - que-1
+ BUNDLE_GEMFILE: gemfiles/faraday-1-collector.gemfile
+ ruby_4-0-0__faraday-2_ubuntu-latest:
+ name: Ruby 4.0.0 - faraday-2
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -503,15 +503,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &8
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-1.gemfile
- ruby_4-0-0__que-2_ubuntu-latest:
- name: Ruby 4.0.0 - que-2
+ BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
+ ruby_4-0-0__faraday-2-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - faraday-2-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -530,15 +531,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *8
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-2.gemfile
- ruby_4-0-0__rails-7-0_ubuntu-latest:
- name: Ruby 4.0.0 - rails-7.0
+ BUNDLE_GEMFILE: gemfiles/faraday-2-collector.gemfile
+ ruby_4-0-0__grape_ubuntu-latest:
+ name: Ruby 4.0.0 - grape
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -557,15 +557,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &9
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
- ruby_4-0-0__rails-7-1_ubuntu-latest:
- name: Ruby 4.0.0 - rails-7.1
+ BUNDLE_GEMFILE: gemfiles/grape.gemfile
+ ruby_4-0-0__grape-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - grape-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -584,15 +585,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *9
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
- ruby_4-0-0__rails-7-2_ubuntu-latest:
- name: Ruby 4.0.0 - rails-7.2
+ BUNDLE_GEMFILE: gemfiles/grape-collector.gemfile
+ ruby_4-0-0__http5_ubuntu-latest:
+ name: Ruby 4.0.0 - http5
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -611,15 +611,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &10
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
- ruby_4-0-0__rails-8-0_ubuntu-latest:
- name: Ruby 4.0.0 - rails-8.0
+ BUNDLE_GEMFILE: gemfiles/http5.gemfile
+ ruby_4-0-0__http5-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - http5-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -638,15 +639,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *10
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-8.0.gemfile
- ruby_4-0-0__resque-2_ubuntu-latest:
- name: Ruby 4.0.0 - resque-2
+ BUNDLE_GEMFILE: gemfiles/http5-collector.gemfile
+ ruby_4-0-0__http6_ubuntu-latest:
+ name: Ruby 4.0.0 - http6
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -665,15 +665,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &11
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
- ruby_4-0-0__resque-3_ubuntu-latest:
- name: Ruby 4.0.0 - resque-3
+ BUNDLE_GEMFILE: gemfiles/http6.gemfile
+ ruby_4-0-0__http6-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - http6-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -692,15 +693,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *11
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
- ruby_4-0-0__sequel_ubuntu-latest:
- name: Ruby 4.0.0 - sequel
+ BUNDLE_GEMFILE: gemfiles/http6-collector.gemfile
+ ruby_4-0-0__mongo_ubuntu-latest:
+ name: Ruby 4.0.0 - mongo
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -719,15 +719,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &12
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sequel.gemfile
- ruby_4-0-0__shoryuken-7_ubuntu-latest:
- name: Ruby 4.0.0 - shoryuken-7
+ BUNDLE_GEMFILE: gemfiles/mongo.gemfile
+ ruby_4-0-0__mongo-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - mongo-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -746,15 +747,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *12
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/shoryuken-7.gemfile
- ruby_4-0-0__sinatra_ubuntu-latest:
- name: Ruby 4.0.0 - sinatra
+ BUNDLE_GEMFILE: gemfiles/mongo-collector.gemfile
+ ruby_4-0-0__ownership_ubuntu-latest:
+ name: Ruby 4.0.0 - ownership
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -773,15 +773,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &13
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
- ruby_4-0-0__webmachine2_ubuntu-latest:
- name: Ruby 4.0.0 - webmachine2
+ BUNDLE_GEMFILE: gemfiles/ownership.gemfile
+ ruby_4-0-0__ownership-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - ownership-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -800,15 +801,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *13
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
- ruby_4-0-0__redis-4_ubuntu-latest:
- name: Ruby 4.0.0 - redis-4
+ BUNDLE_GEMFILE: gemfiles/ownership-collector.gemfile
+ ruby_4-0-0__psych-3_ubuntu-latest:
+ name: Ruby 4.0.0 - psych-3
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -827,15 +827,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &14
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/redis-4.gemfile
- ruby_4-0-0__redis-5_ubuntu-latest:
- name: Ruby 4.0.0 - redis-5
+ BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
+ ruby_4-0-0__psych-3-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - psych-3-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -854,15 +855,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *14
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
- ruby_4-0-0__sidekiq-7_ubuntu-latest:
- name: Ruby 4.0.0 - sidekiq-7
+ BUNDLE_GEMFILE: gemfiles/psych-3-collector.gemfile
+ ruby_4-0-0__psych-4_ubuntu-latest:
+ name: Ruby 4.0.0 - psych-4
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -881,15 +881,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &15
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sidekiq-7.gemfile
- ruby_4-0-0__sidekiq-8_ubuntu-latest:
- name: Ruby 4.0.0 - sidekiq-8
+ BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
+ ruby_4-0-0__psych-4-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - psych-4-collector
needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -908,17 +909,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *15
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sidekiq-8.gemfile
- ruby_4-0-0_macos-14:
- name: Ruby 4.0.0 (macos-14)
- needs: validation
- runs-on: macos-14
+ BUNDLE_GEMFILE: gemfiles/psych-4-collector.gemfile
+ ruby_4-0-0__que-1_ubuntu-latest:
+ name: Ruby 4.0.0 - que-1
+ needs: ruby_4-0-0_ubuntu-latest
+ runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
@@ -935,18 +935,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &16
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
- - name: Run tests without extension
- run: "./script/bundler_wrapper exec rake test:failure"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
- ruby_3-4-1_ubuntu-latest:
- name: Ruby 3.4.1
- needs: validation
+ BUNDLE_GEMFILE: gemfiles/que-1.gemfile
+ ruby_4-0-0__que-1-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - que-1-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -954,7 +953,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -964,18 +963,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
- - name: Run tests without extension
- run: "./script/bundler_wrapper exec rake test:failure"
+ - *16
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
- ruby_3-4-1__capistrano2_ubuntu-latest:
- name: Ruby 3.4.1 - capistrano2
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/que-1-collector.gemfile
+ ruby_4-0-0__que-2_ubuntu-latest:
+ name: Ruby 4.0.0 - que-2
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -983,7 +979,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -993,16 +989,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &17
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
- ruby_3-4-1__capistrano3_ubuntu-latest:
- name: Ruby 3.4.1 - capistrano3
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/que-2.gemfile
+ ruby_4-0-0__que-2-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - que-2-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1010,7 +1007,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1020,16 +1017,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *17
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
- ruby_3-4-1__code_ownership_ubuntu-latest:
- name: Ruby 3.4.1 - code_ownership
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/que-2-collector.gemfile
+ ruby_4-0-0__rails-7-0_ubuntu-latest:
+ name: Ruby 4.0.0 - rails-7.0
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1037,7 +1033,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1047,16 +1043,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &18
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/code_ownership.gemfile
- ruby_3-4-1__dry-monitor_ubuntu-latest:
- name: Ruby 3.4.1 - dry-monitor
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
+ ruby_4-0-0__rails-7-0-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - rails-7.0-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1064,7 +1061,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1074,16 +1071,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *18
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
- ruby_3-4-1__faraday-1_ubuntu-latest:
- name: Ruby 3.4.1 - faraday-1
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-7.0-collector.gemfile
+ ruby_4-0-0__rails-7-1_ubuntu-latest:
+ name: Ruby 4.0.0 - rails-7.1
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1091,7 +1087,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1101,16 +1097,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &19
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
- ruby_3-4-1__faraday-2_ubuntu-latest:
- name: Ruby 3.4.1 - faraday-2
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
+ ruby_4-0-0__rails-7-1-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - rails-7.1-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1118,7 +1115,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1128,16 +1125,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *19
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
- ruby_3-4-1__grape_ubuntu-latest:
- name: Ruby 3.4.1 - grape
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-7.1-collector.gemfile
+ ruby_4-0-0__rails-7-2_ubuntu-latest:
+ name: Ruby 4.0.0 - rails-7.2
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1145,7 +1141,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1155,16 +1151,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &20
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/grape.gemfile
- ruby_3-4-1__hanami-2-0_ubuntu-latest:
- name: Ruby 3.4.1 - hanami-2.0
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
+ ruby_4-0-0__rails-7-2-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - rails-7.2-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1172,7 +1169,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1182,16 +1179,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *20
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.0.gemfile
- ruby_3-4-1__hanami-2-1_ubuntu-latest:
- name: Ruby 3.4.1 - hanami-2.1
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-7.2-collector.gemfile
+ ruby_4-0-0__rails-8-0_ubuntu-latest:
+ name: Ruby 4.0.0 - rails-8.0
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1199,7 +1195,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1209,16 +1205,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &21
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.1.gemfile
- ruby_3-4-1__hanami-2-2_ubuntu-latest:
- name: Ruby 3.4.1 - hanami-2.2
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-8.0.gemfile
+ ruby_4-0-0__rails-8-0-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - rails-8.0-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1226,7 +1223,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1236,16 +1233,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *21
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.2.gemfile
- ruby_3-4-1__http5_ubuntu-latest:
- name: Ruby 3.4.1 - http5
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-8.0-collector.gemfile
+ ruby_4-0-0__resque-2_ubuntu-latest:
+ name: Ruby 4.0.0 - resque-2
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1253,7 +1249,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1263,16 +1259,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &22
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/http5.gemfile
- ruby_3-4-1__http6_ubuntu-latest:
- name: Ruby 3.4.1 - http6
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
+ ruby_4-0-0__resque-2-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - resque-2-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1280,7 +1277,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1290,16 +1287,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *22
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/http6.gemfile
- ruby_3-4-1__mongo_ubuntu-latest:
- name: Ruby 3.4.1 - mongo
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/resque-2-collector.gemfile
+ ruby_4-0-0__resque-3_ubuntu-latest:
+ name: Ruby 4.0.0 - resque-3
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1307,7 +1303,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1317,16 +1313,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &23
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/mongo.gemfile
- ruby_3-4-1__ownership_ubuntu-latest:
- name: Ruby 3.4.1 - ownership
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
+ ruby_4-0-0__resque-3-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - resque-3-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1334,7 +1331,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1344,16 +1341,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *23
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/ownership.gemfile
- ruby_3-4-1__padrino_ubuntu-latest:
- name: Ruby 3.4.1 - padrino
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/resque-3-collector.gemfile
+ ruby_4-0-0__sequel_ubuntu-latest:
+ name: Ruby 4.0.0 - sequel
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1361,7 +1357,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1371,16 +1367,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &24
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/padrino.gemfile
- ruby_3-4-1__psych-3_ubuntu-latest:
- name: Ruby 3.4.1 - psych-3
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sequel.gemfile
+ ruby_4-0-0__sequel-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - sequel-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1388,7 +1385,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1398,16 +1395,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *24
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
- ruby_3-4-1__psych-4_ubuntu-latest:
- name: Ruby 3.4.1 - psych-4
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sequel-collector.gemfile
+ ruby_4-0-0__shoryuken-7_ubuntu-latest:
+ name: Ruby 4.0.0 - shoryuken-7
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1415,7 +1411,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1425,16 +1421,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &25
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
- ruby_3-4-1__que-1_ubuntu-latest:
- name: Ruby 3.4.1 - que-1
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/shoryuken-7.gemfile
+ ruby_4-0-0__shoryuken-7-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - shoryuken-7-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1442,7 +1439,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1452,16 +1449,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *25
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-1.gemfile
- ruby_3-4-1__que-2_ubuntu-latest:
- name: Ruby 3.4.1 - que-2
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/shoryuken-7-collector.gemfile
+ ruby_4-0-0__sinatra_ubuntu-latest:
+ name: Ruby 4.0.0 - sinatra
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1469,7 +1465,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1479,16 +1475,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &26
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-2.gemfile
- ruby_3-4-1__rails-7-0_ubuntu-latest:
- name: Ruby 3.4.1 - rails-7.0
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
+ ruby_4-0-0__sinatra-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - sinatra-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1496,7 +1493,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1506,16 +1503,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *26
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
- ruby_3-4-1__rails-7-1_ubuntu-latest:
- name: Ruby 3.4.1 - rails-7.1
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sinatra-collector.gemfile
+ ruby_4-0-0__webmachine2_ubuntu-latest:
+ name: Ruby 4.0.0 - webmachine2
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1523,7 +1519,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1533,16 +1529,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &27
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
- ruby_3-4-1__rails-7-2_ubuntu-latest:
- name: Ruby 3.4.1 - rails-7.2
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
+ ruby_4-0-0__webmachine2-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - webmachine2-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1550,7 +1547,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1560,16 +1557,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *27
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
- ruby_3-4-1__rails-8-0_ubuntu-latest:
- name: Ruby 3.4.1 - rails-8.0
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/webmachine2-collector.gemfile
+ ruby_4-0-0__redis-4_ubuntu-latest:
+ name: Ruby 4.0.0 - redis-4
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1577,7 +1573,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1587,16 +1583,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &28
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-8.0.gemfile
- ruby_3-4-1__rails-8-1_ubuntu-latest:
- name: Ruby 3.4.1 - rails-8.1
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/redis-4.gemfile
+ ruby_4-0-0__redis-4-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - redis-4-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1604,7 +1601,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1614,16 +1611,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *28
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-8.1.gemfile
- ruby_3-4-1__resque-2_ubuntu-latest:
- name: Ruby 3.4.1 - resque-2
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/redis-4-collector.gemfile
+ ruby_4-0-0__redis-5_ubuntu-latest:
+ name: Ruby 4.0.0 - redis-5
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1631,7 +1627,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1641,16 +1637,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &29
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
- ruby_3-4-1__resque-3_ubuntu-latest:
- name: Ruby 3.4.1 - resque-3
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
+ ruby_4-0-0__redis-5-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - redis-5-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1658,7 +1655,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1668,16 +1665,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *29
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
- ruby_3-4-1__sequel_ubuntu-latest:
- name: Ruby 3.4.1 - sequel
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/redis-5-collector.gemfile
+ ruby_4-0-0__sidekiq-7_ubuntu-latest:
+ name: Ruby 4.0.0 - sidekiq-7
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1685,7 +1681,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1695,16 +1691,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &30
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sequel.gemfile
- ruby_3-4-1__shoryuken-7_ubuntu-latest:
- name: Ruby 3.4.1 - shoryuken-7
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sidekiq-7.gemfile
+ ruby_4-0-0__sidekiq-7-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - sidekiq-7-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1712,7 +1709,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1722,16 +1719,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *30
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/shoryuken-7.gemfile
- ruby_3-4-1__sinatra_ubuntu-latest:
- name: Ruby 3.4.1 - sinatra
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sidekiq-7-collector.gemfile
+ ruby_4-0-0__sidekiq-8_ubuntu-latest:
+ name: Ruby 4.0.0 - sidekiq-8
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1739,7 +1735,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1749,16 +1745,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &31
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
- ruby_3-4-1__webmachine2_ubuntu-latest:
- name: Ruby 3.4.1 - webmachine2
- needs: ruby_3-4-1_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sidekiq-8.gemfile
+ ruby_4-0-0__sidekiq-8-collector_ubuntu-latest:
+ name: Ruby 4.0.0 - sidekiq-8-collector
+ needs: ruby_4-0-0_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1766,7 +1763,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1776,24 +1773,23 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *31
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
- ruby_3-4-1__redis-4_ubuntu-latest:
- name: Ruby 3.4.1 - redis-4
- needs: ruby_3-4-1_ubuntu-latest
- runs-on: ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sidekiq-8-collector.gemfile
+ ruby_4-0-0_macos-14:
+ name: Ruby 4.0.0 (macos-14)
+ needs: validation
+ runs-on: macos-14
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.4.1
+ ruby-version: 4.0.0
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1805,15 +1801,17 @@ jobs:
found'"
- name: Run tests
run: "./script/bundler_wrapper exec rake test"
+ - name: Run tests without extension
+ run: "./script/bundler_wrapper exec rake test:failure"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/redis-4.gemfile
- ruby_3-4-1__redis-5_ubuntu-latest:
- name: Ruby 3.4.1 - redis-5
- needs: ruby_3-4-1_ubuntu-latest
- runs-on: ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
+ ruby_3-4-1_ubuntu-latest:
+ name: Ruby 3.4.1
+ needs: validation
+ runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
@@ -1830,15 +1828,18 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &32
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
+ - name: Run tests without extension
+ run: "./script/bundler_wrapper exec rake test:failure"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
- ruby_3-4-1__sidekiq-7_ubuntu-latest:
- name: Ruby 3.4.1 - sidekiq-7
+ BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
+ ruby_3-4-1__no_dependencies-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - no_dependencies-collector
needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -1857,15 +1858,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *32
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sidekiq-7.gemfile
- ruby_3-4-1__sidekiq-8_ubuntu-latest:
- name: Ruby 3.4.1 - sidekiq-8
+ BUNDLE_GEMFILE: gemfiles/no_dependencies-collector.gemfile
+ ruby_3-4-1__capistrano2_ubuntu-latest:
+ name: Ruby 3.4.1 - capistrano2
needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -1884,17 +1884,18 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &33
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sidekiq-8.gemfile
- ruby_3-4-1_macos-14:
- name: Ruby 3.4.1 (macos-14)
- needs: validation
- runs-on: macos-14
+ BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
+ ruby_3-4-1__capistrano2-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - capistrano2-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
@@ -1911,18 +1912,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
- - name: Run tests without extension
- run: "./script/bundler_wrapper exec rake test:failure"
+ - *33
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
- ruby_3-3-4_ubuntu-latest:
- name: Ruby 3.3.4
- needs: validation
+ BUNDLE_GEMFILE: gemfiles/capistrano2-collector.gemfile
+ ruby_3-4-1__capistrano3_ubuntu-latest:
+ name: Ruby 3.4.1 - capistrano3
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1930,7 +1928,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1940,18 +1938,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &34
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
- - name: Run tests without extension
- run: "./script/bundler_wrapper exec rake test:failure"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
- ruby_3-3-4__capistrano2_ubuntu-latest:
- name: Ruby 3.3.4 - capistrano2
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
+ ruby_3-4-1__capistrano3-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - capistrano3-collector
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1959,7 +1956,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1969,16 +1966,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *34
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
- ruby_3-3-4__capistrano3_ubuntu-latest:
- name: Ruby 3.3.4 - capistrano3
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/capistrano3-collector.gemfile
+ ruby_3-4-1__code_ownership_ubuntu-latest:
+ name: Ruby 3.4.1 - code_ownership
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -1986,7 +1982,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -1996,16 +1992,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &35
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
- ruby_3-3-4__code_ownership_ubuntu-latest:
- name: Ruby 3.3.4 - code_ownership
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/code_ownership.gemfile
+ ruby_3-4-1__code_ownership-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - code_ownership-collector
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2013,7 +2010,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2023,16 +2020,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *35
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/code_ownership.gemfile
- ruby_3-3-4__dry-monitor_ubuntu-latest:
- name: Ruby 3.3.4 - dry-monitor
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/code_ownership-collector.gemfile
+ ruby_3-4-1__dry-monitor_ubuntu-latest:
+ name: Ruby 3.4.1 - dry-monitor
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2040,7 +2036,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2050,16 +2046,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &36
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
- ruby_3-3-4__faraday-1_ubuntu-latest:
- name: Ruby 3.3.4 - faraday-1
- needs: ruby_3-3-4_ubuntu-latest
+ ruby_3-4-1__dry-monitor-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - dry-monitor-collector
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2067,7 +2064,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2077,16 +2074,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *36
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
- ruby_3-3-4__faraday-2_ubuntu-latest:
- name: Ruby 3.3.4 - faraday-2
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/dry-monitor-collector.gemfile
+ ruby_3-4-1__excon_ubuntu-latest:
+ name: Ruby 3.4.1 - excon
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2094,7 +2090,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2104,16 +2100,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &37
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
- ruby_3-3-4__grape_ubuntu-latest:
- name: Ruby 3.3.4 - grape
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/excon.gemfile
+ ruby_3-4-1__excon-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - excon-collector
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2121,7 +2118,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2131,16 +2128,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *37
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/grape.gemfile
- ruby_3-3-4__hanami-2-0_ubuntu-latest:
- name: Ruby 3.3.4 - hanami-2.0
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/excon-collector.gemfile
+ ruby_3-4-1__faraday-1_ubuntu-latest:
+ name: Ruby 3.4.1 - faraday-1
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2148,7 +2144,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2158,16 +2154,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &38
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.0.gemfile
- ruby_3-3-4__hanami-2-1_ubuntu-latest:
- name: Ruby 3.3.4 - hanami-2.1
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
+ ruby_3-4-1__faraday-1-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - faraday-1-collector
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2175,7 +2172,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2185,16 +2182,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *38
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.1.gemfile
- ruby_3-3-4__hanami-2-2_ubuntu-latest:
- name: Ruby 3.3.4 - hanami-2.2
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/faraday-1-collector.gemfile
+ ruby_3-4-1__faraday-2_ubuntu-latest:
+ name: Ruby 3.4.1 - faraday-2
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2202,7 +2198,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2212,16 +2208,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &39
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.2.gemfile
- ruby_3-3-4__http5_ubuntu-latest:
- name: Ruby 3.3.4 - http5
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
+ ruby_3-4-1__faraday-2-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - faraday-2-collector
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2229,7 +2226,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2239,16 +2236,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *39
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/http5.gemfile
- ruby_3-3-4__http6_ubuntu-latest:
- name: Ruby 3.3.4 - http6
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/faraday-2-collector.gemfile
+ ruby_3-4-1__grape_ubuntu-latest:
+ name: Ruby 3.4.1 - grape
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2256,7 +2252,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2266,16 +2262,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &40
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/http6.gemfile
- ruby_3-3-4__mongo_ubuntu-latest:
- name: Ruby 3.3.4 - mongo
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/grape.gemfile
+ ruby_3-4-1__grape-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - grape-collector
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2283,7 +2280,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2293,16 +2290,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *40
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/mongo.gemfile
- ruby_3-3-4__ownership_ubuntu-latest:
- name: Ruby 3.3.4 - ownership
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/grape-collector.gemfile
+ ruby_3-4-1__hanami-2-0_ubuntu-latest:
+ name: Ruby 3.4.1 - hanami-2.0
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2310,7 +2306,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2320,16 +2316,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &41
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/ownership.gemfile
- ruby_3-3-4__padrino_ubuntu-latest:
- name: Ruby 3.3.4 - padrino
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/hanami-2.0.gemfile
+ ruby_3-4-1__hanami-2-0-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - hanami-2.0-collector
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2337,7 +2334,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2347,16 +2344,43 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - *41
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.0-collector.gemfile
+ ruby_3-4-1__hanami-2-1_ubuntu-latest:
+ name: Ruby 3.4.1 - hanami-2.1
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &42
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/padrino.gemfile
- ruby_3-3-4__psych-3_ubuntu-latest:
- name: Ruby 3.3.4 - psych-3
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/hanami-2.1.gemfile
+ ruby_3-4-1__hanami-2-1-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - hanami-2.1-collector
+ needs: ruby_3-4-1_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2364,7 +2388,4631 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *42
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.1-collector.gemfile
+ ruby_3-4-1__hanami-2-2_ubuntu-latest:
+ name: Ruby 3.4.1 - hanami-2.2
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &43
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.2.gemfile
+ ruby_3-4-1__hanami-2-2-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - hanami-2.2-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *43
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.2-collector.gemfile
+ ruby_3-4-1__http5_ubuntu-latest:
+ name: Ruby 3.4.1 - http5
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &44
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http5.gemfile
+ ruby_3-4-1__http5-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - http5-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *44
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http5-collector.gemfile
+ ruby_3-4-1__http6_ubuntu-latest:
+ name: Ruby 3.4.1 - http6
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &45
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http6.gemfile
+ ruby_3-4-1__http6-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - http6-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *45
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http6-collector.gemfile
+ ruby_3-4-1__mongo_ubuntu-latest:
+ name: Ruby 3.4.1 - mongo
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &46
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/mongo.gemfile
+ ruby_3-4-1__mongo-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - mongo-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *46
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/mongo-collector.gemfile
+ ruby_3-4-1__ownership_ubuntu-latest:
+ name: Ruby 3.4.1 - ownership
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &47
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/ownership.gemfile
+ ruby_3-4-1__ownership-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - ownership-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *47
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/ownership-collector.gemfile
+ ruby_3-4-1__padrino_ubuntu-latest:
+ name: Ruby 3.4.1 - padrino
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &48
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/padrino.gemfile
+ ruby_3-4-1__padrino-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - padrino-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *48
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/padrino-collector.gemfile
+ ruby_3-4-1__psych-3_ubuntu-latest:
+ name: Ruby 3.4.1 - psych-3
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &49
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
+ ruby_3-4-1__psych-3-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - psych-3-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *49
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-3-collector.gemfile
+ ruby_3-4-1__psych-4_ubuntu-latest:
+ name: Ruby 3.4.1 - psych-4
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &50
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
+ ruby_3-4-1__psych-4-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - psych-4-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *50
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-4-collector.gemfile
+ ruby_3-4-1__que-1_ubuntu-latest:
+ name: Ruby 3.4.1 - que-1
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &51
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-1.gemfile
+ ruby_3-4-1__que-1-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - que-1-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *51
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-1-collector.gemfile
+ ruby_3-4-1__que-2_ubuntu-latest:
+ name: Ruby 3.4.1 - que-2
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &52
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-2.gemfile
+ ruby_3-4-1__que-2-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - que-2-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *52
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-2-collector.gemfile
+ ruby_3-4-1__rails-7-0_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-7.0
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &53
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
+ ruby_3-4-1__rails-7-0-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-7.0-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *53
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.0-collector.gemfile
+ ruby_3-4-1__rails-7-1_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-7.1
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &54
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
+ ruby_3-4-1__rails-7-1-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-7.1-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *54
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.1-collector.gemfile
+ ruby_3-4-1__rails-7-2_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-7.2
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &55
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
+ ruby_3-4-1__rails-7-2-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-7.2-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *55
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.2-collector.gemfile
+ ruby_3-4-1__rails-8-0_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-8.0
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &56
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-8.0.gemfile
+ ruby_3-4-1__rails-8-0-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-8.0-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *56
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-8.0-collector.gemfile
+ ruby_3-4-1__rails-8-1_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-8.1
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &57
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-8.1.gemfile
+ ruby_3-4-1__rails-8-1-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - rails-8.1-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *57
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-8.1-collector.gemfile
+ ruby_3-4-1__resque-2_ubuntu-latest:
+ name: Ruby 3.4.1 - resque-2
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &58
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
+ ruby_3-4-1__resque-2-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - resque-2-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *58
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/resque-2-collector.gemfile
+ ruby_3-4-1__resque-3_ubuntu-latest:
+ name: Ruby 3.4.1 - resque-3
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &59
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
+ ruby_3-4-1__resque-3-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - resque-3-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *59
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/resque-3-collector.gemfile
+ ruby_3-4-1__sequel_ubuntu-latest:
+ name: Ruby 3.4.1 - sequel
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &60
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sequel.gemfile
+ ruby_3-4-1__sequel-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - sequel-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *60
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sequel-collector.gemfile
+ ruby_3-4-1__shoryuken-7_ubuntu-latest:
+ name: Ruby 3.4.1 - shoryuken-7
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &61
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/shoryuken-7.gemfile
+ ruby_3-4-1__shoryuken-7-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - shoryuken-7-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *61
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/shoryuken-7-collector.gemfile
+ ruby_3-4-1__sinatra_ubuntu-latest:
+ name: Ruby 3.4.1 - sinatra
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &62
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
+ ruby_3-4-1__sinatra-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - sinatra-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *62
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sinatra-collector.gemfile
+ ruby_3-4-1__webmachine2_ubuntu-latest:
+ name: Ruby 3.4.1 - webmachine2
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &63
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
+ ruby_3-4-1__webmachine2-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - webmachine2-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *63
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/webmachine2-collector.gemfile
+ ruby_3-4-1__redis-4_ubuntu-latest:
+ name: Ruby 3.4.1 - redis-4
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &64
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/redis-4.gemfile
+ ruby_3-4-1__redis-4-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - redis-4-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *64
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/redis-4-collector.gemfile
+ ruby_3-4-1__redis-5_ubuntu-latest:
+ name: Ruby 3.4.1 - redis-5
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &65
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
+ ruby_3-4-1__redis-5-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - redis-5-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *65
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/redis-5-collector.gemfile
+ ruby_3-4-1__sidekiq-7_ubuntu-latest:
+ name: Ruby 3.4.1 - sidekiq-7
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &66
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sidekiq-7.gemfile
+ ruby_3-4-1__sidekiq-7-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - sidekiq-7-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *66
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sidekiq-7-collector.gemfile
+ ruby_3-4-1__sidekiq-8_ubuntu-latest:
+ name: Ruby 3.4.1 - sidekiq-8
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &67
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sidekiq-8.gemfile
+ ruby_3-4-1__sidekiq-8-collector_ubuntu-latest:
+ name: Ruby 3.4.1 - sidekiq-8-collector
+ needs: ruby_3-4-1_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *67
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sidekiq-8-collector.gemfile
+ ruby_3-4-1_macos-14:
+ name: Ruby 3.4.1 (macos-14)
+ needs: validation
+ runs-on: macos-14
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.4.1
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ - name: Run tests without extension
+ run: "./script/bundler_wrapper exec rake test:failure"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
+ ruby_3-3-4_ubuntu-latest:
+ name: Ruby 3.3.4
+ needs: validation
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &68
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ - name: Run tests without extension
+ run: "./script/bundler_wrapper exec rake test:failure"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
+ ruby_3-3-4__no_dependencies-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - no_dependencies-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *68
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/no_dependencies-collector.gemfile
+ ruby_3-3-4__capistrano2_ubuntu-latest:
+ name: Ruby 3.3.4 - capistrano2
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &69
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
+ ruby_3-3-4__capistrano2-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - capistrano2-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *69
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/capistrano2-collector.gemfile
+ ruby_3-3-4__capistrano3_ubuntu-latest:
+ name: Ruby 3.3.4 - capistrano3
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &70
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
+ ruby_3-3-4__capistrano3-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - capistrano3-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *70
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/capistrano3-collector.gemfile
+ ruby_3-3-4__code_ownership_ubuntu-latest:
+ name: Ruby 3.3.4 - code_ownership
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &71
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/code_ownership.gemfile
+ ruby_3-3-4__code_ownership-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - code_ownership-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *71
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/code_ownership-collector.gemfile
+ ruby_3-3-4__dry-monitor_ubuntu-latest:
+ name: Ruby 3.3.4 - dry-monitor
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &72
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
+ ruby_3-3-4__dry-monitor-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - dry-monitor-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *72
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/dry-monitor-collector.gemfile
+ ruby_3-3-4__excon_ubuntu-latest:
+ name: Ruby 3.3.4 - excon
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &73
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/excon.gemfile
+ ruby_3-3-4__excon-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - excon-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *73
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/excon-collector.gemfile
+ ruby_3-3-4__faraday-1_ubuntu-latest:
+ name: Ruby 3.3.4 - faraday-1
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &74
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
+ ruby_3-3-4__faraday-1-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - faraday-1-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *74
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/faraday-1-collector.gemfile
+ ruby_3-3-4__faraday-2_ubuntu-latest:
+ name: Ruby 3.3.4 - faraday-2
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &75
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
+ ruby_3-3-4__faraday-2-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - faraday-2-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *75
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/faraday-2-collector.gemfile
+ ruby_3-3-4__grape_ubuntu-latest:
+ name: Ruby 3.3.4 - grape
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &76
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/grape.gemfile
+ ruby_3-3-4__grape-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - grape-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *76
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/grape-collector.gemfile
+ ruby_3-3-4__hanami-2-0_ubuntu-latest:
+ name: Ruby 3.3.4 - hanami-2.0
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &77
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.0.gemfile
+ ruby_3-3-4__hanami-2-0-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - hanami-2.0-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *77
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.0-collector.gemfile
+ ruby_3-3-4__hanami-2-1_ubuntu-latest:
+ name: Ruby 3.3.4 - hanami-2.1
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &78
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.1.gemfile
+ ruby_3-3-4__hanami-2-1-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - hanami-2.1-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *78
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.1-collector.gemfile
+ ruby_3-3-4__hanami-2-2_ubuntu-latest:
+ name: Ruby 3.3.4 - hanami-2.2
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &79
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.2.gemfile
+ ruby_3-3-4__hanami-2-2-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - hanami-2.2-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *79
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.2-collector.gemfile
+ ruby_3-3-4__http5_ubuntu-latest:
+ name: Ruby 3.3.4 - http5
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &80
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http5.gemfile
+ ruby_3-3-4__http5-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - http5-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *80
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http5-collector.gemfile
+ ruby_3-3-4__http6_ubuntu-latest:
+ name: Ruby 3.3.4 - http6
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &81
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http6.gemfile
+ ruby_3-3-4__http6-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - http6-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *81
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http6-collector.gemfile
+ ruby_3-3-4__mongo_ubuntu-latest:
+ name: Ruby 3.3.4 - mongo
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &82
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/mongo.gemfile
+ ruby_3-3-4__mongo-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - mongo-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *82
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/mongo-collector.gemfile
+ ruby_3-3-4__ownership_ubuntu-latest:
+ name: Ruby 3.3.4 - ownership
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &83
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/ownership.gemfile
+ ruby_3-3-4__ownership-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - ownership-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *83
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/ownership-collector.gemfile
+ ruby_3-3-4__padrino_ubuntu-latest:
+ name: Ruby 3.3.4 - padrino
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &84
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/padrino.gemfile
+ ruby_3-3-4__padrino-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - padrino-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *84
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/padrino-collector.gemfile
+ ruby_3-3-4__psych-3_ubuntu-latest:
+ name: Ruby 3.3.4 - psych-3
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &85
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
+ ruby_3-3-4__psych-3-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - psych-3-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *85
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-3-collector.gemfile
+ ruby_3-3-4__psych-4_ubuntu-latest:
+ name: Ruby 3.3.4 - psych-4
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &86
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
+ ruby_3-3-4__psych-4-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - psych-4-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *86
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-4-collector.gemfile
+ ruby_3-3-4__que-1_ubuntu-latest:
+ name: Ruby 3.3.4 - que-1
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &87
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-1.gemfile
+ ruby_3-3-4__que-1-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - que-1-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *87
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-1-collector.gemfile
+ ruby_3-3-4__que-2_ubuntu-latest:
+ name: Ruby 3.3.4 - que-2
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &88
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-2.gemfile
+ ruby_3-3-4__que-2-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - que-2-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *88
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-2-collector.gemfile
+ ruby_3-3-4__rails-6-1_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-6.1
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &89
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-6.1.gemfile
+ ruby_3-3-4__rails-6-1-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-6.1-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *89
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-6.1-collector.gemfile
+ ruby_3-3-4__rails-7-0_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-7.0
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &90
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
+ ruby_3-3-4__rails-7-0-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-7.0-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *90
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.0-collector.gemfile
+ ruby_3-3-4__rails-7-1_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-7.1
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &91
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
+ ruby_3-3-4__rails-7-1-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-7.1-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *91
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.1-collector.gemfile
+ ruby_3-3-4__rails-7-2_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-7.2
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &92
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
+ ruby_3-3-4__rails-7-2-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-7.2-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *92
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.2-collector.gemfile
+ ruby_3-3-4__rails-8-0_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-8.0
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &93
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-8.0.gemfile
+ ruby_3-3-4__rails-8-0-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-8.0-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *93
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-8.0-collector.gemfile
+ ruby_3-3-4__rails-8-1_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-8.1
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &94
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-8.1.gemfile
+ ruby_3-3-4__rails-8-1-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - rails-8.1-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *94
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-8.1-collector.gemfile
+ ruby_3-3-4__resque-2_ubuntu-latest:
+ name: Ruby 3.3.4 - resque-2
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &95
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
+ ruby_3-3-4__resque-2-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - resque-2-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *95
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/resque-2-collector.gemfile
+ ruby_3-3-4__resque-3_ubuntu-latest:
+ name: Ruby 3.3.4 - resque-3
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &96
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
+ ruby_3-3-4__resque-3-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - resque-3-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *96
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/resque-3-collector.gemfile
+ ruby_3-3-4__sequel_ubuntu-latest:
+ name: Ruby 3.3.4 - sequel
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &97
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sequel.gemfile
+ ruby_3-3-4__sequel-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - sequel-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *97
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sequel-collector.gemfile
+ ruby_3-3-4__shoryuken-7_ubuntu-latest:
+ name: Ruby 3.3.4 - shoryuken-7
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &98
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/shoryuken-7.gemfile
+ ruby_3-3-4__shoryuken-7-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - shoryuken-7-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *98
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/shoryuken-7-collector.gemfile
+ ruby_3-3-4__sinatra_ubuntu-latest:
+ name: Ruby 3.3.4 - sinatra
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &99
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
+ ruby_3-3-4__sinatra-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - sinatra-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *99
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/sinatra-collector.gemfile
+ ruby_3-3-4__webmachine2_ubuntu-latest:
+ name: Ruby 3.3.4 - webmachine2
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &100
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
+ ruby_3-3-4__webmachine2-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - webmachine2-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *100
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/webmachine2-collector.gemfile
+ ruby_3-3-4__redis-4_ubuntu-latest:
+ name: Ruby 3.3.4 - redis-4
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &101
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/redis-4.gemfile
+ ruby_3-3-4__redis-4-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - redis-4-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *101
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/redis-4-collector.gemfile
+ ruby_3-3-4__redis-5_ubuntu-latest:
+ name: Ruby 3.3.4 - redis-5
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &102
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
+ ruby_3-3-4__redis-5-collector_ubuntu-latest:
+ name: Ruby 3.3.4 - redis-5-collector
+ needs: ruby_3-3-4_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *102
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/redis-5-collector.gemfile
+ ruby_3-3-4_macos-14:
+ name: Ruby 3.3.4 (macos-14)
+ needs: validation
+ runs-on: macos-14
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.3.4
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ - name: Run tests without extension
+ run: "./script/bundler_wrapper exec rake test:failure"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
+ ruby_3-2-5_ubuntu-latest:
+ name: Ruby 3.2.5
+ needs: validation
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &103
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ - name: Run tests without extension
+ run: "./script/bundler_wrapper exec rake test:failure"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
+ ruby_3-2-5__no_dependencies-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - no_dependencies-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *103
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/no_dependencies-collector.gemfile
+ ruby_3-2-5__capistrano2_ubuntu-latest:
+ name: Ruby 3.2.5 - capistrano2
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &104
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
+ ruby_3-2-5__capistrano2-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - capistrano2-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *104
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/capistrano2-collector.gemfile
+ ruby_3-2-5__capistrano3_ubuntu-latest:
+ name: Ruby 3.2.5 - capistrano3
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &105
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
+ ruby_3-2-5__capistrano3-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - capistrano3-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *105
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/capistrano3-collector.gemfile
+ ruby_3-2-5__dry-monitor_ubuntu-latest:
+ name: Ruby 3.2.5 - dry-monitor
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &106
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
+ ruby_3-2-5__dry-monitor-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - dry-monitor-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *106
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/dry-monitor-collector.gemfile
+ ruby_3-2-5__excon_ubuntu-latest:
+ name: Ruby 3.2.5 - excon
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &107
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/excon.gemfile
+ ruby_3-2-5__excon-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - excon-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *107
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/excon-collector.gemfile
+ ruby_3-2-5__faraday-1_ubuntu-latest:
+ name: Ruby 3.2.5 - faraday-1
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &108
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
+ ruby_3-2-5__faraday-1-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - faraday-1-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *108
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/faraday-1-collector.gemfile
+ ruby_3-2-5__faraday-2_ubuntu-latest:
+ name: Ruby 3.2.5 - faraday-2
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &109
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
+ ruby_3-2-5__faraday-2-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - faraday-2-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *109
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/faraday-2-collector.gemfile
+ ruby_3-2-5__grape_ubuntu-latest:
+ name: Ruby 3.2.5 - grape
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &110
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/grape.gemfile
+ ruby_3-2-5__grape-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - grape-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *110
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/grape-collector.gemfile
+ ruby_3-2-5__hanami-2-0_ubuntu-latest:
+ name: Ruby 3.2.5 - hanami-2.0
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &111
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.0.gemfile
+ ruby_3-2-5__hanami-2-0-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - hanami-2.0-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *111
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.0-collector.gemfile
+ ruby_3-2-5__hanami-2-1_ubuntu-latest:
+ name: Ruby 3.2.5 - hanami-2.1
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &112
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.1.gemfile
+ ruby_3-2-5__hanami-2-1-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - hanami-2.1-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *112
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.1-collector.gemfile
+ ruby_3-2-5__hanami-2-2_ubuntu-latest:
+ name: Ruby 3.2.5 - hanami-2.2
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &113
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.2.gemfile
+ ruby_3-2-5__hanami-2-2-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - hanami-2.2-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *113
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/hanami-2.2-collector.gemfile
+ ruby_3-2-5__http5_ubuntu-latest:
+ name: Ruby 3.2.5 - http5
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &114
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http5.gemfile
+ ruby_3-2-5__http5-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - http5-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *114
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http5-collector.gemfile
+ ruby_3-2-5__http6_ubuntu-latest:
+ name: Ruby 3.2.5 - http6
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &115
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http6.gemfile
+ ruby_3-2-5__http6-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - http6-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *115
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/http6-collector.gemfile
+ ruby_3-2-5__mongo_ubuntu-latest:
+ name: Ruby 3.2.5 - mongo
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &116
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/mongo.gemfile
+ ruby_3-2-5__mongo-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - mongo-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *116
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/mongo-collector.gemfile
+ ruby_3-2-5__ownership_ubuntu-latest:
+ name: Ruby 3.2.5 - ownership
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &117
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/ownership.gemfile
+ ruby_3-2-5__ownership-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - ownership-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *117
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/ownership-collector.gemfile
+ ruby_3-2-5__padrino_ubuntu-latest:
+ name: Ruby 3.2.5 - padrino
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &118
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/padrino.gemfile
+ ruby_3-2-5__padrino-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - padrino-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *118
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/padrino-collector.gemfile
+ ruby_3-2-5__psych-3_ubuntu-latest:
+ name: Ruby 3.2.5 - psych-3
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &119
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
+ ruby_3-2-5__psych-3-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - psych-3-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *119
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-3-collector.gemfile
+ ruby_3-2-5__psych-4_ubuntu-latest:
+ name: Ruby 3.2.5 - psych-4
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &120
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
+ ruby_3-2-5__psych-4-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - psych-4-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *120
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/psych-4-collector.gemfile
+ ruby_3-2-5__que-1_ubuntu-latest:
+ name: Ruby 3.2.5 - que-1
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &121
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-1.gemfile
+ ruby_3-2-5__que-1-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - que-1-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *121
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-1-collector.gemfile
+ ruby_3-2-5__que-2_ubuntu-latest:
+ name: Ruby 3.2.5 - que-2
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &122
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-2.gemfile
+ ruby_3-2-5__que-2-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - que-2-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *122
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/que-2-collector.gemfile
+ ruby_3-2-5__rails-6-1_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-6.1
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &123
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-6.1.gemfile
+ ruby_3-2-5__rails-6-1-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-6.1-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *123
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-6.1-collector.gemfile
+ ruby_3-2-5__rails-7-0_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-7.0
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &124
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
+ ruby_3-2-5__rails-7-0-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-7.0-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *124
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.0-collector.gemfile
+ ruby_3-2-5__rails-7-1_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-7.1
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &125
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
+ ruby_3-2-5__rails-7-1-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-7.1-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *125
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.1-collector.gemfile
+ ruby_3-2-5__rails-7-2_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-7.2
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - &126
+ name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
+ ruby_3-2-5__rails-7-2-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-7.2-collector
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - *126
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/rails-7.2-collector.gemfile
+ ruby_3-2-5__rails-8-0_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-8.0
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2374,16 +7022,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &127
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
- ruby_3-3-4__psych-4_ubuntu-latest:
- name: Ruby 3.3.4 - psych-4
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-8.0.gemfile
+ ruby_3-2-5__rails-8-0-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-8.0-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2391,7 +7040,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2401,16 +7050,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *127
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
- ruby_3-3-4__que-1_ubuntu-latest:
- name: Ruby 3.3.4 - que-1
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-8.0-collector.gemfile
+ ruby_3-2-5__rails-8-1_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-8.1
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2418,7 +7066,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2428,16 +7076,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &128
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-1.gemfile
- ruby_3-3-4__que-2_ubuntu-latest:
- name: Ruby 3.3.4 - que-2
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-8.1.gemfile
+ ruby_3-2-5__rails-8-1-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - rails-8.1-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2445,7 +7094,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2455,16 +7104,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *128
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-2.gemfile
- ruby_3-3-4__rails-6-1_ubuntu-latest:
- name: Ruby 3.3.4 - rails-6.1
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/rails-8.1-collector.gemfile
+ ruby_3-2-5__resque-2_ubuntu-latest:
+ name: Ruby 3.2.5 - resque-2
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2472,7 +7120,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2482,16 +7130,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &129
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-6.1.gemfile
- ruby_3-3-4__rails-7-0_ubuntu-latest:
- name: Ruby 3.3.4 - rails-7.0
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
+ ruby_3-2-5__resque-2-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - resque-2-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2499,7 +7148,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2509,16 +7158,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *129
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
- ruby_3-3-4__rails-7-1_ubuntu-latest:
- name: Ruby 3.3.4 - rails-7.1
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/resque-2-collector.gemfile
+ ruby_3-2-5__resque-3_ubuntu-latest:
+ name: Ruby 3.2.5 - resque-3
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2526,7 +7174,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2536,16 +7184,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &130
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
- ruby_3-3-4__rails-7-2_ubuntu-latest:
- name: Ruby 3.3.4 - rails-7.2
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
+ ruby_3-2-5__resque-3-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - resque-3-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2553,7 +7202,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2563,16 +7212,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *130
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
- ruby_3-3-4__rails-8-0_ubuntu-latest:
- name: Ruby 3.3.4 - rails-8.0
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/resque-3-collector.gemfile
+ ruby_3-2-5__sequel_ubuntu-latest:
+ name: Ruby 3.2.5 - sequel
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2580,7 +7228,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2590,16 +7238,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &131
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-8.0.gemfile
- ruby_3-3-4__rails-8-1_ubuntu-latest:
- name: Ruby 3.3.4 - rails-8.1
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sequel.gemfile
+ ruby_3-2-5__sequel-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - sequel-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2607,7 +7256,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2617,16 +7266,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *131
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-8.1.gemfile
- ruby_3-3-4__resque-2_ubuntu-latest:
- name: Ruby 3.3.4 - resque-2
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sequel-collector.gemfile
+ ruby_3-2-5__shoryuken-7_ubuntu-latest:
+ name: Ruby 3.2.5 - shoryuken-7
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2634,7 +7282,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2644,16 +7292,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &132
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
- ruby_3-3-4__resque-3_ubuntu-latest:
- name: Ruby 3.3.4 - resque-3
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/shoryuken-7.gemfile
+ ruby_3-2-5__shoryuken-7-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - shoryuken-7-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2661,7 +7310,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2671,16 +7320,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *132
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
- ruby_3-3-4__sequel_ubuntu-latest:
- name: Ruby 3.3.4 - sequel
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/shoryuken-7-collector.gemfile
+ ruby_3-2-5__sinatra_ubuntu-latest:
+ name: Ruby 3.2.5 - sinatra
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2688,7 +7336,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2698,16 +7346,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &133
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sequel.gemfile
- ruby_3-3-4__shoryuken-7_ubuntu-latest:
- name: Ruby 3.3.4 - shoryuken-7
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
+ ruby_3-2-5__sinatra-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - sinatra-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2715,7 +7364,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2725,16 +7374,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *133
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/shoryuken-7.gemfile
- ruby_3-3-4__sinatra_ubuntu-latest:
- name: Ruby 3.3.4 - sinatra
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/sinatra-collector.gemfile
+ ruby_3-2-5__webmachine2_ubuntu-latest:
+ name: Ruby 3.2.5 - webmachine2
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2742,7 +7390,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2752,16 +7400,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &134
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
- ruby_3-3-4__webmachine2_ubuntu-latest:
- name: Ruby 3.3.4 - webmachine2
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
+ ruby_3-2-5__webmachine2-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - webmachine2-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2769,7 +7418,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2779,16 +7428,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *134
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
- ruby_3-3-4__redis-4_ubuntu-latest:
- name: Ruby 3.3.4 - redis-4
- needs: ruby_3-3-4_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/webmachine2-collector.gemfile
+ ruby_3-2-5__redis-4_ubuntu-latest:
+ name: Ruby 3.2.5 - redis-4
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2796,7 +7444,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2806,16 +7454,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &135
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
BUNDLE_GEMFILE: gemfiles/redis-4.gemfile
- ruby_3-3-4__redis-5_ubuntu-latest:
- name: Ruby 3.3.4 - redis-5
- needs: ruby_3-3-4_ubuntu-latest
+ ruby_3-2-5__redis-4-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - redis-4-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2823,7 +7472,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2833,24 +7482,23 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *135
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
- ruby_3-3-4_macos-14:
- name: Ruby 3.3.4 (macos-14)
- needs: validation
- runs-on: macos-14
+ BUNDLE_GEMFILE: gemfiles/redis-4-collector.gemfile
+ ruby_3-2-5__redis-5_ubuntu-latest:
+ name: Ruby 3.2.5 - redis-5
+ needs: ruby_3-2-5_ubuntu-latest
+ runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.3.4
+ ruby-version: 3.2.5
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2860,18 +7508,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &136
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
- - name: Run tests without extension
- run: "./script/bundler_wrapper exec rake test:failure"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
- ruby_3-2-5_ubuntu-latest:
- name: Ruby 3.2.5
- needs: validation
+ BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
+ ruby_3-2-5__redis-5-collector_ubuntu-latest:
+ name: Ruby 3.2.5 - redis-5-collector
+ needs: ruby_3-2-5_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2889,19 +7536,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
- - name: Run tests without extension
- run: "./script/bundler_wrapper exec rake test:failure"
+ - *136
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
- ruby_3-2-5__capistrano2_ubuntu-latest:
- name: Ruby 3.2.5 - capistrano2
- needs: ruby_3-2-5_ubuntu-latest
- runs-on: ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/redis-5-collector.gemfile
+ ruby_3-2-5_macos-14:
+ name: Ruby 3.2.5 (macos-14)
+ needs: validation
+ runs-on: macos-14
steps:
- name: Check out repository
uses: actions/checkout@v4
@@ -2920,14 +7564,16 @@ jobs:
found'"
- name: Run tests
run: "./script/bundler_wrapper exec rake test"
+ - name: Run tests without extension
+ run: "./script/bundler_wrapper exec rake test:failure"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
- ruby_3-2-5__capistrano3_ubuntu-latest:
- name: Ruby 3.2.5 - capistrano3
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
+ ruby_3-1-6_ubuntu-latest:
+ name: Ruby 3.1.6
+ needs: validation
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2935,7 +7581,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2945,16 +7591,19 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &137
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
+ - name: Run tests without extension
+ run: "./script/bundler_wrapper exec rake test:failure"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
- ruby_3-2-5__dry-monitor_ubuntu-latest:
- name: Ruby 3.2.5 - dry-monitor
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
+ ruby_3-1-6__no_dependencies-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - no_dependencies-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2962,7 +7611,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2972,16 +7621,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *137
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
- ruby_3-2-5__faraday-1_ubuntu-latest:
- name: Ruby 3.2.5 - faraday-1
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/no_dependencies-collector.gemfile
+ ruby_3-1-6__capistrano2_ubuntu-latest:
+ name: Ruby 3.1.6 - capistrano2
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -2989,7 +7637,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -2999,16 +7647,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &138
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
- ruby_3-2-5__faraday-2_ubuntu-latest:
- name: Ruby 3.2.5 - faraday-2
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
+ ruby_3-1-6__capistrano2-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - capistrano2-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3016,7 +7665,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3026,16 +7675,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *138
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
- ruby_3-2-5__grape_ubuntu-latest:
- name: Ruby 3.2.5 - grape
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/capistrano2-collector.gemfile
+ ruby_3-1-6__capistrano3_ubuntu-latest:
+ name: Ruby 3.1.6 - capistrano3
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3043,7 +7691,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3053,16 +7701,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &139
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/grape.gemfile
- ruby_3-2-5__hanami-2-0_ubuntu-latest:
- name: Ruby 3.2.5 - hanami-2.0
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
+ ruby_3-1-6__capistrano3-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - capistrano3-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3070,7 +7719,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3080,16 +7729,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *139
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.0.gemfile
- ruby_3-2-5__hanami-2-1_ubuntu-latest:
- name: Ruby 3.2.5 - hanami-2.1
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/capistrano3-collector.gemfile
+ ruby_3-1-6__dry-monitor_ubuntu-latest:
+ name: Ruby 3.1.6 - dry-monitor
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3097,7 +7745,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3107,16 +7755,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &140
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.1.gemfile
- ruby_3-2-5__hanami-2-2_ubuntu-latest:
- name: Ruby 3.2.5 - hanami-2.2
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
+ ruby_3-1-6__dry-monitor-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - dry-monitor-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3124,7 +7773,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3134,16 +7783,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *140
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.2.gemfile
- ruby_3-2-5__http5_ubuntu-latest:
- name: Ruby 3.2.5 - http5
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/dry-monitor-collector.gemfile
+ ruby_3-1-6__excon_ubuntu-latest:
+ name: Ruby 3.1.6 - excon
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3151,7 +7799,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3161,16 +7809,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &141
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/http5.gemfile
- ruby_3-2-5__http6_ubuntu-latest:
- name: Ruby 3.2.5 - http6
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/excon.gemfile
+ ruby_3-1-6__excon-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - excon-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3178,7 +7827,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3188,16 +7837,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *141
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/http6.gemfile
- ruby_3-2-5__mongo_ubuntu-latest:
- name: Ruby 3.2.5 - mongo
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/excon-collector.gemfile
+ ruby_3-1-6__faraday-1_ubuntu-latest:
+ name: Ruby 3.1.6 - faraday-1
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3205,7 +7853,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3215,16 +7863,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &142
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/mongo.gemfile
- ruby_3-2-5__ownership_ubuntu-latest:
- name: Ruby 3.2.5 - ownership
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
+ ruby_3-1-6__faraday-1-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - faraday-1-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3232,7 +7881,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3242,16 +7891,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *142
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/ownership.gemfile
- ruby_3-2-5__padrino_ubuntu-latest:
- name: Ruby 3.2.5 - padrino
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/faraday-1-collector.gemfile
+ ruby_3-1-6__faraday-2_ubuntu-latest:
+ name: Ruby 3.1.6 - faraday-2
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3259,7 +7907,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3269,16 +7917,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &143
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/padrino.gemfile
- ruby_3-2-5__psych-3_ubuntu-latest:
- name: Ruby 3.2.5 - psych-3
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
+ ruby_3-1-6__faraday-2-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - faraday-2-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3286,7 +7935,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3296,16 +7945,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *143
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
- ruby_3-2-5__psych-4_ubuntu-latest:
- name: Ruby 3.2.5 - psych-4
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/faraday-2-collector.gemfile
+ ruby_3-1-6__grape_ubuntu-latest:
+ name: Ruby 3.1.6 - grape
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3313,7 +7961,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3323,16 +7971,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &144
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
- ruby_3-2-5__que-1_ubuntu-latest:
- name: Ruby 3.2.5 - que-1
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/grape.gemfile
+ ruby_3-1-6__grape-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - grape-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3340,7 +7989,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3350,16 +7999,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *144
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-1.gemfile
- ruby_3-2-5__que-2_ubuntu-latest:
- name: Ruby 3.2.5 - que-2
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/grape-collector.gemfile
+ ruby_3-1-6__hanami-2-0_ubuntu-latest:
+ name: Ruby 3.1.6 - hanami-2.0
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3367,7 +8015,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3377,16 +8025,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &145
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-2.gemfile
- ruby_3-2-5__rails-6-1_ubuntu-latest:
- name: Ruby 3.2.5 - rails-6.1
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/hanami-2.0.gemfile
+ ruby_3-1-6__hanami-2-0-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - hanami-2.0-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3394,7 +8043,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3404,16 +8053,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *145
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-6.1.gemfile
- ruby_3-2-5__rails-7-0_ubuntu-latest:
- name: Ruby 3.2.5 - rails-7.0
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/hanami-2.0-collector.gemfile
+ ruby_3-1-6__hanami-2-1_ubuntu-latest:
+ name: Ruby 3.1.6 - hanami-2.1
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3421,7 +8069,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3431,16 +8079,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &146
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
- ruby_3-2-5__rails-7-1_ubuntu-latest:
- name: Ruby 3.2.5 - rails-7.1
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/hanami-2.1.gemfile
+ ruby_3-1-6__hanami-2-1-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - hanami-2.1-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3448,7 +8097,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3458,16 +8107,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *146
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
- ruby_3-2-5__rails-7-2_ubuntu-latest:
- name: Ruby 3.2.5 - rails-7.2
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/hanami-2.1-collector.gemfile
+ ruby_3-1-6__hanami-2-2_ubuntu-latest:
+ name: Ruby 3.1.6 - hanami-2.2
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3475,7 +8123,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3485,16 +8133,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &147
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
- ruby_3-2-5__rails-8-0_ubuntu-latest:
- name: Ruby 3.2.5 - rails-8.0
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/hanami-2.2.gemfile
+ ruby_3-1-6__hanami-2-2-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - hanami-2.2-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3502,7 +8151,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3512,16 +8161,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *147
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-8.0.gemfile
- ruby_3-2-5__rails-8-1_ubuntu-latest:
- name: Ruby 3.2.5 - rails-8.1
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/hanami-2.2-collector.gemfile
+ ruby_3-1-6__http5_ubuntu-latest:
+ name: Ruby 3.1.6 - http5
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3529,7 +8177,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3539,16 +8187,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &148
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-8.1.gemfile
- ruby_3-2-5__resque-2_ubuntu-latest:
- name: Ruby 3.2.5 - resque-2
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/http5.gemfile
+ ruby_3-1-6__http5-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - http5-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3556,7 +8205,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3566,16 +8215,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *148
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
- ruby_3-2-5__resque-3_ubuntu-latest:
- name: Ruby 3.2.5 - resque-3
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/http5-collector.gemfile
+ ruby_3-1-6__mongo_ubuntu-latest:
+ name: Ruby 3.1.6 - mongo
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3583,7 +8231,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3593,16 +8241,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &149
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
- ruby_3-2-5__sequel_ubuntu-latest:
- name: Ruby 3.2.5 - sequel
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/mongo.gemfile
+ ruby_3-1-6__mongo-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - mongo-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3610,7 +8259,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3620,16 +8269,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *149
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sequel.gemfile
- ruby_3-2-5__shoryuken-7_ubuntu-latest:
- name: Ruby 3.2.5 - shoryuken-7
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/mongo-collector.gemfile
+ ruby_3-1-6__ownership_ubuntu-latest:
+ name: Ruby 3.1.6 - ownership
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3637,7 +8285,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3647,16 +8295,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &150
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/shoryuken-7.gemfile
- ruby_3-2-5__sinatra_ubuntu-latest:
- name: Ruby 3.2.5 - sinatra
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/ownership.gemfile
+ ruby_3-1-6__ownership-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - ownership-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3664,7 +8313,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3674,16 +8323,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *150
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
- ruby_3-2-5__webmachine2_ubuntu-latest:
- name: Ruby 3.2.5 - webmachine2
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/ownership-collector.gemfile
+ ruby_3-1-6__padrino_ubuntu-latest:
+ name: Ruby 3.1.6 - padrino
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3691,7 +8339,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3701,16 +8349,17 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &151
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
- ruby_3-2-5__redis-4_ubuntu-latest:
- name: Ruby 3.2.5 - redis-4
- needs: ruby_3-2-5_ubuntu-latest
+ BUNDLE_GEMFILE: gemfiles/padrino.gemfile
+ ruby_3-1-6__padrino-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - padrino-collector
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3718,7 +8367,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3728,16 +8377,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *151
env:
- RAILS_ENV: test
- JRUBY_OPTS: ''
- COV: '1'
- BUNDLE_GEMFILE: gemfiles/redis-4.gemfile
- ruby_3-2-5__redis-5_ubuntu-latest:
- name: Ruby 3.2.5 - redis-5
- needs: ruby_3-2-5_ubuntu-latest
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/padrino-collector.gemfile
+ ruby_3-1-6__psych-3_ubuntu-latest:
+ name: Ruby 3.1.6 - psych-3
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3745,7 +8393,7 @@ jobs:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3755,24 +8403,25 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &152
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
- ruby_3-2-5_macos-14:
- name: Ruby 3.2.5 (macos-14)
- needs: validation
- runs-on: macos-14
+ BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
+ ruby_3-1-6__psych-3-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - psych-3-collector
+ needs: ruby_3-1-6_ubuntu-latest
+ runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: 3.2.5
+ ruby-version: 3.1.6
bundler-cache: true
- name: Install gem extension
run: "./script/bundler_wrapper exec rake extension:install"
@@ -3782,18 +8431,15 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
- - name: Run tests without extension
- run: "./script/bundler_wrapper exec rake test:failure"
+ - *152
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
- ruby_3-1-6_ubuntu-latest:
- name: Ruby 3.1.6
- needs: validation
+ BUNDLE_GEMFILE: gemfiles/psych-3-collector.gemfile
+ ruby_3-1-6__psych-4_ubuntu-latest:
+ name: Ruby 3.1.6 - psych-4
+ needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Check out repository
@@ -3811,17 +8457,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &153
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
- - name: Run tests without extension
- run: "./script/bundler_wrapper exec rake test:failure"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/no_dependencies.gemfile
- ruby_3-1-6__capistrano2_ubuntu-latest:
- name: Ruby 3.1.6 - capistrano2
+ BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
+ ruby_3-1-6__psych-4-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - psych-4-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -3840,15 +8485,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *153
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano2.gemfile
- ruby_3-1-6__capistrano3_ubuntu-latest:
- name: Ruby 3.1.6 - capistrano3
+ BUNDLE_GEMFILE: gemfiles/psych-4-collector.gemfile
+ ruby_3-1-6__que-1_ubuntu-latest:
+ name: Ruby 3.1.6 - que-1
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -3867,15 +8511,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &154
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
- ruby_3-1-6__dry-monitor_ubuntu-latest:
- name: Ruby 3.1.6 - dry-monitor
+ BUNDLE_GEMFILE: gemfiles/que-1.gemfile
+ ruby_3-1-6__que-1-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - que-1-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -3894,15 +8539,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *154
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
- ruby_3-1-6__faraday-1_ubuntu-latest:
- name: Ruby 3.1.6 - faraday-1
+ BUNDLE_GEMFILE: gemfiles/que-1-collector.gemfile
+ ruby_3-1-6__que-2_ubuntu-latest:
+ name: Ruby 3.1.6 - que-2
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -3921,15 +8565,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &155
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-1.gemfile
- ruby_3-1-6__faraday-2_ubuntu-latest:
- name: Ruby 3.1.6 - faraday-2
+ BUNDLE_GEMFILE: gemfiles/que-2.gemfile
+ ruby_3-1-6__que-2-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - que-2-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -3948,15 +8593,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *155
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/faraday-2.gemfile
- ruby_3-1-6__grape_ubuntu-latest:
- name: Ruby 3.1.6 - grape
+ BUNDLE_GEMFILE: gemfiles/que-2-collector.gemfile
+ ruby_3-1-6__rails-6-1_ubuntu-latest:
+ name: Ruby 3.1.6 - rails-6.1
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -3975,15 +8619,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &156
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/grape.gemfile
- ruby_3-1-6__hanami-2-0_ubuntu-latest:
- name: Ruby 3.1.6 - hanami-2.0
+ BUNDLE_GEMFILE: gemfiles/rails-6.1.gemfile
+ ruby_3-1-6__rails-6-1-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - rails-6.1-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4002,15 +8647,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *156
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.0.gemfile
- ruby_3-1-6__hanami-2-1_ubuntu-latest:
- name: Ruby 3.1.6 - hanami-2.1
+ BUNDLE_GEMFILE: gemfiles/rails-6.1-collector.gemfile
+ ruby_3-1-6__rails-7-0_ubuntu-latest:
+ name: Ruby 3.1.6 - rails-7.0
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4029,15 +8673,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &157
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.1.gemfile
- ruby_3-1-6__hanami-2-2_ubuntu-latest:
- name: Ruby 3.1.6 - hanami-2.2
+ BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
+ ruby_3-1-6__rails-7-0-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - rails-7.0-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4056,15 +8701,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *157
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/hanami-2.2.gemfile
- ruby_3-1-6__http5_ubuntu-latest:
- name: Ruby 3.1.6 - http5
+ BUNDLE_GEMFILE: gemfiles/rails-7.0-collector.gemfile
+ ruby_3-1-6__rails-7-1_ubuntu-latest:
+ name: Ruby 3.1.6 - rails-7.1
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4083,15 +8727,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &158
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/http5.gemfile
- ruby_3-1-6__mongo_ubuntu-latest:
- name: Ruby 3.1.6 - mongo
+ BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
+ ruby_3-1-6__rails-7-1-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - rails-7.1-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4110,15 +8755,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *158
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/mongo.gemfile
- ruby_3-1-6__ownership_ubuntu-latest:
- name: Ruby 3.1.6 - ownership
+ BUNDLE_GEMFILE: gemfiles/rails-7.1-collector.gemfile
+ ruby_3-1-6__rails-7-2_ubuntu-latest:
+ name: Ruby 3.1.6 - rails-7.2
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4137,15 +8781,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &159
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/ownership.gemfile
- ruby_3-1-6__padrino_ubuntu-latest:
- name: Ruby 3.1.6 - padrino
+ BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
+ ruby_3-1-6__rails-7-2-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - rails-7.2-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4164,15 +8809,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *159
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/padrino.gemfile
- ruby_3-1-6__psych-3_ubuntu-latest:
- name: Ruby 3.1.6 - psych-3
+ BUNDLE_GEMFILE: gemfiles/rails-7.2-collector.gemfile
+ ruby_3-1-6__resque-2_ubuntu-latest:
+ name: Ruby 3.1.6 - resque-2
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4191,15 +8835,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &160
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-3.gemfile
- ruby_3-1-6__psych-4_ubuntu-latest:
- name: Ruby 3.1.6 - psych-4
+ BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
+ ruby_3-1-6__resque-2-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - resque-2-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4218,15 +8863,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *160
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/psych-4.gemfile
- ruby_3-1-6__que-1_ubuntu-latest:
- name: Ruby 3.1.6 - que-1
+ BUNDLE_GEMFILE: gemfiles/resque-2-collector.gemfile
+ ruby_3-1-6__resque-3_ubuntu-latest:
+ name: Ruby 3.1.6 - resque-3
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4245,15 +8889,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &161
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-1.gemfile
- ruby_3-1-6__que-2_ubuntu-latest:
- name: Ruby 3.1.6 - que-2
+ BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
+ ruby_3-1-6__resque-3-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - resque-3-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4272,15 +8917,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *161
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/que-2.gemfile
- ruby_3-1-6__rails-6-1_ubuntu-latest:
- name: Ruby 3.1.6 - rails-6.1
+ BUNDLE_GEMFILE: gemfiles/resque-3-collector.gemfile
+ ruby_3-1-6__sequel_ubuntu-latest:
+ name: Ruby 3.1.6 - sequel
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4299,15 +8943,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &162
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-6.1.gemfile
- ruby_3-1-6__rails-7-0_ubuntu-latest:
- name: Ruby 3.1.6 - rails-7.0
+ BUNDLE_GEMFILE: gemfiles/sequel.gemfile
+ ruby_3-1-6__sequel-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - sequel-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4326,15 +8971,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *162
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.0.gemfile
- ruby_3-1-6__rails-7-1_ubuntu-latest:
- name: Ruby 3.1.6 - rails-7.1
+ BUNDLE_GEMFILE: gemfiles/sequel-collector.gemfile
+ ruby_3-1-6__shoryuken-6_ubuntu-latest:
+ name: Ruby 3.1.6 - shoryuken-6
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4353,15 +8997,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &163
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.1.gemfile
- ruby_3-1-6__rails-7-2_ubuntu-latest:
- name: Ruby 3.1.6 - rails-7.2
+ BUNDLE_GEMFILE: gemfiles/shoryuken-6.gemfile
+ ruby_3-1-6__shoryuken-6-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - shoryuken-6-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4380,15 +9025,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *163
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/rails-7.2.gemfile
- ruby_3-1-6__resque-2_ubuntu-latest:
- name: Ruby 3.1.6 - resque-2
+ BUNDLE_GEMFILE: gemfiles/shoryuken-6-collector.gemfile
+ ruby_3-1-6__sinatra_ubuntu-latest:
+ name: Ruby 3.1.6 - sinatra
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4407,15 +9051,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &164
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-2.gemfile
- ruby_3-1-6__resque-3_ubuntu-latest:
- name: Ruby 3.1.6 - resque-3
+ BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
+ ruby_3-1-6__sinatra-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - sinatra-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4434,15 +9079,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *164
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/resque-3.gemfile
- ruby_3-1-6__sequel_ubuntu-latest:
- name: Ruby 3.1.6 - sequel
+ BUNDLE_GEMFILE: gemfiles/sinatra-collector.gemfile
+ ruby_3-1-6__webmachine2_ubuntu-latest:
+ name: Ruby 3.1.6 - webmachine2
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4461,15 +9105,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &165
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sequel.gemfile
- ruby_3-1-6__shoryuken-6_ubuntu-latest:
- name: Ruby 3.1.6 - shoryuken-6
+ BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
+ ruby_3-1-6__webmachine2-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - webmachine2-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4488,15 +9133,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *165
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/shoryuken-6.gemfile
- ruby_3-1-6__sinatra_ubuntu-latest:
- name: Ruby 3.1.6 - sinatra
+ BUNDLE_GEMFILE: gemfiles/webmachine2-collector.gemfile
+ ruby_3-1-6__redis-4_ubuntu-latest:
+ name: Ruby 3.1.6 - redis-4
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4515,15 +9159,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &166
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/sinatra.gemfile
- ruby_3-1-6__webmachine2_ubuntu-latest:
- name: Ruby 3.1.6 - webmachine2
+ BUNDLE_GEMFILE: gemfiles/redis-4.gemfile
+ ruby_3-1-6__redis-4-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - redis-4-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4542,15 +9187,14 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *166
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/webmachine2.gemfile
- ruby_3-1-6__redis-4_ubuntu-latest:
- name: Ruby 3.1.6 - redis-4
+ BUNDLE_GEMFILE: gemfiles/redis-4-collector.gemfile
+ ruby_3-1-6__redis-5_ubuntu-latest:
+ name: Ruby 3.1.6 - redis-5
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4569,15 +9213,16 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
+ - &167
+ name: Run tests
run: "./script/bundler_wrapper exec rake test"
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/redis-4.gemfile
- ruby_3-1-6__redis-5_ubuntu-latest:
- name: Ruby 3.1.6 - redis-5
+ BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
+ ruby_3-1-6__redis-5-collector_ubuntu-latest:
+ name: Ruby 3.1.6 - redis-5-collector
needs: ruby_3-1-6_ubuntu-latest
runs-on: ubuntu-latest
steps:
@@ -4596,13 +9241,12 @@ jobs:
- name: Print Makefile log file
run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
found'"
- - name: Run tests
- run: "./script/bundler_wrapper exec rake test"
+ - *167
env:
RAILS_ENV: test
JRUBY_OPTS: ''
COV: '1'
- BUNDLE_GEMFILE: gemfiles/redis-5.gemfile
+ BUNDLE_GEMFILE: gemfiles/redis-5-collector.gemfile
ruby_3-1-6_macos-14:
name: Ruby 3.1.6 (macos-14)
needs: validation
@@ -4742,6 +9386,33 @@ jobs:
JRUBY_OPTS: ''
COV: '1'
BUNDLE_GEMFILE: gemfiles/dry-monitor.gemfile
+ ruby_3-0-7__excon_ubuntu-latest:
+ name: Ruby 3.0.7 - excon
+ needs: ruby_3-0-7_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 3.0.7
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/excon.gemfile
ruby_3-0-7__faraday-1_ubuntu-latest:
name: Ruby 3.0.7 - faraday-1
needs: ruby_3-0-7_ubuntu-latest
@@ -5529,6 +10200,33 @@ jobs:
JRUBY_OPTS: ''
COV: '1'
BUNDLE_GEMFILE: gemfiles/capistrano3.gemfile
+ ruby_2-7-8__excon_ubuntu-latest:
+ name: Ruby 2.7.8 - excon
+ needs: ruby_2-7-8_ubuntu-latest
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: Install Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: 2.7.8
+ bundler-cache: true
+ - name: Install gem extension
+ run: "./script/bundler_wrapper exec rake extension:install"
+ - name: Print extension install report
+ run: "[ -e ext/install.report ] && cat ext/install.report || echo 'No ext/install.report
+ file found'"
+ - name: Print Makefile log file
+ run: "[ -f ext/mkmf.log ] && cat ext/mkmf.log || echo 'No ext/mkmf.log file
+ found'"
+ - name: Run tests
+ run: "./script/bundler_wrapper exec rake test"
+ env:
+ RAILS_ENV: test
+ JRUBY_OPTS: ''
+ COV: '1'
+ BUNDLE_GEMFILE: gemfiles/excon.gemfile
ruby_2-7-8__faraday-1_ubuntu-latest:
name: Ruby 2.7.8 - faraday-1
needs: ruby_2-7-8_ubuntu-latest
diff --git a/Rakefile b/Rakefile
index 021e7cf76..66c53f814 100644
--- a/Rakefile
+++ b/Rakefile
@@ -72,7 +72,50 @@ GITHUB_ACTION_WORKFLOW_FILE = ".github/workflows/ci.yml"
PRIMARY_JOB_GEMSET = "no_dependencies"
DEFAULT_RUNS_ON = "ubuntu-latest"
+COLLECTOR_GEMFILE_PARTIAL = "collector.rb"
+
namespace :build_matrix do
+ namespace :gemfiles do
+ # Generates a `-collector.gemfile` next to each base gemfile. The
+ # variant layers the optional OpenTelemetry gems (`gemfiles/collector.rb`)
+ # on top of the base, so collector-mode test runs resolve them while the
+ # base gemfiles (and the gemspec) stay OpenTelemetry-free. Regenerate
+ # whenever a base gemfile is added or removed.
+ task :generate do
+ base_gemfiles =
+ Dir["gemfiles/*.gemfile"]
+ .map { |path| File.basename(path) }
+ .reject { |name| name.end_with?("-collector.gemfile") }
+ .sort
+
+ base_gemfiles.each do |base|
+ name = base.sub(/\.gemfile\z/, "")
+ contents =
+ "# DO NOT EDIT\n" \
+ "# This is a generated file by the " \
+ "`rake build_matrix:gemfiles:generate` task.\n" \
+ "# It layers the optional OpenTelemetry gems (gemfiles/" \
+ "#{COLLECTOR_GEMFILE_PARTIAL}) on top of #{base}.\n" \
+ "\n" \
+ "eval_gemfile File.expand_path(#{base.inspect}, __dir__)\n" \
+ "eval_gemfile File.expand_path(" \
+ "#{COLLECTOR_GEMFILE_PARTIAL.inspect}, __dir__)\n"
+ File.write("gemfiles/#{name}-collector.gemfile", contents)
+ end
+
+ puts "Generated #{base_gemfiles.count} `-collector` gemfiles."
+ end
+
+ task :validate => :generate do
+ output = `git status --porcelain gemfiles`
+ if output.include?("-collector.gemfile")
+ puts "The `-collector` gemfiles are out of date. The changes were not committed."
+ puts "Please run `rake build_matrix:gemfiles:generate` and commit the changes."
+ exit 1
+ end
+ end
+ end
+
namespace :github do
task :generate do
yaml = YAML.load_file("build_matrix.yml")
@@ -113,6 +156,21 @@ namespace :build_matrix do
job["steps"] << test_step
builds[build_matrix_key(ruby["ruby"], :ruby_gem => ruby_gem["gem"])] = job
end
+
+ # On collector-capable Rubies, additionally run the gem's
+ # `-collector` gemfile (base gems + optional OpenTelemetry gems) so
+ # collector-mode specs are exercised. These always depend on the
+ # primary job for the Ruby version and run on Ubuntu only.
+ next unless collector_ruby?(matrix, ruby_version)
+
+ collector_gem = "#{ruby_gem["gem"]}-collector"
+ collector_job = build_job(ruby_version, :ruby_gem => collector_gem)
+ collector_job["env"] = matrix["env"]
+ .merge("BUNDLE_GEMFILE" => "gemfiles/#{collector_gem}.gemfile")
+ collector_job["needs"] = build_matrix_key(ruby["ruby"])
+ collector_job["steps"] << test_step
+ builds[build_matrix_key(ruby["ruby"], :ruby_gem => collector_gem)] =
+ collector_job
end
# Add build for macOS
@@ -191,6 +249,14 @@ namespace :build_matrix do
out << "#{bundler_version} #{gemfile_env} ./script/bundler_wrapper install --quiet || { echo 'Bundling failed'; exit 1; }"
out << "echo 'Running #{gemfile} in #{ruby_version}'"
out << "#{bundler_version} #{gemfile_env} ./script/bundler_wrapper exec rspec || { echo 'Running specs failed'; exit 1; }"
+
+ next unless collector_ruby?(matrix, ruby_version)
+
+ collector_env = "env BUNDLE_GEMFILE=gemfiles/#{gemfile}-collector.gemfile"
+ out << "echo 'Bundling #{gemfile}-collector in #{ruby_version}'"
+ out << "#{bundler_version} #{collector_env} ./script/bundler_wrapper install --quiet || { echo 'Bundling failed'; exit 1; }"
+ out << "echo 'Running #{gemfile}-collector in #{ruby_version}'"
+ out << "#{bundler_version} #{collector_env} ./script/bundler_wrapper exec rspec || { echo 'Running specs failed'; exit 1; }"
end
# rubocop:enable Layout/LineLength
out << ""
@@ -207,6 +273,10 @@ namespace :build_matrix do
end
end
+ def collector_ruby?(matrix, ruby_version)
+ Array(matrix.dig("collector", "ruby")).include?(ruby_version)
+ end
+
def gemset_for_ruby(ruby, matrix)
gems = matrix["gems"]
if ruby["gems"]
diff --git a/appsignal.gemspec b/appsignal.gemspec
index 087b13c7d..07969a87f 100644
--- a/appsignal.gemspec
+++ b/appsignal.gemspec
@@ -60,6 +60,13 @@ Gem::Specification.new do |gem|
# Needs 2.0+ because we rely on Rack::Events
gem.add_dependency "rack", ">= 2.0.0"
+ # The OpenTelemetry SDK and OTLP exporters are *optional* and intentionally
+ # not declared here: they're only needed in collector mode (Ruby 3.1+), so
+ # bundling them would break Ruby 2.7 and burden non-collector users. Apps
+ # that enable collector mode add them to their own Gemfile; the minimum
+ # versions live in `lib/appsignal/opentelemetry/dependencies.rb` and are
+ # enforced at boot by `Appsignal::OpenTelemetry.configure`.
+
gem.add_development_dependency "pry"
gem.add_development_dependency "rake", ">= 12"
gem.add_development_dependency "rspec", "~> 3.8"
diff --git a/build_matrix.yml b/build_matrix.yml
index 6b4637b42..dc383a8e0 100644
--- a/build_matrix.yml
+++ b/build_matrix.yml
@@ -107,6 +107,18 @@ matrix:
JRUBY_OPTS: ""
COV: "1"
+ # Ruby versions that additionally run each gem's `-collector` gemfile (the
+ # optional OpenTelemetry gems layered on top). Collector mode requires Ruby
+ # 3.1+ (the OTel metrics SDK's fork hooks rely on `Process._fork`) and does
+ # not support JRuby's forking model, so only these versions are listed.
+ collector:
+ ruby:
+ - "4.0.0"
+ - "3.4.1"
+ - "3.3.4"
+ - "3.2.5"
+ - "3.1.6"
+
gemsets: # By default all gems are tested
none:
- "no_dependencies"
@@ -150,6 +162,7 @@ matrix:
- "3.2.5"
- "3.1.6"
- "3.0.7"
+ - gem: "excon"
- gem: "faraday-1"
- gem: "faraday-2"
only:
@@ -278,6 +291,7 @@ matrix:
- "3.4.1"
- "3.3.4"
- "3.2.5"
+ - gem: "mongo"
- gem: "resque-2"
- gem: "resque-3"
only:
diff --git a/gemfiles/capistrano2-collector.gemfile b/gemfiles/capistrano2-collector.gemfile
new file mode 100644
index 000000000..6c4960552
--- /dev/null
+++ b/gemfiles/capistrano2-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of capistrano2.gemfile.
+
+eval_gemfile File.expand_path("capistrano2.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/capistrano3-collector.gemfile b/gemfiles/capistrano3-collector.gemfile
new file mode 100644
index 000000000..3c87f37ec
--- /dev/null
+++ b/gemfiles/capistrano3-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of capistrano3.gemfile.
+
+eval_gemfile File.expand_path("capistrano3.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/code_ownership-collector.gemfile b/gemfiles/code_ownership-collector.gemfile
new file mode 100644
index 000000000..e3e4add7f
--- /dev/null
+++ b/gemfiles/code_ownership-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of code_ownership.gemfile.
+
+eval_gemfile File.expand_path("code_ownership.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/collector.rb b/gemfiles/collector.rb
new file mode 100644
index 000000000..15be7a64b
--- /dev/null
+++ b/gemfiles/collector.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# Gemfile fragment that adds the optional OpenTelemetry gems collector mode
+# needs. `eval_gemfile`'d by the `*-collector.gemfile` variants on top of their
+# base gemfile. The versions come from the single source of truth shared with
+# the runtime version gate.
+require_relative "../lib/appsignal/opentelemetry/dependencies"
+
+Appsignal::OpenTelemetry::REQUIRED_GEMS.each do |name, minimum_version|
+ gem name, ">= #{minimum_version}"
+end
diff --git a/gemfiles/dry-monitor-collector.gemfile b/gemfiles/dry-monitor-collector.gemfile
new file mode 100644
index 000000000..9bd8b9410
--- /dev/null
+++ b/gemfiles/dry-monitor-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of dry-monitor.gemfile.
+
+eval_gemfile File.expand_path("dry-monitor.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/excon-collector.gemfile b/gemfiles/excon-collector.gemfile
new file mode 100644
index 000000000..d8567df01
--- /dev/null
+++ b/gemfiles/excon-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of excon.gemfile.
+
+eval_gemfile File.expand_path("excon.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/excon.gemfile b/gemfiles/excon.gemfile
new file mode 100644
index 000000000..526081e0b
--- /dev/null
+++ b/gemfiles/excon.gemfile
@@ -0,0 +1,5 @@
+source "https://rubygems.org"
+
+gem "excon"
+
+gemspec :path => "../"
diff --git a/gemfiles/faraday-1-collector.gemfile b/gemfiles/faraday-1-collector.gemfile
new file mode 100644
index 000000000..252188545
--- /dev/null
+++ b/gemfiles/faraday-1-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of faraday-1.gemfile.
+
+eval_gemfile File.expand_path("faraday-1.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/faraday-2-collector.gemfile b/gemfiles/faraday-2-collector.gemfile
new file mode 100644
index 000000000..4af7281ed
--- /dev/null
+++ b/gemfiles/faraday-2-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of faraday-2.gemfile.
+
+eval_gemfile File.expand_path("faraday-2.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/faraday-2.gemfile b/gemfiles/faraday-2.gemfile
index 00a571da9..27a42cbd5 100644
--- a/gemfiles/faraday-2.gemfile
+++ b/gemfiles/faraday-2.gemfile
@@ -1,7 +1,5 @@
source "https://rubygems.org"
-gem "excon"
gem "faraday", "~> 2.0"
-gem "faraday-excon"
gemspec :path => "../"
diff --git a/gemfiles/grape-collector.gemfile b/gemfiles/grape-collector.gemfile
new file mode 100644
index 000000000..84298ef55
--- /dev/null
+++ b/gemfiles/grape-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of grape.gemfile.
+
+eval_gemfile File.expand_path("grape.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/hanami-2.0-collector.gemfile b/gemfiles/hanami-2.0-collector.gemfile
new file mode 100644
index 000000000..61d138330
--- /dev/null
+++ b/gemfiles/hanami-2.0-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of hanami-2.0.gemfile.
+
+eval_gemfile File.expand_path("hanami-2.0.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/hanami-2.1-collector.gemfile b/gemfiles/hanami-2.1-collector.gemfile
new file mode 100644
index 000000000..eae8eadfe
--- /dev/null
+++ b/gemfiles/hanami-2.1-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of hanami-2.1.gemfile.
+
+eval_gemfile File.expand_path("hanami-2.1.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/hanami-2.2-collector.gemfile b/gemfiles/hanami-2.2-collector.gemfile
new file mode 100644
index 000000000..2dbec63ea
--- /dev/null
+++ b/gemfiles/hanami-2.2-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of hanami-2.2.gemfile.
+
+eval_gemfile File.expand_path("hanami-2.2.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/http5-collector.gemfile b/gemfiles/http5-collector.gemfile
new file mode 100644
index 000000000..5e72919bf
--- /dev/null
+++ b/gemfiles/http5-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of http5.gemfile.
+
+eval_gemfile File.expand_path("http5.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/http6-collector.gemfile b/gemfiles/http6-collector.gemfile
new file mode 100644
index 000000000..eda44c063
--- /dev/null
+++ b/gemfiles/http6-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of http6.gemfile.
+
+eval_gemfile File.expand_path("http6.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/mongo-collector.gemfile b/gemfiles/mongo-collector.gemfile
new file mode 100644
index 000000000..b1cbafcf6
--- /dev/null
+++ b/gemfiles/mongo-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of mongo.gemfile.
+
+eval_gemfile File.expand_path("mongo.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/no_dependencies-collector.gemfile b/gemfiles/no_dependencies-collector.gemfile
new file mode 100644
index 000000000..04f8ff2ba
--- /dev/null
+++ b/gemfiles/no_dependencies-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of no_dependencies.gemfile.
+
+eval_gemfile File.expand_path("no_dependencies.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/ownership-collector.gemfile b/gemfiles/ownership-collector.gemfile
new file mode 100644
index 000000000..964401c18
--- /dev/null
+++ b/gemfiles/ownership-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of ownership.gemfile.
+
+eval_gemfile File.expand_path("ownership.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/padrino-collector.gemfile b/gemfiles/padrino-collector.gemfile
new file mode 100644
index 000000000..8c04046d8
--- /dev/null
+++ b/gemfiles/padrino-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of padrino.gemfile.
+
+eval_gemfile File.expand_path("padrino.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/psych-3-collector.gemfile b/gemfiles/psych-3-collector.gemfile
new file mode 100644
index 000000000..570c8704c
--- /dev/null
+++ b/gemfiles/psych-3-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of psych-3.gemfile.
+
+eval_gemfile File.expand_path("psych-3.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/psych-4-collector.gemfile b/gemfiles/psych-4-collector.gemfile
new file mode 100644
index 000000000..55546c9e4
--- /dev/null
+++ b/gemfiles/psych-4-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of psych-4.gemfile.
+
+eval_gemfile File.expand_path("psych-4.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/que-1-collector.gemfile b/gemfiles/que-1-collector.gemfile
new file mode 100644
index 000000000..df75ac571
--- /dev/null
+++ b/gemfiles/que-1-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of que-1.gemfile.
+
+eval_gemfile File.expand_path("que-1.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/que-2-collector.gemfile b/gemfiles/que-2-collector.gemfile
new file mode 100644
index 000000000..ed1b038b1
--- /dev/null
+++ b/gemfiles/que-2-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of que-2.gemfile.
+
+eval_gemfile File.expand_path("que-2.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/rails-6.0-collector.gemfile b/gemfiles/rails-6.0-collector.gemfile
new file mode 100644
index 000000000..5e4a313e9
--- /dev/null
+++ b/gemfiles/rails-6.0-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of rails-6.0.gemfile.
+
+eval_gemfile File.expand_path("rails-6.0.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/rails-6.1-collector.gemfile b/gemfiles/rails-6.1-collector.gemfile
new file mode 100644
index 000000000..b690ee546
--- /dev/null
+++ b/gemfiles/rails-6.1-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of rails-6.1.gemfile.
+
+eval_gemfile File.expand_path("rails-6.1.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/rails-7.0-collector.gemfile b/gemfiles/rails-7.0-collector.gemfile
new file mode 100644
index 000000000..63b692381
--- /dev/null
+++ b/gemfiles/rails-7.0-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of rails-7.0.gemfile.
+
+eval_gemfile File.expand_path("rails-7.0.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/rails-7.1-collector.gemfile b/gemfiles/rails-7.1-collector.gemfile
new file mode 100644
index 000000000..733c2dba5
--- /dev/null
+++ b/gemfiles/rails-7.1-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of rails-7.1.gemfile.
+
+eval_gemfile File.expand_path("rails-7.1.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/rails-7.2-collector.gemfile b/gemfiles/rails-7.2-collector.gemfile
new file mode 100644
index 000000000..458c31cf4
--- /dev/null
+++ b/gemfiles/rails-7.2-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of rails-7.2.gemfile.
+
+eval_gemfile File.expand_path("rails-7.2.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/rails-8.0-collector.gemfile b/gemfiles/rails-8.0-collector.gemfile
new file mode 100644
index 000000000..7ea545dc3
--- /dev/null
+++ b/gemfiles/rails-8.0-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of rails-8.0.gemfile.
+
+eval_gemfile File.expand_path("rails-8.0.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/rails-8.1-collector.gemfile b/gemfiles/rails-8.1-collector.gemfile
new file mode 100644
index 000000000..2d67888a2
--- /dev/null
+++ b/gemfiles/rails-8.1-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of rails-8.1.gemfile.
+
+eval_gemfile File.expand_path("rails-8.1.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/redis-4-collector.gemfile b/gemfiles/redis-4-collector.gemfile
new file mode 100644
index 000000000..2e5d42742
--- /dev/null
+++ b/gemfiles/redis-4-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of redis-4.gemfile.
+
+eval_gemfile File.expand_path("redis-4.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/redis-5-collector.gemfile b/gemfiles/redis-5-collector.gemfile
new file mode 100644
index 000000000..dfeb0015a
--- /dev/null
+++ b/gemfiles/redis-5-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of redis-5.gemfile.
+
+eval_gemfile File.expand_path("redis-5.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/resque-2-collector.gemfile b/gemfiles/resque-2-collector.gemfile
new file mode 100644
index 000000000..49486175a
--- /dev/null
+++ b/gemfiles/resque-2-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of resque-2.gemfile.
+
+eval_gemfile File.expand_path("resque-2.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/resque-3-collector.gemfile b/gemfiles/resque-3-collector.gemfile
new file mode 100644
index 000000000..f88745316
--- /dev/null
+++ b/gemfiles/resque-3-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of resque-3.gemfile.
+
+eval_gemfile File.expand_path("resque-3.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/sequel-collector.gemfile b/gemfiles/sequel-collector.gemfile
new file mode 100644
index 000000000..884a85b96
--- /dev/null
+++ b/gemfiles/sequel-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of sequel.gemfile.
+
+eval_gemfile File.expand_path("sequel.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/shoryuken-6-collector.gemfile b/gemfiles/shoryuken-6-collector.gemfile
new file mode 100644
index 000000000..cfb5bb853
--- /dev/null
+++ b/gemfiles/shoryuken-6-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of shoryuken-6.gemfile.
+
+eval_gemfile File.expand_path("shoryuken-6.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/shoryuken-7-collector.gemfile b/gemfiles/shoryuken-7-collector.gemfile
new file mode 100644
index 000000000..92a40d573
--- /dev/null
+++ b/gemfiles/shoryuken-7-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of shoryuken-7.gemfile.
+
+eval_gemfile File.expand_path("shoryuken-7.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/sidekiq-7-collector.gemfile b/gemfiles/sidekiq-7-collector.gemfile
new file mode 100644
index 000000000..b8971bbcd
--- /dev/null
+++ b/gemfiles/sidekiq-7-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of sidekiq-7.gemfile.
+
+eval_gemfile File.expand_path("sidekiq-7.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/sidekiq-8-collector.gemfile b/gemfiles/sidekiq-8-collector.gemfile
new file mode 100644
index 000000000..26781c4e3
--- /dev/null
+++ b/gemfiles/sidekiq-8-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of sidekiq-8.gemfile.
+
+eval_gemfile File.expand_path("sidekiq-8.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/sinatra-collector.gemfile b/gemfiles/sinatra-collector.gemfile
new file mode 100644
index 000000000..921768d1e
--- /dev/null
+++ b/gemfiles/sinatra-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of sinatra.gemfile.
+
+eval_gemfile File.expand_path("sinatra.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/gemfiles/webmachine2-collector.gemfile b/gemfiles/webmachine2-collector.gemfile
new file mode 100644
index 000000000..6f62919d2
--- /dev/null
+++ b/gemfiles/webmachine2-collector.gemfile
@@ -0,0 +1,6 @@
+# DO NOT EDIT
+# This is a generated file by the `rake build_matrix:gemfiles:generate` task.
+# It layers the optional OpenTelemetry gems (gemfiles/collector.rb) on top of webmachine2.gemfile.
+
+eval_gemfile File.expand_path("webmachine2.gemfile", __dir__)
+eval_gemfile File.expand_path("collector.rb", __dir__)
diff --git a/lib/appsignal.rb b/lib/appsignal.rb
index 754182108..75c6ef40a 100644
--- a/lib/appsignal.rb
+++ b/lib/appsignal.rb
@@ -8,6 +8,8 @@
require "appsignal/utils/stdout_and_logger_message"
require "appsignal/helpers/instrumentation"
require "appsignal/helpers/metrics"
+require "appsignal/backends"
+require "appsignal/opentelemetry"
# AppSignal for Ruby gem's main module.
#
@@ -143,6 +145,8 @@ def start
Appsignal::Hooks.load_hooks
Appsignal::Loaders.start
+ Appsignal::OpenTelemetry.configure(config) if config.collector_mode_configured?
+
if config[:enable_allocation_tracking] && !Appsignal::System.jruby?
Appsignal::Extension.install_allocation_event_hook
Appsignal::Environment.report_enabled("allocation_tracking")
@@ -241,6 +245,10 @@ def _load_config!(env_param = nil, &block)
# @return [void]
# @since 1.0.0
def stop(called_by = nil)
+ # Wrapped in `Thread.new ... .join` so this is safe to call from a
+ # `Signal.trap` block: `Mutex#synchronize` (used by
+ # `CheckIn::Scheduler`) is unsafe in trap handlers, and running on a
+ # separate thread sidesteps that restriction. See PR #1295.
Thread.new do
if called_by
internal_logger.info("Stopping AppSignal (#{called_by})")
@@ -250,6 +258,7 @@ def stop(called_by = nil)
Appsignal::Extension.stop
Appsignal::Probes.stop
Appsignal::CheckIn.stop
+ Appsignal::OpenTelemetry.shutdown
end.join
nil
end
diff --git a/lib/appsignal/backends.rb b/lib/appsignal/backends.rb
new file mode 100644
index 000000000..e0d50425c
--- /dev/null
+++ b/lib/appsignal/backends.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "appsignal/metrics/extension_backend"
+require "appsignal/metrics/opentelemetry_backend"
+require "appsignal/logger/extension_backend"
+require "appsignal/logger/opentelemetry_backend"
+require "appsignal/transaction/base_backend"
+require "appsignal/transaction/extension_backend"
+require "appsignal/transaction/opentelemetry_backend"
+
+module Appsignal
+ # @!visibility private
+ #
+ # Looks up the active backend for each AppSignal subsystem. In normal
+ # operation, subsystems route through the C-extension (and its agent).
+ # When collector mode is configured and the OpenTelemetry SDK has booted
+ # successfully, supported subsystems route through OTel instead.
+ #
+ # Centralizes the mode-check so per-subsystem call sites don't repeat the
+ # "if collector? then OTel else Extension" branch. Future subsystems plug
+ # in by adding one more lookup method here.
+ module Backends
+ class << self
+ def metrics
+ if collector?
+ Appsignal::Metrics::OpenTelemetryBackend
+ else
+ Appsignal::Metrics::ExtensionBackend
+ end
+ end
+
+ def logger
+ if collector?
+ Appsignal::Logger::OpenTelemetryBackend
+ else
+ Appsignal::Logger::ExtensionBackend
+ end
+ end
+
+ def transaction
+ if collector?
+ Appsignal::Transaction::OpenTelemetryBackend
+ else
+ Appsignal::Transaction::ExtensionBackend
+ end
+ end
+
+ private
+
+ def collector?
+ Appsignal.config&.collector_mode? || false
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/config.rb b/lib/appsignal/config.rb
index b938342d1..e70c20431 100644
--- a/lib/appsignal/config.rb
+++ b/lib/appsignal/config.rb
@@ -91,6 +91,7 @@ def dsl_config_file?
DEFAULT_CONFIG = {
:activejob_report_errors => "all",
:ca_file_path => File.expand_path(File.join("../../../resources/cacert.pem"), __FILE__),
+ :collector_endpoint => nil,
:dns_servers => [],
:enable_allocation_tracking => true,
:enable_at_exit_hook => "on_error",
@@ -106,8 +107,12 @@ def dsl_config_file?
:enable_rake_performance_instrumentation => false,
:endpoint => "https://push.appsignal.com",
:files_world_accessible => true,
+ :filter_attributes => [],
+ :filter_function_parameters => [],
:filter_metadata => [],
:filter_parameters => [],
+ :filter_request_payload => [],
+ :filter_request_query_parameters => [],
:filter_session_data => [],
:ignore_actions => [],
:ignore_errors => [],
@@ -130,9 +135,14 @@ def dsl_config_file?
REQUEST_METHOD REQUEST_PATH SERVER_NAME SERVER_PORT
SERVER_PROTOCOL
],
+ :response_headers => [],
:send_environment_metadata => true,
+ :send_function_parameters => nil,
:send_params => true,
+ :send_request_payload => nil,
+ :send_request_query_parameters => nil,
:send_session_data => true,
+ :service_name => nil,
:sidekiq_report_errors => "all",
:default_tags => {}
}.freeze
@@ -158,6 +168,7 @@ def dsl_config_file?
:name => "APPSIGNAL_APP_NAME",
:bind_address => "APPSIGNAL_BIND_ADDRESS",
:ca_file_path => "APPSIGNAL_CA_FILE_PATH",
+ :collector_endpoint => "APPSIGNAL_COLLECTOR_ENDPOINT",
:enable_at_exit_hook => "APPSIGNAL_ENABLE_AT_EXIT_HOOK",
:hostname => "APPSIGNAL_HOSTNAME",
:host_role => "APPSIGNAL_HOST_ROLE",
@@ -168,6 +179,7 @@ def dsl_config_file?
:logging_endpoint => "APPSIGNAL_LOGGING_ENDPOINT",
:endpoint => "APPSIGNAL_PUSH_API_ENDPOINT",
:push_api_key => "APPSIGNAL_PUSH_API_KEY",
+ :service_name => "APPSIGNAL_SERVICE_NAME",
:sidekiq_report_errors => "APPSIGNAL_SIDEKIQ_REPORT_ERRORS",
:statsd_port => "APPSIGNAL_STATSD_PORT",
:nginx_port => "APPSIGNAL_NGINX_PORT",
@@ -202,21 +214,29 @@ def dsl_config_file?
:ownership_set_namespace => "APPSIGNAL_OWNERSHIP_SET_NAMESPACE",
:running_in_container => "APPSIGNAL_RUNNING_IN_CONTAINER",
:send_environment_metadata => "APPSIGNAL_SEND_ENVIRONMENT_METADATA",
+ :send_function_parameters => "APPSIGNAL_SEND_FUNCTION_PARAMETERS",
:send_params => "APPSIGNAL_SEND_PARAMS",
+ :send_request_payload => "APPSIGNAL_SEND_REQUEST_PAYLOAD",
+ :send_request_query_parameters => "APPSIGNAL_SEND_REQUEST_QUERY_PARAMETERS",
:send_session_data => "APPSIGNAL_SEND_SESSION_DATA"
}.freeze
# @!visibility private
ARRAY_OPTIONS = {
:dns_servers => "APPSIGNAL_DNS_SERVERS",
+ :filter_attributes => "APPSIGNAL_FILTER_ATTRIBUTES",
+ :filter_function_parameters => "APPSIGNAL_FILTER_FUNCTION_PARAMETERS",
:filter_metadata => "APPSIGNAL_FILTER_METADATA",
:filter_parameters => "APPSIGNAL_FILTER_PARAMETERS",
+ :filter_request_payload => "APPSIGNAL_FILTER_REQUEST_PAYLOAD",
+ :filter_request_query_parameters => "APPSIGNAL_FILTER_REQUEST_QUERY_PARAMETERS",
:filter_session_data => "APPSIGNAL_FILTER_SESSION_DATA",
:ignore_actions => "APPSIGNAL_IGNORE_ACTIONS",
:ignore_errors => "APPSIGNAL_IGNORE_ERRORS",
:ignore_logs => "APPSIGNAL_IGNORE_LOGS",
:ignore_namespaces => "APPSIGNAL_IGNORE_NAMESPACES",
- :request_headers => "APPSIGNAL_REQUEST_HEADERS"
+ :request_headers => "APPSIGNAL_REQUEST_HEADERS",
+ :response_headers => "APPSIGNAL_RESPONSE_HEADERS"
}.freeze
# @!visibility private
@@ -229,6 +249,39 @@ def dsl_config_file?
:default_tags => "APPSIGNAL_DEFAULT_TAGS"
}.freeze
+ # Collector mode requires Ruby 3.1+. The OpenTelemetry Ruby SDK relies on
+ # `Process._fork` (introduced in Ruby 3.1) for its fork hooks, without
+ # which background reader threads don't restart in child processes and
+ # buffered telemetry is lost after a fork.
+ # @!visibility private
+ MIN_RUBY_VERSION_FOR_COLLECTOR_MODE = "3.1"
+
+ # Configuration options that only have an effect when the integration is
+ # in collector mode. When the agent is in use, setting any of these emits
+ # a warning at startup.
+ # @!visibility private
+ COLLECTOR_ONLY_OPTIONS = [
+ :filter_attributes,
+ :filter_function_parameters,
+ :filter_request_payload,
+ :filter_request_query_parameters,
+ :response_headers,
+ :send_function_parameters,
+ :send_request_payload,
+ :send_request_query_parameters,
+ :service_name
+ ].freeze
+
+ # Existing AppSignal options that only affect the agent's handling of
+ # trace data. In collector mode these don't filter anything; users need
+ # their collector-mode equivalents (see COLLECTOR_ONLY_OPTIONS).
+ # @!visibility private
+ AGENT_ONLY_TRACE_OPTIONS = [
+ :filter_metadata,
+ :filter_parameters,
+ :send_params
+ ].freeze
+
# @!visibility private
attr_reader :root_path, :env, :config_hash
@@ -433,6 +486,57 @@ def active?
valid? && active_for_env?
end
+ # Check if collector mode is configured.
+ #
+ # Returns true when a non-empty `collector_endpoint` is set and the
+ # running Ruby version is at least {MIN_RUBY_VERSION_FOR_COLLECTOR_MODE}.
+ # On older Rubies, `collector_endpoint` is ignored (with a warning) and
+ # the AppSignal agent is used instead.
+ #
+ # This is the *intent* check — it answers "did the user ask for
+ # collector mode, and could we honor it?". It does not say whether the
+ # OpenTelemetry SDK actually booted. See {#collector_mode?} for that.
+ #
+ # Memoised: the result is cached on first call so hot paths avoid
+ # re-running the string-strip predicate, and so the unsupported-Ruby
+ # warning is emitted at most once per `Config` instance.
+ #
+ # @return [Boolean] True if collector mode is configured.
+ def collector_mode_configured?
+ return @collector_mode_configured if defined?(@collector_mode_configured)
+
+ endpoint = config_hash[:collector_endpoint]
+ configured = !endpoint.nil? && !endpoint.to_s.strip.empty?
+
+ if configured && Gem::Version.new(RUBY_VERSION) <
+ Gem::Version.new(MIN_RUBY_VERSION_FOR_COLLECTOR_MODE)
+ Appsignal::Utils::StdoutAndLoggerMessage.warning(
+ "Collector mode requires Ruby #{MIN_RUBY_VERSION_FOR_COLLECTOR_MODE} or higher " \
+ "(running Ruby #{RUBY_VERSION}). The `collector_endpoint` option will be " \
+ "ignored and the AppSignal agent will be used instead."
+ )
+ @collector_mode_configured = false
+ else
+ @collector_mode_configured = configured
+ end
+ end
+
+ # Check if AppSignal is actively running in collector mode.
+ #
+ # True only if collector mode is {#collector_mode_configured? configured}
+ # *and* `Appsignal::OpenTelemetry.configure` has successfully booted the
+ # SDK in this process. Use this for backend dispatch on hot paths
+ # (metric and log emits): if the OTel boot failed, callers fall back to
+ # the agent backend rather than silently dropping data into no-op
+ # providers.
+ #
+ # @return [Boolean] True if collector mode is configured and started.
+ def collector_mode?
+ collector_mode_configured? &&
+ defined?(Appsignal::OpenTelemetry) &&
+ Appsignal::OpenTelemetry.started?
+ end
+
# @!visibility private
def write_to_environment
ENV["_APPSIGNAL_ACTIVE"] = active?.to_s
@@ -509,6 +613,30 @@ def validate
else
@valid = true
end
+
+ warn_for_mode_mismatch
+ end
+
+ # Emit warnings when a configuration option is set that has no effect in
+ # the current mode (collector vs. agent).
+ #
+ # Uses {#collector_mode_configured?} (intent) rather than
+ # {#collector_mode?} so the warnings fire based on what the user asked
+ # for, independent of whether the OpenTelemetry SDK successfully booted.
+ # @!visibility private
+ def warn_for_mode_mismatch
+ if collector_mode_configured?
+ warn_user_modified(AGENT_ONLY_TRACE_OPTIONS) do |option|
+ "The collector is in use. The '#{option}' configuration option is " \
+ "only used by the agent for trace data and will be ignored."
+ end
+ else
+ warn_user_modified(COLLECTOR_ONLY_OPTIONS) do |option|
+ "The agent is in use. The '#{option}' configuration option is " \
+ "only used by the collector and will be ignored. Set " \
+ "'collector_endpoint' to use the collector."
+ end
+ end
end
# Deep freeze the config object so it cannot be modified during the runtime
@@ -532,6 +660,17 @@ def yml_config_file?
private
+ # Yield a warning for each option in `options` whose effective value
+ # differs from the default. Setting an option to its default value is
+ # a no-op, so we don't warn about it.
+ def warn_user_modified(options)
+ options.each do |option|
+ next if config_hash[option] == DEFAULT_CONFIG[option]
+
+ logger.warn(yield(option))
+ end
+ end
+
def logger
Appsignal.internal_logger
end
diff --git a/lib/appsignal/helpers/instrumentation.rb b/lib/appsignal/helpers/instrumentation.rb
index 7c1acbd35..cb7ef2f34 100644
--- a/lib/appsignal/helpers/instrumentation.rb
+++ b/lib/appsignal/helpers/instrumentation.rb
@@ -715,7 +715,7 @@ def add_headers(headers = nil, &block)
# Breadcrumbs can be used to trace what path a user has taken
# before encountering an error.
#
- # Only the last 20 added breadcrumbs will be saved.
+ # At most 20 of the added breadcrumbs will be saved.
#
# @example
# Appsignal.add_breadcrumb(
@@ -800,10 +800,18 @@ def instrument(
title = nil,
body = nil,
body_format = Appsignal::EventFormatter::DEFAULT,
+ opentelemetry_kind: nil,
&block
)
Appsignal::Transaction.current
- .instrument(name, title, body, body_format, &block)
+ .instrument(
+ name,
+ title,
+ body,
+ body_format,
+ :opentelemetry_kind => opentelemetry_kind,
+ &block
+ )
end
# Instrumentation helper for SQL queries.
diff --git a/lib/appsignal/helpers/metrics.rb b/lib/appsignal/helpers/metrics.rb
index 5932b8a1f..ded4d0ec7 100644
--- a/lib/appsignal/helpers/metrics.rb
+++ b/lib/appsignal/helpers/metrics.rb
@@ -16,14 +16,7 @@ module Metrics
# @see https://docs.appsignal.com/metrics/custom.html
# Metrics documentation
def set_gauge(name, value, tags = {})
- Appsignal::Extension.set_gauge(
- name.to_s,
- value.to_f,
- Appsignal::Utils::Data.generate(tags)
- )
- rescue RangeError
- Appsignal.internal_logger
- .warn("The gauge value '#{value}' for metric '#{name}' is too big")
+ Appsignal::Backends.metrics.set_gauge(name, value, tags)
end
# Report a counter metric.
@@ -39,14 +32,7 @@ def set_gauge(name, value, tags = {})
# @see https://docs.appsignal.com/metrics/custom.html
# Metrics documentation
def increment_counter(name, value = 1.0, tags = {})
- Appsignal::Extension.increment_counter(
- name.to_s,
- value.to_f,
- Appsignal::Utils::Data.generate(tags)
- )
- rescue RangeError
- Appsignal.internal_logger
- .warn("The counter value '#{value}' for metric '#{name}' is too big")
+ Appsignal::Backends.metrics.increment_counter(name, value, tags)
end
# Report a distribution metric.
@@ -62,14 +48,7 @@ def increment_counter(name, value = 1.0, tags = {})
# @see https://docs.appsignal.com/metrics/custom.html
# Metrics documentation
def add_distribution_value(name, value, tags = {})
- Appsignal::Extension.add_distribution_value(
- name.to_s,
- value.to_f,
- Appsignal::Utils::Data.generate(tags)
- )
- rescue RangeError
- Appsignal.internal_logger
- .warn("The distribution value '#{value}' for metric '#{name}' is too big")
+ Appsignal::Backends.metrics.add_distribution_value(name, value, tags)
end
end
end
diff --git a/lib/appsignal/hooks/active_job.rb b/lib/appsignal/hooks/active_job.rb
index 2b6446d0b..71a2934cd 100644
--- a/lib/appsignal/hooks/active_job.rb
+++ b/lib/appsignal/hooks/active_job.rb
@@ -29,8 +29,11 @@ def install
ActiveSupport.on_load(:active_job) do
::ActiveJob::Base
.extend ::Appsignal::Hooks::ActiveJobHook::ActiveJobClassInstrumentation
+ # Carry W3C trace context across the enqueue/perform boundary in
+ # collector mode (no-ops otherwise). The patches are cheap and
+ # mode-gated inside their method bodies, so install them unconditionally.
::ActiveJob::Base
- .prepend ::Appsignal::Hooks::ActiveJobHook::ActiveJobEnqueueInstrumentation
+ .prepend ::Appsignal::Hooks::ActiveJobHook::ActiveJobTraceContext
next unless Appsignal::Hooks::ActiveJobHook.version_7_1_or_higher?
@@ -43,33 +46,6 @@ def install
end
end
- # Records an `enqueue.active_job` event when a job is enqueued, so the
- # enqueue shows up on the active transaction's timeline (e.g. when
- # enqueuing from within a web request or another job).
- #
- # Wrapping `enqueue` ourselves -- rather than relying on Rails' native
- # `enqueue.active_job` notification, which the AppSignal notifications
- # path now suppresses -- gives us a single event we own. Like all
- # AppSignal events, this only records when there's an active transaction;
- # an enqueue with no transaction is a transparent pass-through.
- #
- # @!visibility private
- module ActiveJobEnqueueInstrumentation
- def enqueue(*, **)
- Appsignal.instrument("enqueue.active_job", "enqueue #{self.class.name} job") do
- # Active Job enqueues through an adapter (Sidekiq, Resque, ...) that
- # has its own enqueue instrumentation. Suppress it so the enqueue is
- # recorded once, as this event, rather than as nested Active Job +
- # adapter events.
- if Appsignal::Transaction.current?
- Appsignal::Transaction.current.suppress_job_enqueue_events { super }
- else
- super
- end
- end
- end
- end
-
module ActiveJobClassInstrumentation
def execute(job)
enqueued_at = job["enqueued_at"]
@@ -91,8 +67,17 @@ def execute(job)
# We don't have a separate integration for this QueueAdapter like
# we do for Sidekiq.
#
+ # Read the trace context off the job so the transaction links back
+ # to the enqueuer (no-op outside collector mode). Only here, in the
+ # standalone branch: when a wrapper integration (e.g. Sidekiq)
+ # created the transaction, it already extracted, so we must not
+ # extract a second time.
+ #
# Prefer job_id from provider, instead of ActiveJob's internal ID.
- Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
+ Appsignal::Transaction.create(
+ Appsignal::Transaction::BACKGROUND_JOB,
+ :opentelemetry_context => Appsignal::OpenTelemetry.extract_job_context(job)
+ )
end
begin
@@ -152,6 +137,66 @@ def transaction_set_error(transaction, exception)
end
end
+ # Reads and writes W3C trace context on the ActiveJob enqueue/perform
+ # boundary, wire-compatible with OpenTelemetry's ActiveJob instrumentation.
+ # All of this no-ops outside collector mode.
+ #
+ # Context rides on the job under `__otel_headers`, the same carrier OTel
+ # uses. Stock `serialize`/`deserialize` only carry a fixed key set, so --
+ # like OTel -- we patch both plus an accessor to round-trip it. The on-wire
+ # value is run through ActiveJob's argument serializer (an array of
+ # `[key, value]` pairs), matching OTel byte-for-byte so an AppSignal- and an
+ # OTel-instrumented service read each other's jobs.
+ module ActiveJobTraceContext
+ # Inject on enqueue from inside a producer event, so the job carries this
+ # transaction's context and the perform later links back. Mirrors the
+ # Sidekiq client middleware: an AppSignal event (a producer span in
+ # collector mode), not a direct SDK span. `Appsignal.instrument` is a
+ # transparent pass-through when there's no active transaction, and
+ # `inject_context` no-ops outside collector mode.
+ def enqueue(*, **)
+ Appsignal.instrument(
+ "enqueue.active_job",
+ "enqueue #{self.class.name} job",
+ :opentelemetry_kind => :producer
+ ) do
+ Appsignal::OpenTelemetry.inject_context(__otel_headers)
+ # Active Job enqueues through an adapter (Sidekiq, Resque, ...) that
+ # has its own enqueue instrumentation. Suppress it so the enqueue is
+ # recorded once, as this event, rather than as nested Active Job +
+ # adapter events.
+ if Appsignal::Transaction.current?
+ Appsignal::Transaction.current.suppress_job_enqueue_events { super }
+ else
+ super
+ end
+ end
+ end
+
+ def serialize
+ super.tap do |data|
+ Appsignal::OpenTelemetry.if_started do
+ next if __otel_headers.empty?
+
+ data["__otel_headers"] = ::ActiveJob::Arguments.serialize(__otel_headers)
+ end
+ end
+ end
+
+ def deserialize(job_data)
+ super
+ serialized = job_data["__otel_headers"]
+ @__otel_headers =
+ serialized ? ::ActiveJob::Arguments.deserialize(serialized).to_h : {}
+ end
+
+ def __otel_headers
+ @__otel_headers ||= {}
+ end
+
+ attr_writer :__otel_headers
+ end
+
module ActiveJobHelpers
ACTION_MAILER_CLASSES = [
"ActionMailer::DeliveryJob",
diff --git a/lib/appsignal/hooks/excon.rb b/lib/appsignal/hooks/excon.rb
index ff09de8bf..09b308438 100644
--- a/lib/appsignal/hooks/excon.rb
+++ b/lib/appsignal/hooks/excon.rb
@@ -12,7 +12,21 @@ def dependencies_present?
def install
require "appsignal/integrations/excon"
+ require "appsignal/integrations/excon/appsignal_middleware"
::Excon.defaults[:instrumentor] = Appsignal::Integrations::ExconIntegration
+
+ # Insert our middleware just before the Mock middleware (the innermost,
+ # where the response is produced) so its `request_call` runs before the
+ # request is sent -- and, being inside the Instrumentor middleware, while
+ # our event span is current, so the injected `traceparent` reflects it.
+ # Appending to the end would place it after Mock, which short-circuits
+ # the request chain before reaching it.
+ middlewares = ::Excon.defaults[:middlewares].dup
+ return if middlewares.include?(Appsignal::Integrations::ExconMiddleware)
+
+ index = middlewares.index(::Excon::Middleware::Mock) || middlewares.length
+ middlewares.insert(index, Appsignal::Integrations::ExconMiddleware)
+ ::Excon.defaults[:middlewares] = middlewares
end
end
end
diff --git a/lib/appsignal/hooks/http.rb b/lib/appsignal/hooks/http.rb
index f6c00421c..f3f8901ef 100644
--- a/lib/appsignal/hooks/http.rb
+++ b/lib/appsignal/hooks/http.rb
@@ -32,6 +32,11 @@ def install
if defined?(HTTP::Session)
HTTP::Session.prepend Appsignal::Integrations::HttpIntegration::KeywordOptions
end
+ # Propagate trace context onto every outgoing hop (redirects included) at
+ # `Client#perform`, where the live request headers are reachable. Kept
+ # separate from the request-boundary event above: it only injects context
+ # and no-ops outside collector mode.
+ HTTP::Client.prepend Appsignal::Integrations::HttpIntegration::ContextInjection
Appsignal::Environment.report_enabled("http_rb")
end
diff --git a/lib/appsignal/hooks/resque.rb b/lib/appsignal/hooks/resque.rb
index 01183fd25..1cd159e17 100644
--- a/lib/appsignal/hooks/resque.rb
+++ b/lib/appsignal/hooks/resque.rb
@@ -15,7 +15,7 @@ def install
Resque::Job.prepend Appsignal::Integrations::ResqueIntegration
# Resque enqueues through the `Resque.push` singleton method, so prepend
- # onto its singleton class to record the enqueue event.
+ # onto its singleton class to write the trace context onto outgoing jobs.
Resque.singleton_class.prepend Appsignal::Integrations::ResquePushIntegration
end
end
diff --git a/lib/appsignal/hooks/sequel.rb b/lib/appsignal/hooks/sequel.rb
index 2f690fecc..c9356a85c 100644
--- a/lib/appsignal/hooks/sequel.rb
+++ b/lib/appsignal/hooks/sequel.rb
@@ -10,7 +10,8 @@ def log_yield(sql, args = nil)
"sql.sequel",
nil,
sql,
- Appsignal::EventFormatter::SQL_BODY_FORMAT
+ Appsignal::EventFormatter::SQL_BODY_FORMAT,
+ :opentelemetry_kind => :client
) do
super
end
@@ -25,7 +26,8 @@ def log_connection_yield(sql, conn, args = nil)
"sql.sequel",
nil,
sql,
- Appsignal::EventFormatter::SQL_BODY_FORMAT
+ Appsignal::EventFormatter::SQL_BODY_FORMAT,
+ :opentelemetry_kind => :client
) do
super
end
diff --git a/lib/appsignal/hooks/shoryuken.rb b/lib/appsignal/hooks/shoryuken.rb
index 4ab7cd205..b95f5b834 100644
--- a/lib/appsignal/hooks/shoryuken.rb
+++ b/lib/appsignal/hooks/shoryuken.rb
@@ -19,9 +19,9 @@ def install
end
# Servers enqueue jobs too, so they need the client middleware that
- # records the enqueue event. Shoryuken only yields `configure_client`
- # outside the server, so register it here as well for enqueues from
- # within a worker.
+ # writes the trace context onto outgoing messages. Shoryuken only
+ # yields `configure_client` outside the server, so register it here as
+ # well for enqueues from within a worker.
config.client_middleware do |chain|
chain.add Appsignal::Integrations::ShoryukenClientMiddleware
end
diff --git a/lib/appsignal/hooks/sidekiq.rb b/lib/appsignal/hooks/sidekiq.rb
index f78a0839f..62f25912a 100644
--- a/lib/appsignal/hooks/sidekiq.rb
+++ b/lib/appsignal/hooks/sidekiq.rb
@@ -44,7 +44,7 @@ def install
end
# Servers enqueue jobs too, so they need the client middleware that
- # records the enqueue event.
+ # writes the trace context onto outgoing jobs.
config.client_middleware do |chain|
chain.add Appsignal::Integrations::SidekiqClientMiddleware
end
diff --git a/lib/appsignal/integrations/active_support_notifications.rb b/lib/appsignal/integrations/active_support_notifications.rb
index 074219964..99f4faab2 100644
--- a/lib/appsignal/integrations/active_support_notifications.rb
+++ b/lib/appsignal/integrations/active_support_notifications.rb
@@ -7,17 +7,30 @@ module ActiveSupportNotificationsIntegration
class << self
BANG = "!"
- # Events a dedicated AppSignal integration already records, so the
- # generic notifications path must not record them a second time. The
- # ActiveJob hook owns `enqueue.active_job` (it wraps the enqueue in its
- # own event, with Rails' native notification nested inside), and the
- # Faraday integration owns `request.faraday`.
+ # ActiveSupport::Notifications events whose span represents an outgoing
+ # call to a datastore, so they carry CLIENT kind in collector mode (to
+ # match the dedicated DB integrations). Kept deliberately narrow:
+ # `start_event` runs for every instrumented Rails event and span kind is
+ # immutable, so only genuine client calls belong here. Object
+ # instantiation (`instantiation.active_record`) is not a client call.
+ CLIENT_EVENT_NAMES = ["sql.active_record"].freeze
+
+ # Events a dedicated AppSignal integration already records with richer
+ # semantics, so the generic notifications path must not record them a
+ # second time. The ActiveJob hook owns `enqueue.active_job`: it wraps the
+ # enqueue in a producer event that also injects trace context, and the
+ # native notification fires nested inside it. The Faraday integration owns
+ # `request.faraday`: its middleware records the request as a client event
+ # and injects trace context, and Faraday's own instrumentation
+ # notification, if the user added that middleware, fires nested inside it.
SUPPRESSED_EVENT_NAMES = ["enqueue.active_job", "request.faraday"].freeze
def start_event(name)
return unless record_event?(name)
- Appsignal::Transaction.current.start_event
+ Appsignal::Transaction.current.start_event(
+ :opentelemetry_kind => CLIENT_EVENT_NAMES.include?(name.to_s) ? :client : nil
+ )
end
def finish_event(name, payload = {})
diff --git a/lib/appsignal/integrations/data_mapper.rb b/lib/appsignal/integrations/data_mapper.rb
index 358ea4ada..50ef9797d 100644
--- a/lib/appsignal/integrations/data_mapper.rb
+++ b/lib/appsignal/integrations/data_mapper.rb
@@ -21,13 +21,15 @@ def log(message)
body_format = Appsignal::EventFormatter::DEFAULT
end
- # Record event
+ # Record event. The query is an outgoing call to the database, so tag it
+ # as a client span (collector mode); no-op in agent mode.
Appsignal::Transaction.current.record_event(
"query.data_mapper",
"DataMapper Query",
body_content,
message.duration,
- body_format
+ body_format,
+ :opentelemetry_kind => :client
)
super
end
diff --git a/lib/appsignal/integrations/dry_monitor.rb b/lib/appsignal/integrations/dry_monitor.rb
index bad3e495b..e2ef8b5f5 100644
--- a/lib/appsignal/integrations/dry_monitor.rb
+++ b/lib/appsignal/integrations/dry_monitor.rb
@@ -4,8 +4,13 @@ module Appsignal
module Integrations
# @!visibility private
module DryMonitorIntegration
+ # ROM emits its SQL queries as dry-monitor `"sql"` events; tag those as
+ # CLIENT in collector mode to match the dedicated DB integrations. Span
+ # kind is immutable, so it has to be set here at event start.
def instrument(event_id, payload = {}, &block)
- Appsignal::Transaction.current.start_event
+ Appsignal::Transaction.current.start_event(
+ :opentelemetry_kind => event_id.to_s == "sql" ? :client : nil
+ )
super
ensure
diff --git a/lib/appsignal/integrations/excon.rb b/lib/appsignal/integrations/excon.rb
index b659caffb..56a92dd1f 100644
--- a/lib/appsignal/integrations/excon.rb
+++ b/lib/appsignal/integrations/excon.rb
@@ -22,7 +22,7 @@ def self.instrument(name, data, &block)
else
"#{data[:method].to_s.upcase} #{data[:scheme]}://#{data[:host]}"
end
- Appsignal.instrument(rails_name, title, &block)
+ Appsignal.instrument(rails_name, title, :opentelemetry_kind => :client, &block)
end
end
end
diff --git a/lib/appsignal/integrations/excon/appsignal_middleware.rb b/lib/appsignal/integrations/excon/appsignal_middleware.rb
new file mode 100644
index 000000000..32b736834
--- /dev/null
+++ b/lib/appsignal/integrations/excon/appsignal_middleware.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Appsignal
+ module Integrations
+ # Excon middleware that writes trace context onto the outgoing request, so
+ # the called service joins this trace. The existing Excon instrumentor
+ # records the event span; this middleware only injects.
+ #
+ # @!visibility private
+ class ExconMiddleware < ::Excon::Middleware::Base
+ def request_call(datum)
+ datum[:headers] ||= {}
+ # Inject from whatever span is current. The instrumentor's event span is
+ # active during the request, so the written `traceparent` reflects the
+ # Excon client event. No-op outside collector mode.
+ Appsignal::OpenTelemetry.inject_context(datum[:headers])
+ super
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/integrations/faraday.rb b/lib/appsignal/integrations/faraday.rb
index af5d253df..38cd4336d 100644
--- a/lib/appsignal/integrations/faraday.rb
+++ b/lib/appsignal/integrations/faraday.rb
@@ -2,10 +2,11 @@
module Appsignal
module Integrations
- # Faraday middleware that records each request as a `request.faraday` event
- # and suppresses the downstream HTTP client's own instrumentation, so the
- # request is recorded once rather than as nested Faraday + Net::HTTP (or
- # Excon) client events.
+ # Faraday middleware that records each request as a `request.faraday` client
+ # event, writes trace context onto the outgoing request so the called service
+ # joins this trace, and suppresses the downstream HTTP client's own
+ # instrumentation, so the request is recorded once rather than as nested
+ # Faraday + Net::HTTP (or Excon) client events.
#
# @!visibility private
class FaradayMiddleware < ::Faraday::Middleware
@@ -16,8 +17,16 @@ def call(env)
# Net::HTTP's (scheme and host only), keeping paths out of event titles.
Appsignal.instrument(
"request.faraday",
- "#{http_method} #{uri.scheme}://#{uri.host}"
+ "#{http_method} #{uri.scheme}://#{uri.host}",
+ :opentelemetry_kind => :client
) do
+ # Write trace context onto the outgoing request so the called service
+ # joins this trace. Injected inside the instrument block, so the written
+ # `traceparent` reflects the Faraday client event's span. No-op outside
+ # collector mode. `env.request_headers` is the live outgoing header set
+ # and a valid carrier (it responds to `[]=`).
+ Appsignal::OpenTelemetry.inject_context(env.request_headers)
+
# Faraday's default adapter is Net::HTTP, which AppSignal also
# instruments. Suppress the adapter's own instrumentation so the
# request appears once (as the Faraday event) rather than as nested
@@ -37,8 +46,9 @@ def call(env)
# the build path is the only way to instrument every connection automatically.
#
# Just before the adapter (the innermost handler, where the request is sent)
- # it inserts `FaradayMiddleware`, which records the `request.faraday` event
- # and suppresses the downstream client. Skipped if it's already present.
+ # it inserts `FaradayMiddleware`, which records the `request.faraday` event,
+ # injects trace context, and suppresses the downstream client. Skipped if it's
+ # already present.
#
# @!visibility private
module FaradayRackBuilderPatch
diff --git a/lib/appsignal/integrations/http.rb b/lib/appsignal/integrations/http.rb
index fa65cfbec..80227d928 100644
--- a/lib/appsignal/integrations/http.rb
+++ b/lib/appsignal/integrations/http.rb
@@ -9,7 +9,12 @@ def self.instrument(verb, uri, &block)
parsed_request_uri = uri.is_a?(URI) ? uri : uri_module.parse(uri.to_s)
request_uri = "#{parsed_request_uri.scheme}://#{parsed_request_uri.host}"
- Appsignal.instrument("request.http_rb", "#{verb.upcase} #{request_uri}", &block)
+ Appsignal.instrument(
+ "request.http_rb",
+ "#{verb.to_s.upcase} #{request_uri}",
+ :opentelemetry_kind => :client,
+ &block
+ )
end
# The event is recorded at the request boundary, so a redirected request
@@ -32,6 +37,21 @@ def request(verb, uri, **opts)
HttpIntegration.instrument(verb, uri) { super }
end
end
+
+ # Trace context has to ride on each outgoing hop's headers, so it's
+ # injected at `HTTP::Client#perform` -- the single send chokepoint in both
+ # http5 and http6, called once per request and once per redirect hop --
+ # where the live request headers are reachable. The event stays at the
+ # request boundary above, so a redirected request is still a single event;
+ # this only propagates context, and every hop carries it. No-op outside
+ # collector mode. `req.headers` is the live outgoing header set and a valid
+ # carrier (it responds to `[]=`).
+ module ContextInjection
+ def perform(req, options)
+ Appsignal::OpenTelemetry.inject_context(req.headers)
+ super
+ end
+ end
end
end
end
diff --git a/lib/appsignal/integrations/mongo_ruby_driver.rb b/lib/appsignal/integrations/mongo_ruby_driver.rb
index e5950223c..c3e9841de 100644
--- a/lib/appsignal/integrations/mongo_ruby_driver.rb
+++ b/lib/appsignal/integrations/mongo_ruby_driver.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "json"
+
module Appsignal
class Hooks
# @!visibility private
@@ -19,8 +21,8 @@ def started(event)
store = transaction.store("mongo_driver")
store[event.request_id] = command
- # Start this event
- transaction.start_event
+ # Start this event. The query is an outgoing client call.
+ transaction.start_event(:opentelemetry_kind => :client)
end
# Called by Mongo::Monitor when query succeeds
@@ -47,8 +49,9 @@ def finish(result, event)
command = store.delete(event.request_id) || {}
# Finish the event. The sanitized command is a (nested) Hash; emit it
- # as a JSON string. The agent serializes structured bodies to JSON
- # anyway, so this is equivalent output.
+ # as a JSON string so it works with both transaction backends. The
+ # agent serializes structured bodies to JSON anyway, so this is
+ # equivalent output there, and the collector receives a plain string.
transaction.finish_event(
"query.mongodb",
"#{event.command_name} | #{event.database_name} | #{result}",
diff --git a/lib/appsignal/integrations/net_http.rb b/lib/appsignal/integrations/net_http.rb
index 731085b91..7dba3fac8 100644
--- a/lib/appsignal/integrations/net_http.rb
+++ b/lib/appsignal/integrations/net_http.rb
@@ -14,8 +14,13 @@ def request(request, body = nil, &block)
Appsignal.instrument(
"request.net_http",
- "#{request.method} #{use_ssl? ? "https" : "http"}://#{request["host"] || address}"
+ "#{request.method} #{use_ssl? ? "https" : "http"}://#{request["host"] || address}",
+ :opentelemetry_kind => :client
) do
+ # Write trace context onto the outgoing request so the called service
+ # joins this trace. No-op outside collector mode. The request object
+ # is a valid carrier (it responds to `[]=`).
+ Appsignal::OpenTelemetry.inject_context(request)
super
end
end
diff --git a/lib/appsignal/integrations/que.rb b/lib/appsignal/integrations/que.rb
index 5e5c54cb6..361688275 100644
--- a/lib/appsignal/integrations/que.rb
+++ b/lib/appsignal/integrations/que.rb
@@ -2,11 +2,77 @@
module Appsignal
module Integrations
+ # @!visibility private
+ #
+ # Reads and writes W3C trace context the way OpenTelemetry's Que
+ # instrumentation does: as `"key:value"` strings in the job's tags array
+ # (the only carrier Que's enqueue API exposes). Collector mode only.
+ module QueTraceContext
+ module_function
+
+ # Que has no header map, so context rides in the tags array. OTel writes
+ # each header as a `"key:value"` tag; mirror that exact format.
+ module TagSetter
+ def self.set(carrier, key, value)
+ carrier << "#{key}:#{value}"
+ end
+ end
+
+ # Que rejects jobs with too many or too-long tags, so injected context
+ # must stay within these or the enqueue would raise. Read the limits from
+ # Que when available, with the documented defaults as a fallback.
+ MAX_TAGS_COUNT =
+ defined?(::Que::Job::MAXIMUM_TAGS_COUNT) ? ::Que::Job::MAXIMUM_TAGS_COUNT : 5
+ MAX_TAG_LENGTH =
+ defined?(::Que::Job::MAXIMUM_TAG_LENGTH) ? ::Que::Job::MAXIMUM_TAG_LENGTH : 100
+
+ # Read the incoming context off the job's tags. Splits each `"key:value"`
+ # tag on the first colon back into a carrier hash, then extracts. Returns
+ # an `OpenTelemetry::Context`, or `nil` outside collector mode.
+ def extract(tags)
+ Appsignal::OpenTelemetry.if_started do
+ carrier = Array(tags)
+ .map { |tag| tag.split(":", 2) }
+ .select { |pair| pair.size == 2 }
+ .to_h
+ ::OpenTelemetry.propagation.extract(carrier)
+ end
+ end
+
+ # Returns the tags array to enqueue the job with. In collector mode injects
+ # the current context into a copy of the tags; keeps the result only if it
+ # still fits Que's limits, otherwise returns the original tags unchanged --
+ # we skip propagation rather than break the user's enqueue. Outside
+ # collector mode returns the tags unchanged.
+ def inject(tags)
+ original = Array(tags)
+ injected = Appsignal::OpenTelemetry.if_started do
+ copy = original.dup
+ ::OpenTelemetry.propagation.inject(copy, :setter => TagSetter)
+ copy
+ end
+ return original if injected.nil? || !within_limits?(injected)
+
+ injected
+ end
+
+ def within_limits?(tags)
+ tags.length <= MAX_TAGS_COUNT && tags.all? { |tag| tag.length <= MAX_TAG_LENGTH }
+ end
+ end
+
# @!visibility private
module QuePlugin
def _run(*args)
+ local_attrs = respond_to?(:que_attrs) ? que_attrs : attrs
+
+ # Read the incoming trace context off the job's tags so the transaction
+ # links back to the enqueuer. No-op outside collector mode.
transaction =
- Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
+ Appsignal::Transaction.create(
+ Appsignal::Transaction::BACKGROUND_JOB,
+ :opentelemetry_context => QueTraceContext.extract(local_attrs.dig(:data, :tags))
+ )
begin
Appsignal.instrument("perform_job.que") { super }
@@ -14,7 +80,6 @@ def _run(*args)
transaction.set_error(error)
raise error
ensure
- local_attrs = respond_to?(:que_attrs) ? que_attrs : attrs
transaction.set_action_if_nil("#{local_attrs[:job_class]}#run")
transaction.add_params_if_nil do
{
@@ -37,27 +102,54 @@ def _run(*args)
# @!visibility private
#
- # Prepended to `Que::Job`'s singleton so it records each enqueue as an
- # `enqueue.que` event under the active transaction. Like all AppSignal
- # events, it only records when there's an active transaction (e.g. enqueuing
- # from within a web request or another job); otherwise it's a transparent
- # pass-through.
+ # Prepended to `Que::Job`'s singleton so it wraps enqueues. Records the
+ # enqueue as an AppSignal event (a producer span in collector mode), and in
+ # collector mode writes the current trace context onto the job's tags so the
+ # job that later performs links back to it. Like all AppSignal events, the
+ # enqueue only records when there's an active transaction; otherwise it's a
+ # transparent pass-through.
module QueClientPlugin
- def enqueue(*_args, job_options: {}, **_rest)
- # Inside a `bulk_enqueue` block the batch is recorded once by the
- # `bulk_enqueue` wrapper, so each inner enqueue is a pass-through to
- # avoid recording an event per job.
+ def enqueue(*args, job_options: {}, **rest)
+ # Inside a `bulk_enqueue` block the per-job enqueue must stay a
+ # pass-through: tags come from `bulk_enqueue`'s own `job_options` (Que
+ # raises if an inner enqueue passes them), and the batch's event and
+ # propagation are recorded once by the `bulk_enqueue` wrapper.
return super if Thread.current[:appsignal_que_bulk_enqueue]
- # Under Active Job the enqueue is already recorded as an
- # `enqueue.active_job` event, so skip recording it again here.
- return super if Appsignal::Transaction.current? &&
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
-
# Resolve the job class the way Que does: an explicit `:job_class`, else
# the class `enqueue` was called on.
title = "enqueue #{job_options[:job_class] || name} job"
- Appsignal.instrument("enqueue.que", title) { super }
+ record_enqueue(job_options, "enqueue.que", title) do |merged|
+ super(*args, :job_options => merged, **rest)
+ end
+ end
+
+ private
+
+ # Records the enqueue as a producer event and, in collector mode, injects
+ # the current trace context into the job's tags so the job that later
+ # performs links back. Yields the (possibly tag-augmented) `job_options` to
+ # do the actual enqueue.
+ def record_enqueue(job_options, event_name, title)
+ # Under Active Job the enqueue is already recorded as an
+ # `enqueue.active_job` event, so skip recording it again here. The trace
+ # context is still injected so the performed job links back.
+ if Appsignal::Transaction.current? &&
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
+ return yield job_options_with_context(job_options)
+ end
+
+ Appsignal.instrument(event_name, title, :opentelemetry_kind => :producer) do
+ yield job_options_with_context(job_options)
+ end
+ end
+
+ # In collector mode, injects the current trace context into a copy of the
+ # job's tags and returns the tag-augmented `job_options`; a no-op that
+ # returns `job_options` unchanged outside collector mode.
+ def job_options_with_context(job_options)
+ tags = QueTraceContext.inject(job_options[:tags])
+ tags.empty? ? job_options : job_options.merge(:tags => tags)
end
end
@@ -65,22 +157,18 @@ def enqueue(*_args, job_options: {}, **_rest)
#
# `bulk_enqueue` exists only on Que 2+, so this lives in its own module that
# the hook prepends only when Que has the method -- otherwise we'd define a
- # `bulk_enqueue` on Que versions that have none. The whole batch records a
- # single `bulk_enqueue.que` event; the inner enqueues are pass-throughs.
+ # `bulk_enqueue` on Que versions that have none. The whole batch shares one
+ # `job_options`, so it records a single `bulk_enqueue.que` producer event and
+ # the inner enqueues are pass-throughs.
module QueBulkClientPlugin
- def bulk_enqueue(*_args, job_options: {}, **_rest)
- # Under Active Job the enqueue is already recorded as an
- # `enqueue.active_job` event, so skip recording it again here.
- return super if Appsignal::Transaction.current? &&
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
-
- Appsignal.instrument("bulk_enqueue.que", bulk_enqueue_title(job_options)) do
+ def bulk_enqueue(job_options: {}, **rest, &block)
+ record_enqueue(job_options, "bulk_enqueue.que", bulk_enqueue_title(job_options)) do |merged|
# Flag the batch so the enqueues this block triggers pass through
# without recording, without reading Que's internal bulk state.
was_bulk = Thread.current[:appsignal_que_bulk_enqueue]
Thread.current[:appsignal_que_bulk_enqueue] = true
begin
- super
+ super(:job_options => merged, **rest, &block)
ensure
Thread.current[:appsignal_que_bulk_enqueue] = was_bulk
end
diff --git a/lib/appsignal/integrations/redis.rb b/lib/appsignal/integrations/redis.rb
index ddb1ccd96..898f60cee 100644
--- a/lib/appsignal/integrations/redis.rb
+++ b/lib/appsignal/integrations/redis.rb
@@ -12,7 +12,12 @@ def write(command)
"#{command[0]}#{" ?" * (command.size - 1)}"
end
- Appsignal.instrument "query.redis", id, sanitized_command do
+ Appsignal.instrument(
+ "query.redis",
+ id,
+ sanitized_command,
+ :opentelemetry_kind => :client
+ ) do
super
end
end
diff --git a/lib/appsignal/integrations/redis_client.rb b/lib/appsignal/integrations/redis_client.rb
index 891e915e7..d30230248 100644
--- a/lib/appsignal/integrations/redis_client.rb
+++ b/lib/appsignal/integrations/redis_client.rb
@@ -12,7 +12,12 @@ def write(command)
"#{command[0]}#{" ?" * (command.size - 1)}"
end
- Appsignal.instrument "query.redis", @config.id, sanitized_command do
+ Appsignal.instrument(
+ "query.redis",
+ @config.id,
+ sanitized_command,
+ :opentelemetry_kind => :client
+ ) do
super
end
end
diff --git a/lib/appsignal/integrations/resque.rb b/lib/appsignal/integrations/resque.rb
index 70e86d451..d7154f87a 100644
--- a/lib/appsignal/integrations/resque.rb
+++ b/lib/appsignal/integrations/resque.rb
@@ -5,7 +5,12 @@ module Integrations
# @!visibility private
module ResqueIntegration
def perform
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
+ # Read trace context off the job so the transaction links back to the
+ # enqueuer. No-op outside collector mode.
+ transaction = Appsignal::Transaction.create(
+ Appsignal::Transaction::BACKGROUND_JOB,
+ :opentelemetry_context => Appsignal::OpenTelemetry.extract_job_context(payload)
+ )
Appsignal.instrument "perform.resque" do
super
@@ -25,8 +30,10 @@ def perform
end
end
- # Wraps `Resque.push` to record an `enqueue.resque` event so the enqueue
- # shows up under the active transaction.
+ # Wraps `Resque.push` to record an `enqueue.resque` event so the
+ # enqueue shows up under the active transaction (both modes), and in
+ # collector mode writes the trace context onto the job hash so the job that
+ # later performs links back to it.
#
# Like all AppSignal events, this only records when there's an active
# transaction (e.g. enqueuing from within a web request or another job).
@@ -34,13 +41,24 @@ def perform
#
# @!visibility private
module ResquePushIntegration
- def push(_queue, item)
+ def push(queue, item)
# Under Active Job the enqueue is already recorded as an
- # `enqueue.active_job` event, so skip recording it again here.
- return super if Appsignal::Transaction.current? &&
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
+ # `enqueue.active_job` event, so skip recording it again here. The trace
+ # context is still injected so the performed job links back.
+ if Appsignal::Transaction.current? &&
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
+ Appsignal::OpenTelemetry.inject_context(item)
+ return super
+ end
- Appsignal.instrument("enqueue.resque", "enqueue #{item["class"]} job") { super }
+ Appsignal.instrument(
+ "enqueue.resque",
+ "enqueue #{item["class"]} job",
+ :opentelemetry_kind => :producer
+ ) do
+ Appsignal::OpenTelemetry.inject_context(item)
+ super
+ end
end
end
diff --git a/lib/appsignal/integrations/shoryuken.rb b/lib/appsignal/integrations/shoryuken.rb
index 3c46b4b2c..6b79894c1 100644
--- a/lib/appsignal/integrations/shoryuken.rb
+++ b/lib/appsignal/integrations/shoryuken.rb
@@ -2,17 +2,91 @@
module Appsignal
module Integrations
+ # @!visibility private
+ #
+ # Reads and writes W3C trace context the way OpenTelemetry's aws-sdk
+ # instrumentation does: as SQS message attributes, using the global
+ # propagator. Staying wire-equivalent means that if both AppSignal and
+ # OpenTelemetry's aws-sdk instrumentation are active, one simply shadows the
+ # other rather than corrupting the carrier. Collector mode only.
+ module ShoryukenTraceContext
+ module_function
+
+ # SQS allows at most 10 message attributes per message. Mirror
+ # OpenTelemetry and skip propagation rather than risk the enqueue failing
+ # when the user already fills the slots.
+ MAX_MESSAGE_ATTRIBUTES = 10
+
+ # Writes each trace header as an SQS message attribute, matching the shape
+ # OpenTelemetry's aws-sdk instrumentation injects on send.
+ module MessageAttributeSetter
+ def self.set(carrier, key, value)
+ return if carrier.length >= MAX_MESSAGE_ATTRIBUTES
+
+ carrier[key] = { :string_value => value, :data_type => "String" }
+ end
+ end
+
+ # Reads a trace header back out of a message attribute. Works both for the
+ # plain hash we inject and for the `Aws::SQS::Types::MessageAttributeValue`
+ # struct delivered on receive, since both respond to `[:string_value]` /
+ # `[:data_type]`.
+ module MessageAttributeGetter
+ def self.get(carrier, key)
+ attribute = carrier[key]
+ attribute[:string_value] if attribute && attribute[:data_type] == "String"
+ end
+ end
+
+ # Read the incoming context off a message's SQS message attributes so the
+ # transaction links back to the enqueuer. Returns an
+ # `OpenTelemetry::Context`, or `nil` outside collector mode.
+ def extract(message_attributes)
+ Appsignal::OpenTelemetry.if_started do
+ ::OpenTelemetry.propagation.extract(
+ message_attributes || {},
+ :getter => MessageAttributeGetter
+ )
+ end
+ end
+
+ # Write the current trace context into the outgoing send `options`.
+ # Injects into a scratch carrier first and merges it into the message
+ # attributes only when something was written, so an enqueue with no active
+ # span (no transaction, or outside collector mode) leaves the options
+ # untouched -- a transparent pass-through.
+ def inject(options)
+ Appsignal::OpenTelemetry.if_started do
+ carrier = {}
+ ::OpenTelemetry.propagation.inject(carrier, :setter => MessageAttributeSetter)
+ next if carrier.empty?
+
+ options[:message_attributes] = (options[:message_attributes] || {}).merge(carrier)
+ end
+ end
+ end
+
# @!visibility private
class ShoryukenMiddleware
def call(worker_instance, queue, sqs_msg, body, &block)
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
+ batch = sqs_msg.is_a?(Array)
+
+ # Read the incoming trace context off the message so the transaction
+ # links back to the enqueuer. A batch carries messages from multiple
+ # traces with no single parent, so only single messages link back.
+ # No-op outside collector mode.
+ context = ShoryukenTraceContext.extract(sqs_msg.message_attributes) unless batch
+
+ transaction = Appsignal::Transaction.create(
+ Appsignal::Transaction::BACKGROUND_JOB,
+ :opentelemetry_context => context
+ )
Appsignal.instrument("perform_job.shoryuken", &block)
rescue Exception => error
transaction.set_error(error)
raise
ensure
- batch = sqs_msg.is_a?(Array)
attributes = fetch_attributes(batch, sqs_msg)
transaction.set_action_if_nil("#{worker_instance.class.name}#perform")
transaction.add_params_if_nil { fetch_args(batch, sqs_msg, body) }
@@ -73,7 +147,9 @@ def fetch_args(batch, sqs_msg, body)
end
# Shoryuken client middleware that records an `enqueue.shoryuken` event so
- # the enqueue shows up under the active transaction.
+ # the enqueue shows up under the active transaction (both modes), and in
+ # collector mode writes the current trace context onto the outgoing message
+ # so the job that later performs links back to it.
#
# Like all AppSignal events, this only records when there's an active
# transaction (e.g. enqueuing from within a web request or another job). An
@@ -81,13 +157,24 @@ def fetch_args(batch, sqs_msg, body)
#
# @!visibility private
class ShoryukenClientMiddleware
- def call(options, &block)
+ def call(options)
# Under Active Job the enqueue is already recorded as an
- # `enqueue.active_job` event, so skip recording it again here.
- return yield if Appsignal::Transaction.current? &&
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
+ # `enqueue.active_job` event, so skip recording it again here. The trace
+ # context is still injected so the performed job links back.
+ if Appsignal::Transaction.current? &&
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
+ ShoryukenTraceContext.inject(options)
+ return yield
+ end
- Appsignal.instrument("enqueue.shoryuken", enqueue_title(options), &block)
+ Appsignal.instrument(
+ "enqueue.shoryuken",
+ enqueue_title(options),
+ :opentelemetry_kind => :producer
+ ) do
+ ShoryukenTraceContext.inject(options)
+ yield
+ end
end
private
diff --git a/lib/appsignal/integrations/sidekiq.rb b/lib/appsignal/integrations/sidekiq.rb
index 98a8e0ab1..861ad6dbe 100644
--- a/lib/appsignal/integrations/sidekiq.rb
+++ b/lib/appsignal/integrations/sidekiq.rb
@@ -49,23 +49,35 @@ def call(exception, sidekiq_context, _sidekiq_config = nil)
end
end
- # Sidekiq client middleware that runs on enqueue. Records an
- # `enqueue.sidekiq` event so the enqueue shows up under the active
- # transaction.
+ # Client middleware that runs on enqueue. Records an `enqueue.sidekiq`
+ # event so the enqueue shows up under the active transaction (both modes),
+ # and in collector mode writes the trace context onto the job hash so the
+ # job that later performs links back to it.
#
# Like all AppSignal events, this only records when there's an active
- # transaction (e.g. enqueuing from within a web request or another job). An
- # enqueue with no transaction is a transparent pass-through.
+ # transaction (e.g. enqueuing from within a web request or another job).
+ # An enqueue with no transaction is a transparent pass-through.
#
# @!visibility private
class SidekiqClientMiddleware
- def call(_worker_class, job, _queue, _redis_pool, &block)
+ def call(_worker_class, job, _queue, _redis_pool)
# Under Active Job the enqueue is already recorded as an
- # `enqueue.active_job` event, so skip recording it again here.
- return yield if Appsignal::Transaction.current? &&
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
+ # `enqueue.active_job` event, so skip recording it again here. The trace
+ # context is still injected so the performed job links back.
+ if Appsignal::Transaction.current? &&
+ Appsignal::Transaction.current.job_enqueue_events_suppressed?
+ Appsignal::OpenTelemetry.inject_context(job)
+ return yield
+ end
- Appsignal.instrument("enqueue.sidekiq", "enqueue #{job["class"]} job", &block)
+ Appsignal.instrument(
+ "enqueue.sidekiq",
+ "enqueue #{job["class"]} job",
+ :opentelemetry_kind => :producer
+ ) do
+ Appsignal::OpenTelemetry.inject_context(job)
+ yield
+ end
end
end
@@ -76,7 +88,7 @@ class SidekiqMiddleware
EXCLUDED_JOB_KEYS = %w[
args backtrace class created_at enqueued_at error_backtrace error_class
error_message failed_at jid retried_at retry wrapped cattr tags retry_for
- unique_for
+ unique_for traceparent tracestate __otel_headers
].freeze
def self.sidekiq8?
@@ -88,7 +100,12 @@ def self.sidekiq8?
def call(_worker, item, _queue, &block)
job_status = nil
action_name = formatted_action_name(item)
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
+ # Read trace context off the job so the transaction links back to the
+ # enqueuer. No-op outside collector mode.
+ transaction = Appsignal::Transaction.create(
+ Appsignal::Transaction::BACKGROUND_JOB,
+ :opentelemetry_context => Appsignal::OpenTelemetry.extract_job_context(item)
+ )
transaction.set_action_if_nil(action_name)
formatted_metadata(item).each do |key, value|
diff --git a/lib/appsignal/integrations/webmachine.rb b/lib/appsignal/integrations/webmachine.rb
index 884143053..cf5381c80 100644
--- a/lib/appsignal/integrations/webmachine.rb
+++ b/lib/appsignal/integrations/webmachine.rb
@@ -10,7 +10,16 @@ def run
if has_parent_transaction
Appsignal::Transaction.current
else
- Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST)
+ # Read the incoming trace context off the request headers so the
+ # transaction continues the upstream trace. No-op outside collector
+ # mode. Webmachine isn't Rack: `request.headers` is a case-insensitive
+ # `Webmachine::Headers`, so the default getter reads it directly.
+ Appsignal::Transaction.create(
+ Appsignal::Transaction::HTTP_REQUEST,
+ :opentelemetry_context => Appsignal::OpenTelemetry.if_started do
+ ::OpenTelemetry.propagation.extract(request.headers)
+ end
+ )
end
begin
diff --git a/lib/appsignal/logger.rb b/lib/appsignal/logger.rb
index c912e5401..e41335ae8 100644
--- a/lib/appsignal/logger.rb
+++ b/lib/appsignal/logger.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "logger"
-require "set"
module Appsignal
# Logger that flushes logs to the AppSignal logging service.
@@ -55,6 +54,8 @@ def to_proc
# @!visibility private
AUTODETECT = 3
# @!visibility private
+ FORMATS = [PLAINTEXT, LOGFMT, JSON, AUTODETECT].freeze
+ # @!visibility private
SEVERITY_MAP = {
DEBUG => 2,
INFO => 3,
@@ -83,7 +84,7 @@ def initialize(group, level: INFO, format: AUTODETECT, attributes: {})
@group = group
@level = level
@silenced = false
- @format = format
+ @format = validated_format(format)
@mutex = Mutex.new
@default_attributes = attributes
@appsignal_attributes = attributes
@@ -145,13 +146,7 @@ def add(severity, message = nil, group = nil, &block)
message = formatter.call(severity, Time.now, group, message) if formatter
- Appsignal::Extension.log(
- group,
- SEVERITY_MAP.fetch(severity, 0),
- @format,
- message.to_s,
- Appsignal::Utils::Data.generate(appsignal_attributes)
- )
+ Appsignal::Backends.logger.emit(group, severity, @format, message.to_s, appsignal_attributes)
false
end
@@ -299,5 +294,14 @@ def add_with_attributes(severity, message, group, attributes, &block)
ensure
@appsignal_attributes = default_attributes
end
+
+ def validated_format(format)
+ return format if FORMATS.include?(format)
+
+ Appsignal.internal_logger.warn(
+ "Unknown Appsignal::Logger format #{format.inspect}; falling back to AUTODETECT"
+ )
+ AUTODETECT
+ end
end
end
diff --git a/lib/appsignal/logger/extension_backend.rb b/lib/appsignal/logger/extension_backend.rb
new file mode 100644
index 000000000..3ab716d54
--- /dev/null
+++ b/lib/appsignal/logger/extension_backend.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Appsignal
+ class Logger < ::Logger
+ # @!visibility private
+ #
+ # Routes Appsignal::Logger emits through the AppSignal C-extension,
+ # which forwards them to the agent. This is the default backend used
+ # when collector mode is not active.
+ module ExtensionBackend
+ class << self
+ def emit(group, severity, format, message, attributes)
+ Appsignal::Extension.log(
+ group,
+ SEVERITY_MAP.fetch(severity, 0),
+ format,
+ message,
+ Appsignal::Utils::Data.generate(attributes)
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/logger/opentelemetry_backend.rb b/lib/appsignal/logger/opentelemetry_backend.rb
new file mode 100644
index 000000000..1cf4f62be
--- /dev/null
+++ b/lib/appsignal/logger/opentelemetry_backend.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require "appsignal/opentelemetry/attributes"
+
+module Appsignal
+ class Logger < ::Logger
+ # @!visibility private
+ #
+ # Routes Appsignal::Logger emits through the OpenTelemetry logs SDK
+ # using the logger provider configured at `Appsignal.start` time when
+ # collector mode is active.
+ #
+ # Each emit attaches two well-known attributes that the AppSignal
+ # collector consumes:
+ #
+ # - `appsignal.group` — overrides the collector's default
+ # `service.name`-based grouping with the logger's `group` argument.
+ # - `appsignal.format` — the lowercase parse-format name
+ # (`plaintext`/`logfmt`/`json`/`autodetect`) the processor uses to
+ # extract structured attributes from the message body.
+ module OpenTelemetryBackend
+ # Maps Ruby `::Logger` severities to OTel SeverityNumber + the
+ # human-readable severity text.
+ OTEL_SEVERITY_MAP = {
+ ::Logger::DEBUG => [5, "DEBUG"],
+ ::Logger::INFO => [9, "INFO"],
+ ::Logger::WARN => [13, "WARN"],
+ ::Logger::ERROR => [17, "ERROR"],
+ ::Logger::FATAL => [21, "FATAL"]
+ }.freeze
+
+ # Maps the integer parse-format flag on `Appsignal::Logger` to the
+ # lowercase string the AppSignal collector and processor share.
+ FORMAT_NAMES = {
+ Appsignal::Logger::PLAINTEXT => "plaintext",
+ Appsignal::Logger::LOGFMT => "logfmt",
+ Appsignal::Logger::JSON => "json",
+ Appsignal::Logger::AUTODETECT => "autodetect"
+ }.freeze
+
+ MUTEX = Mutex.new
+
+ class << self
+ def emit(group, severity, format, message, attributes)
+ number, text = OTEL_SEVERITY_MAP.fetch(severity, [0, nil])
+ otel_attributes = Appsignal::OpenTelemetry::Attributes.format(attributes)
+ otel_attributes["appsignal.group"] = group.to_s
+ otel_attributes["appsignal.format"] = FORMAT_NAMES.fetch(format, "autodetect")
+ logger.on_emit(
+ :severity_number => number,
+ :severity_text => text,
+ :body => message,
+ :attributes => otel_attributes
+ )
+ end
+
+ # @!visibility private
+ #
+ # Test-only. Drops the cached logger so the next call re-resolves
+ # `OpenTelemetry.logger_provider`.
+ def reset!
+ MUTEX.synchronize { @logger = nil }
+ end
+
+ private
+
+ # Double-checked locking: read the cached logger without the
+ # mutex on the hot path, take the lock and re-check only on the
+ # first call.
+ def logger
+ @logger || MUTEX.synchronize do
+ @logger ||= ::OpenTelemetry.logger_provider.logger(:name => "appsignal-logger")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/metrics/extension_backend.rb b/lib/appsignal/metrics/extension_backend.rb
new file mode 100644
index 000000000..ad2d63909
--- /dev/null
+++ b/lib/appsignal/metrics/extension_backend.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Appsignal
+ module Metrics
+ # @!visibility private
+ #
+ # Routes custom metric helper calls through the AppSignal C-extension,
+ # which forwards them to the agent. This is the default backend used
+ # when collector mode is not active.
+ module ExtensionBackend
+ class << self
+ def set_gauge(name, value, tags)
+ Appsignal::Extension.set_gauge(
+ name.to_s,
+ value.to_f,
+ Appsignal::Utils::Data.generate(tags)
+ )
+ rescue RangeError
+ Appsignal.internal_logger
+ .warn("The gauge value '#{value}' for metric '#{name}' is too big")
+ end
+
+ def increment_counter(name, value, tags)
+ Appsignal::Extension.increment_counter(
+ name.to_s,
+ value.to_f,
+ Appsignal::Utils::Data.generate(tags)
+ )
+ rescue RangeError
+ Appsignal.internal_logger
+ .warn("The counter value '#{value}' for metric '#{name}' is too big")
+ end
+
+ def add_distribution_value(name, value, tags)
+ Appsignal::Extension.add_distribution_value(
+ name.to_s,
+ value.to_f,
+ Appsignal::Utils::Data.generate(tags)
+ )
+ rescue RangeError
+ Appsignal.internal_logger
+ .warn("The distribution value '#{value}' for metric '#{name}' is too big")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/metrics/opentelemetry_backend.rb b/lib/appsignal/metrics/opentelemetry_backend.rb
new file mode 100644
index 000000000..188a73ae1
--- /dev/null
+++ b/lib/appsignal/metrics/opentelemetry_backend.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require "appsignal/opentelemetry/attributes"
+
+module Appsignal
+ module Metrics
+ # @!visibility private
+ #
+ # Routes custom metric helper calls through the OpenTelemetry metrics
+ # SDK using the meter provider configured at `Appsignal.start` time when
+ # collector mode is active. Mirrors the Python integration's
+ # `appsignal/metrics.py`:
+ #
+ # - `set_gauge` uses a synchronous OTel Gauge.
+ # - `increment_counter` uses an UpDownCounter so negative increments
+ # work (Counter would reject them).
+ # - `add_distribution_value` uses a Histogram.
+ #
+ # Instruments are created once per name and cached: the OTel SDK logs a
+ # "duplicate instrument registration" warning and swaps the instrument
+ # if `create_*` is called again for the same name. Tags attach at record
+ # time, not at instrument creation time.
+ module OpenTelemetryBackend
+ MUTEX = Mutex.new
+
+ class << self
+ def set_gauge(name, value, tags)
+ instrument(:gauge, name).record(
+ value.to_f,
+ :attributes => Appsignal::OpenTelemetry::Attributes.format(tags)
+ )
+ end
+
+ def increment_counter(name, value, tags)
+ instrument(:up_down_counter, name).add(
+ value.to_f,
+ :attributes => Appsignal::OpenTelemetry::Attributes.format(tags)
+ )
+ end
+
+ def add_distribution_value(name, value, tags)
+ instrument(:histogram, name).record(
+ value.to_f,
+ :attributes => Appsignal::OpenTelemetry::Attributes.format(tags)
+ )
+ end
+
+ # @!visibility private
+ #
+ # Test-only. Drops the cached meter and instruments so the next
+ # call re-resolves `OpenTelemetry.meter_provider`.
+ def reset!
+ MUTEX.synchronize do
+ @meter = nil
+ @gauges = nil
+ @counters = nil
+ @histograms = nil
+ end
+ end
+
+ private
+
+ # Fetch the named instrument, creating and caching it on first use.
+ # The lookup-or-create runs under the mutex so two concurrent
+ # first-time calls don't both create the instrument (which would
+ # make the SDK log a duplicate-registration warning).
+ def instrument(kind, name)
+ name = name.to_s
+ MUTEX.synchronize do
+ case kind
+ when :gauge
+ (@gauges ||= {})[name] ||= meter.create_gauge(name)
+ when :up_down_counter
+ (@counters ||= {})[name] ||= meter.create_up_down_counter(name)
+ when :histogram
+ (@histograms ||= {})[name] ||= meter.create_histogram(name)
+ end
+ end
+ end
+
+ # Only called from `instrument` while the mutex is held, so the plain
+ # memoisation needs no extra locking of its own.
+ def meter
+ @meter ||= ::OpenTelemetry.meter_provider.meter("appsignal-helpers")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/opentelemetry.rb b/lib/appsignal/opentelemetry.rb
new file mode 100644
index 000000000..77527b27b
--- /dev/null
+++ b/lib/appsignal/opentelemetry.rb
@@ -0,0 +1,291 @@
+# frozen_string_literal: true
+
+require "appsignal/opentelemetry/attributes"
+require "appsignal/opentelemetry/dependencies"
+
+module Appsignal
+ # @!visibility private
+ module OpenTelemetry
+ class << self
+ # Configure the global OpenTelemetry SDK to export OTLP/HTTP protobuf to
+ # the collector endpoint defined in `config[:collector_endpoint]`.
+ #
+ # Lazily requires the OpenTelemetry SDK and OTLP exporter gems so that
+ # users not in collector mode do not pay the load cost.
+ #
+ # Sets `@started` to `true` on success, `false` if the SDK gems can't be
+ # loaded or any other error occurs. Callers can read this via
+ # {.started?} to decide whether to route through the OTel backends.
+ def configure(config)
+ # The OTel Ruby SDK exposes no programmatic knob for the default
+ # aggregation temporality; this env var is the only way to set
+ # it. We pick `:delta` to match the Python integration. (Note:
+ # the Ruby SDK keeps `UpDownCounter` cumulative regardless of
+ # this preference, per the OTel spec.)
+ ENV["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] ||= "delta"
+
+ # With the metrics and logs SDK gems loaded, `SDK.configure` below
+ # auto-installs a metrics reader and a log processor from these env
+ # vars (both default to "otlp", pointed at the default OTLP
+ # endpoint), each with its own background thread. We replace both
+ # providers with our own right after, which would orphan those
+ # threads -- unreachable by any shutdown. Suppress the auto-setup;
+ # the exporters we build below are the only ones that should run.
+ # Set unconditionally: a user-set "otlp" here would otherwise slip
+ # past and reintroduce the orphaned threads.
+ ENV["OTEL_METRICS_EXPORTER"] = "none"
+ ENV["OTEL_LOGS_EXPORTER"] = "none"
+
+ require_sdk_gems
+
+ # The OpenTelemetry gems are optional and installed by the user (not
+ # declared in the gemspec). If they're present but older than the
+ # versions we support, fall back to the agent rather than booting an
+ # SDK that may misbehave (e.g. a metrics SDK without fork hooks).
+ return unless required_gem_versions_met?
+
+ endpoint = config[:collector_endpoint].to_s.sub(%r{/+\z}, "")
+ # Merge with the SDK's default resource so all three signal types
+ # carry the same `telemetry.sdk.*` and `process.*` attributes that
+ # `SDK.configure` would have added on its own. `MeterProvider` and
+ # `LoggerProvider` take a `resource:` kwarg that replaces (not
+ # merges), so we do the merge ourselves and use the same merged
+ # resource for the tracer provider to keep all three in sync.
+ resource = ::OpenTelemetry::SDK::Resources::Resource.default.merge(build_resource(config))
+
+ ::OpenTelemetry::SDK.configure do |c|
+ c.resource = resource
+ c.add_span_processor(
+ ::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
+ ::OpenTelemetry::Exporter::OTLP::Exporter.new(
+ :endpoint => "#{endpoint}/v1/traces"
+ )
+ )
+ )
+ end
+
+ # Wrap the OTLP MetricsExporter in a PeriodicMetricReader so that
+ # `MeterProvider#force_flush` actually triggers an export. The OTLP
+ # exporter itself is also a MetricReader but its inherited
+ # `force_flush` is a no-op.
+ ::OpenTelemetry.meter_provider =
+ ::OpenTelemetry::SDK::Metrics::MeterProvider.new(:resource => resource)
+ ::OpenTelemetry.meter_provider.add_metric_reader(
+ ::OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(
+ :exporter => ::OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new(
+ :endpoint => "#{endpoint}/v1/metrics"
+ )
+ )
+ )
+
+ ::OpenTelemetry.logger_provider =
+ ::OpenTelemetry::SDK::Logs::LoggerProvider.new(:resource => resource)
+ ::OpenTelemetry.logger_provider.add_log_record_processor(
+ ::OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(
+ ::OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(
+ :endpoint => "#{endpoint}/v1/logs"
+ )
+ )
+ )
+
+ @started = true
+ rescue LoadError => e
+ @started = false
+ Appsignal::Utils::StdoutAndLoggerMessage.error(
+ "Cannot configure OpenTelemetry SDK for collector mode: #{e.class}: #{e.message}"
+ )
+ rescue => e
+ @started = false
+ Appsignal::Utils::StdoutAndLoggerMessage.error(
+ "Error configuring OpenTelemetry SDK for collector mode: " \
+ "#{e.class}: #{e.message}\n#{e.backtrace&.join("\n")}"
+ )
+ end
+
+ # Whether {.configure} has successfully booted the OpenTelemetry SDK
+ # for this process. Returns `false` before {.configure} runs and
+ # `false` if it ran but raised.
+ def started?
+ defined?(@started) ? @started : false
+ end
+
+ # Write the current trace context onto an outgoing carrier (HTTP request,
+ # job hash, ...) using the globally configured propagator (W3C
+ # TraceContext + baggage). Called by integrations on the emit side so a
+ # downstream service joins the same trace.
+ #
+ # No-op unless the SDK has booted ({.started?}); outside collector mode
+ # there is no context to propagate. The carrier is injected from whatever
+ # span is current at call time -- inside an `Appsignal.instrument` block
+ # that is the AppSignal event span, so the written `traceparent` reflects
+ # it.
+ def inject_context(carrier)
+ if_started do
+ ::OpenTelemetry.propagation.inject(carrier)
+ end
+ end
+
+ # Read the trace context off an incoming Rack request env using the
+ # globally configured propagator, so an AppSignal transaction created for
+ # the request can continue the upstream trace. Returns an
+ # `OpenTelemetry::Context` (its current span is the remote parent), or
+ # `nil` when the SDK has not booted -- outside collector mode there is
+ # nothing to continue. `rack_env_getter` reads the `HTTP_*`-mangled header
+ # names Rack puts in the env.
+ def extract_rack_context(env)
+ if_started do
+ ::OpenTelemetry.propagation.extract(
+ env,
+ :getter => ::OpenTelemetry::Common::Propagation.rack_env_getter
+ )
+ end
+ end
+
+ # Read the trace context off an incoming background job hash, so a
+ # transaction created for the job can link back to the enqueuer. Returns
+ # an `OpenTelemetry::Context`, or `nil` when the SDK has not booted.
+ #
+ # Reads both carriers a job can arrive with: top-level `traceparent` /
+ # `tracestate` keys (how OpenTelemetry's Sidekiq instrumentation injects)
+ # and a nested `__otel_headers` (how its ActiveJob instrumentation does).
+ # ActiveJob runs the headers through ActiveJob's argument serializer, so
+ # `__otel_headers` arrives as an array of `[key, value]` pairs, not a hash;
+ # accept both shapes. The nested keys win when present, since ActiveJob is
+ # the outer, more specific layer.
+ def extract_job_context(item)
+ if_started do
+ carrier = item
+ nested = item["__otel_headers"]
+ nested = nested.to_h if otel_header_pairs?(nested)
+ carrier = item.merge(nested) if nested.is_a?(Hash)
+ ::OpenTelemetry.propagation.extract(carrier)
+ end
+ end
+
+ # Run `block` only when the OpenTelemetry SDK has booted (collector mode),
+ # returning its result; a no-op returning `nil` otherwise. The block can
+ # touch the OTel SDK freely -- it only runs when the SDK is loaded.
+ #
+ # This is the gate every integration's OTel-specific work goes through, so
+ # integration-specific carrier/getter/setter logic lives in the
+ # integration rather than as a bespoke helper here.
+ def if_started
+ return unless started?
+
+ yield
+ end
+
+ # @!visibility private
+ #
+ # Test-only. Drops the started flag so subsequent tests start from a
+ # clean slate; does not touch the global `::OpenTelemetry` providers.
+ def reset!
+ @started = false
+ end
+
+ # Flush and shut down the OpenTelemetry SDK providers booted by
+ # {.configure}. Called from `Appsignal.stop` so buffered
+ # metrics/logs/spans don't get dropped on exit.
+ def shutdown
+ return unless started?
+
+ ::OpenTelemetry.tracer_provider&.shutdown
+ ::OpenTelemetry.meter_provider&.shutdown
+ ::OpenTelemetry.logger_provider&.shutdown
+ rescue => e
+ Appsignal.internal_logger.error(
+ "Error shutting down OpenTelemetry SDK: #{e.class}: #{e.message}"
+ )
+ end
+
+ # Build the OpenTelemetry Resource that carries AppSignal config to the
+ # collector. Attributes whose underlying option is nil or an empty array
+ # are omitted so the collector applies its own defaults.
+ def build_resource(config)
+ revision = config[:revision].to_s.empty? ? "unknown" : config[:revision]
+ service_name = config[:service_name].to_s.empty? ? "unknown" : config[:service_name]
+ host_name = config[:hostname].to_s.empty? ? "unknown" : config[:hostname]
+
+ attrs = {
+ "appsignal.config.name" => config[:name],
+ "appsignal.config.environment" => config.env,
+ "appsignal.config.push_api_key" => config[:push_api_key],
+ "appsignal.config.revision" => revision,
+ "appsignal.config.language_integration" => "ruby",
+ "service.name" => service_name,
+ "host.name" => host_name,
+ "appsignal.config.filter_attributes" => config[:filter_attributes],
+ "appsignal.config.filter_function_parameters" => config[:filter_function_parameters],
+ "appsignal.config.filter_request_query_parameters" =>
+ config[:filter_request_query_parameters],
+ "appsignal.config.filter_request_payload" => config[:filter_request_payload],
+ "appsignal.config.filter_request_session_data" => config[:filter_session_data],
+ "appsignal.config.ignore_actions" => config[:ignore_actions],
+ "appsignal.config.ignore_errors" => config[:ignore_errors],
+ "appsignal.config.ignore_namespaces" => config[:ignore_namespaces],
+ "appsignal.config.response_headers" => config[:response_headers],
+ "appsignal.config.request_headers" => config[:request_headers],
+ "appsignal.config.send_function_parameters" => config[:send_function_parameters],
+ "appsignal.config.send_request_query_parameters" =>
+ config[:send_request_query_parameters],
+ "appsignal.config.send_request_payload" => config[:send_request_payload],
+ "appsignal.config.send_request_session_data" => config[:send_session_data]
+ }
+ attrs.reject! { |_, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }
+ ::OpenTelemetry::SDK::Resources::Resource.create(attrs)
+ end
+
+ private
+
+ # Whether a `__otel_headers` value is the array-of-`[key, value]`-pairs
+ # shape produced by ActiveJob's argument serializer, so it can be turned
+ # into a hash carrier. Anything else (including a malformed array) is left
+ # alone rather than raising on `to_h` inside a job perform.
+ def otel_header_pairs?(value)
+ value.is_a?(Array) && value.all? { |pair| pair.is_a?(Array) && pair.size == 2 }
+ end
+
+ # The optional OpenTelemetry gems, required lazily so users not in
+ # collector mode don't pay the load cost. A missing gem raises LoadError,
+ # caught by {.configure}.
+ def require_sdk_gems
+ require "opentelemetry/sdk"
+ require "opentelemetry-common"
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry-metrics-sdk"
+ require "opentelemetry-exporter-otlp-metrics"
+ require "opentelemetry-logs-sdk"
+ require "opentelemetry-exporter-otlp-logs"
+ end
+
+ # Checks the installed OpenTelemetry gem versions against {REQUIRED_GEMS}.
+ # On a shortfall, warns and flags the SDK as not started so the caller
+ # falls back to the agent; returns whether all requirements are met.
+ def required_gem_versions_met?
+ unmet = unmet_gem_requirements
+ return true if unmet.empty?
+
+ @started = false
+ Appsignal::Utils::StdoutAndLoggerMessage.warning(
+ "Cannot enable collector mode: the installed OpenTelemetry gems are " \
+ "older than the minimum supported versions (#{unmet.join(", ")}). " \
+ "Update them in your Gemfile; the AppSignal agent will be used instead."
+ )
+ false
+ end
+
+ # Descriptions of the OpenTelemetry gems that are missing or older than
+ # the minimum version in {REQUIRED_GEMS}. Empty when all are satisfied.
+ def unmet_gem_requirements
+ REQUIRED_GEMS.filter_map do |name, minimum|
+ spec = Gem.loaded_specs[name]
+ if spec.nil?
+ "#{name} (not installed)"
+ elsif spec.version < Gem::Version.new(minimum)
+ "#{name} #{spec.version} (requires >= #{minimum})"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/opentelemetry/attributes.rb b/lib/appsignal/opentelemetry/attributes.rb
new file mode 100644
index 000000000..6a664ef10
--- /dev/null
+++ b/lib/appsignal/opentelemetry/attributes.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Appsignal
+ module OpenTelemetry
+ # @!visibility private
+ #
+ # Coerces user-supplied tag hashes into a shape the OpenTelemetry SDK
+ # accepts as attribute values: string keys, and values restricted to
+ # the primitive types the OTLP spec allows. Anything else falls back
+ # to `to_s`. Shared by the metric and log backends so both behave
+ # identically.
+ module Attributes
+ class << self
+ def format(attrs)
+ attrs.each_with_object({}) do |(key, value), result|
+ result[key.to_s] = format_value(value)
+ end
+ end
+
+ private
+
+ def format_value(value)
+ case value
+ when String, Integer, Float, TrueClass, FalseClass then value
+ else value.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/opentelemetry/dependencies.rb b/lib/appsignal/opentelemetry/dependencies.rb
new file mode 100644
index 000000000..ce13ebfd4
--- /dev/null
+++ b/lib/appsignal/opentelemetry/dependencies.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Appsignal
+ module OpenTelemetry
+ # @!visibility private
+ #
+ # The OpenTelemetry gems collector mode depends on, mapped to the minimum
+ # version we support. These gems are *not* declared in the gemspec: they
+ # are optional and only required when collector mode is active. Apps that
+ # opt into collector mode install them into their own bundle (see the
+ # collector documentation).
+ #
+ # The floors are the first releases that support Ruby 3.1 (the family-wide
+ # "3.1 min version" train), except `opentelemetry-metrics-sdk`, which is
+ # floored at the release that added `Process._fork`-based fork recovery for
+ # the periodic metric reader. That fork support is why collector mode
+ # itself requires Ruby 3.1 (see `MIN_RUBY_VERSION_FOR_COLLECTOR_MODE` in
+ # `Appsignal::Config`).
+ #
+ # This file must stay free of any other dependency so it can be required
+ # directly from a Gemfile (see `gemfiles/collector.rb`) and from the
+ # runtime version gate in `Appsignal::OpenTelemetry.configure` without
+ # loading the rest of the gem.
+ REQUIRED_GEMS = {
+ "opentelemetry-sdk" => "1.8.0",
+ "opentelemetry-common" => "0.20.0",
+ "opentelemetry-metrics-sdk" => "0.7.1",
+ "opentelemetry-logs-sdk" => "0.2.0",
+ "opentelemetry-exporter-otlp" => "0.30.0",
+ "opentelemetry-exporter-otlp-metrics" => "0.4.0",
+ "opentelemetry-exporter-otlp-logs" => "0.2.0"
+ }.freeze
+ end
+end
diff --git a/lib/appsignal/rack/abstract_middleware.rb b/lib/appsignal/rack/abstract_middleware.rb
index 8761559d5..6758aedfb 100644
--- a/lib/appsignal/rack/abstract_middleware.rb
+++ b/lib/appsignal/rack/abstract_middleware.rb
@@ -33,7 +33,10 @@ def call(env)
if wrapped_instrumentation
env[Appsignal::Rack::APPSIGNAL_TRANSACTION]
else
- Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST)
+ Appsignal::Transaction.create(
+ Appsignal::Transaction::HTTP_REQUEST,
+ :opentelemetry_context => Appsignal::OpenTelemetry.extract_rack_context(env)
+ )
end
unless wrapped_instrumentation
diff --git a/lib/appsignal/rack/event_handler.rb b/lib/appsignal/rack/event_handler.rb
index 77c299dd6..6ae605c02 100644
--- a/lib/appsignal/rack/event_handler.rb
+++ b/lib/appsignal/rack/event_handler.rb
@@ -63,7 +63,10 @@ def on_start(request, _response)
request.env[APPSIGNAL_EVENT_HANDLER_ID] ||= id
return unless request_handler?(request.env[APPSIGNAL_EVENT_HANDLER_ID])
- transaction = Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST)
+ transaction = Appsignal::Transaction.create(
+ Appsignal::Transaction::HTTP_REQUEST,
+ :opentelemetry_context => Appsignal::OpenTelemetry.extract_rack_context(request.env)
+ )
transaction.start_event
request.env[APPSIGNAL_TRANSACTION] = transaction
diff --git a/lib/appsignal/transaction.rb b/lib/appsignal/transaction.rb
index de328628d..7e9714f79 100644
--- a/lib/appsignal/transaction.rb
+++ b/lib/appsignal/transaction.rb
@@ -29,7 +29,7 @@ class << self
#
# @param namespace [String] Namespace of the to be created transaction.
# @return [Transaction]
- def create(namespace)
+ def create(namespace, opentelemetry_context: nil)
# Reset the transaction if it was already completed but not cleared
if Thread.current[:appsignal_transaction]&.completed?
Thread.current[:appsignal_transaction] = nil
@@ -37,7 +37,12 @@ def create(namespace)
if Thread.current[:appsignal_transaction].nil?
# If not, start a new transaction
- set_current_transaction(Appsignal::Transaction.new(namespace))
+ set_current_transaction(
+ Appsignal::Transaction.new(
+ namespace,
+ :opentelemetry_context => opentelemetry_context
+ )
+ )
else
transaction = current
# Otherwise, log the issue about trying to start another transaction
@@ -160,7 +165,7 @@ def last_errors
# @param namespace [String] Namespace of the to be created transaction.
# @see create
# @!visibility private
- def initialize(namespace, id: SecureRandom.uuid, ext: nil)
+ def initialize(namespace, id: SecureRandom.uuid, backend: nil, opentelemetry_context: nil)
@transaction_id = id
@action = nil
@namespace = namespace
@@ -168,7 +173,6 @@ def initialize(namespace, id: SecureRandom.uuid, ext: nil)
@discarded = false
@completed = false
@tags = {}
- @breadcrumbs = []
@store = Hash.new { |hash, key| hash[key] = {} }
@error_blocks = Hash.new { |hash, key| hash[key] = [] }
@is_duplicate = false
@@ -179,11 +183,11 @@ def initialize(namespace, id: SecureRandom.uuid, ext: nil)
@headers = Appsignal::SampleData.new(:headers, Hash)
@custom_data = Appsignal::SampleData.new(:custom_data)
- @ext = ext || Appsignal::Extension.start_transaction(
+ @backend = backend || Appsignal::Backends.transaction.new(
@transaction_id,
@namespace,
- 0
- ) || Appsignal::Extension::MockTransaction.new
+ :opentelemetry_context => opentelemetry_context
+ )
run_after_create_hooks
end
@@ -205,9 +209,22 @@ def completed?
# @!visibility private
def complete
+ # Completing is idempotent: a transaction can be completed explicitly and
+ # then again by a `complete_current!` cleanup path. Re-running would, for a
+ # multi-error transaction, re-record the extra errors (a second duplicate
+ # in agent mode, or an event on an already-finished span in collector mode).
+ return if completed?
+
if discarded?
Appsignal.internal_logger.debug "Skipping transaction '#{transaction_id}' " \
"because it was manually discarded."
+ # Let the backend tear itself down. The agent backend drops the
+ # transaction (nothing is sent); the OpenTelemetry backend still
+ # finishes and exports the root span, but flags it with
+ # `appsignal.ignore_subtrace` so the collector ignores the subtrace.
+ # `@completed` stays false either way: a discarded transaction was
+ # never reported.
+ @backend.discard
return
end
@@ -220,39 +237,17 @@ def complete
unless duplicate?
self.class.last_errors = @error_blocks.keys
- should_sample = @ext.finish(0)
+ should_sample = @backend.finish
end
- @error_blocks.each do |error, blocks|
- # Ignore the error that is already set in this transaction.
- next if error == @error_set
-
- duplicate.tap do |transaction|
- # In the duplicate transaction for each error, set an error
- # with a block that calls all the blocks set for that error
- # in the original transaction.
- transaction.internal_set_error(error) do
- blocks.each { |block| block.call(transaction) }
- end
-
- transaction.complete
- end
- end
-
- if @error_set && @error_blocks[@error_set].any?
- self.class.with_transaction(self) do
- @error_blocks[@error_set].each do |block|
- block.call(self)
- end
- end
- end
+ report_errors
run_before_complete_hooks
sample_data if should_sample
@completed = true
- @ext.complete
+ @backend.complete
end
# @!visibility private
@@ -519,14 +514,16 @@ def add_breadcrumb(category, action, message = "", metadata = {}, time = Time.no
return
end
- @breadcrumbs.push(
+ # The backend owns how breadcrumbs are stored: the agent backend buffers
+ # them and flushes at completion, the OpenTelemetry backend emits each as a
+ # span event right away (by completion its target span has finished).
+ @backend.add_breadcrumb(
:time => time.to_i,
:category => category,
:action => action,
:message => message,
:metadata => metadata
)
- @breadcrumbs = @breadcrumbs.last(BREADCRUMB_LIMIT)
end
# Set an action name for the transaction.
@@ -543,7 +540,7 @@ def set_action(action)
return unless action
@action = action
- @ext.set_action(action)
+ @backend.set_action(action)
end
# Set an action name only if there is no current action set.
@@ -591,7 +588,7 @@ def set_namespace(namespace)
return unless namespace
@namespace = namespace
- @ext.set_namespace(namespace)
+ @backend.set_namespace(namespace)
end
# Set queue start time for transaction.
@@ -605,7 +602,7 @@ def set_namespace(namespace)
def set_queue_start(start)
return unless start
- @ext.set_queue_start(start)
+ @backend.set_queue_start(start)
rescue RangeError
Appsignal.internal_logger.warn("Queue start value #{start} is too big")
end
@@ -615,7 +612,7 @@ def set_metadata(key, value)
return unless key && value
return if Appsignal.config[:filter_metadata].include?(key.to_s)
- @ext.set_metadata(key, value)
+ @backend.set_metadata(key, value)
end
# @!visibility private
@@ -648,10 +645,10 @@ def add_error(error, &block)
# @!visibility private
# @see Helpers::Instrumentation#instrument
- def start_event
+ def start_event(opentelemetry_kind: nil)
return if paused?
- @ext.start_event(0)
+ @backend.start_event(:opentelemetry_kind => opentelemetry_kind)
end
# @!visibility private
@@ -659,34 +656,46 @@ def start_event
def finish_event(name, title, body, body_format = Appsignal::EventFormatter::DEFAULT)
return if paused?
- @ext.finish_event(
+ @backend.finish_event(
name,
title || BLANK,
body || BLANK,
- body_format || Appsignal::EventFormatter::DEFAULT,
- 0
+ body_format || Appsignal::EventFormatter::DEFAULT
)
end
# @!visibility private
# @see Helpers::Instrumentation#instrument
- def record_event(name, title, body, duration, body_format = Appsignal::EventFormatter::DEFAULT)
+ def record_event( # rubocop:disable Metrics/ParameterLists
+ name,
+ title,
+ body,
+ duration,
+ body_format = Appsignal::EventFormatter::DEFAULT,
+ opentelemetry_kind: nil
+ )
return if paused?
- @ext.record_event(
+ @backend.record_event(
name,
title || BLANK,
body || BLANK,
body_format || Appsignal::EventFormatter::DEFAULT,
duration,
- 0
+ :opentelemetry_kind => opentelemetry_kind
)
end
# @!visibility private
# @see Helpers::Instrumentation#instrument
- def instrument(name, title = nil, body = nil, body_format = Appsignal::EventFormatter::DEFAULT)
- start_event
+ def instrument(
+ name,
+ title = nil,
+ body = nil,
+ body_format = Appsignal::EventFormatter::DEFAULT,
+ opentelemetry_kind: nil
+ )
+ start_event(:opentelemetry_kind => opentelemetry_kind)
yield if block_given?
ensure
finish_event(name, title, body, body_format)
@@ -694,34 +703,42 @@ def instrument(name, title = nil, body = nil, body_format = Appsignal::EventForm
# @!visibility private
def to_h
- JSON.parse(@ext.to_json)
+ JSON.parse(@backend.to_json)
end
alias to_hash to_h
protected
# @!visibility private
- attr_writer :is_duplicate, :tags, :custom_data, :breadcrumbs, :params,
+ attr_writer :is_duplicate, :tags, :custom_data, :params,
:session_data, :headers
# @!visibility private
def internal_set_error(error, &block)
- _set_error(error) if @error_blocks.empty?
+ is_new_error = !@error_blocks.include?(error)
- if !@error_blocks.include?(error) && @error_blocks.length >= ERRORS_LIMIT
+ if is_new_error && @error_blocks.length >= ERRORS_LIMIT
Appsignal.internal_logger.warn "Appsignal::Transaction#add_error: Transaction has more " \
"than #{ERRORS_LIMIT} distinct errors. Only the first " \
"#{ERRORS_LIMIT} distinct errors will be reported."
return
end
+
+ if @error_blocks.empty?
+ _set_error(error)
+ elsif is_new_error && @backend.records_errors_eagerly?
+ # Record additional errors immediately so each exception event lands on
+ # the span current now, not the root span at completion. The agent
+ # backend instead reports extras as duplicate transactions.
+ _send_error_to_backend(error)
+ end
+
@error_blocks[error] << block
@error_blocks[error].compact!
end
private
- attr_reader :breadcrumbs
-
def run_after_create_hooks
self.class.after_create.each do |block|
block.call(self)
@@ -734,23 +751,99 @@ def run_before_complete_hooks
end
end
+ # Reports the errors stored on the transaction at completion, in one of two
+ # ways depending on the backend:
+ #
+ # - eager (collector): each error was already recorded as its own exception
+ # event when added, on the span current at that moment; here we only run
+ # the error blocks.
+ # - deferred (agent): the extension holds a single error, so the primary
+ # error's blocks run on this transaction and every additional error is
+ # reported as a duplicate transaction.
+ def report_errors
+ if @backend.records_errors_eagerly?
+ run_error_blocks
+ else
+ report_errors_as_duplicates
+ end
+ end
+
+ # Eager mode: the errors are already recorded, so just run their blocks.
+ # Blocks run in add-order, so a later error's block wins on a shared key, and
+ # all block-set metadata merges onto the root span. (Per-error metadata
+ # isolation is deferred -- the processor/UI does not read per-event
+ # attributes yet.)
+ def run_error_blocks
+ @error_blocks.each_value do |blocks|
+ self.class.with_transaction(self) do
+ blocks.each { |block| block.call(self) }
+ end
+ end
+ end
+
+ # Agent mode: the extension transaction holds a single error, so report each
+ # additional error as a duplicate transaction.
+ def report_errors_as_duplicates
+ @error_blocks.each do |error, blocks|
+ # Ignore the error that is already set in this transaction.
+ next if error == @error_set
+
+ duplicate.tap do |transaction|
+ # In the duplicate transaction for each error, set an error
+ # with a block that calls all the blocks set for that error
+ # in the original transaction.
+ transaction.internal_set_error(error) do
+ blocks.each { |block| block.call(transaction) }
+ end
+
+ transaction.complete
+ end
+ end
+
+ return unless @error_set && @error_blocks[@error_set].any?
+
+ self.class.with_transaction(self) do
+ @error_blocks[@error_set].each do |block|
+ block.call(self)
+ end
+ end
+ end
+
def _set_error(error)
- backtrace = cleaned_backtrace(error.backtrace)
- @ext.set_error(
+ @error_set = error
+ _send_error_to_backend(error)
+ end
+
+ # Records an error on the backend. The cause chain is walked once into
+ # neutral data ({name, message, backtrace}); each backend projects what it
+ # needs -- the agent's first-line `error_causes` sample data, or the
+ # OpenTelemetry `appsignal.error_causes` attribute. Called for the first
+ # error and, in collector mode, for each additional error as it is added.
+ def _send_error_to_backend(error)
+ causes, root_cause_missing = _error_causes(error)
+ @backend.set_error(
error.class.name,
cleaned_error_message(error),
- backtrace ? Appsignal::Utils::Data.generate(backtrace) : Appsignal::Extension.data_array_new
+ cleaned_backtrace(error.backtrace),
+ causes.map do |cause|
+ {
+ :name => cause.class.name,
+ :message => cleaned_error_message(cause),
+ :backtrace => cleaned_backtrace(cause.backtrace)
+ }
+ end,
+ root_cause_missing
)
- @error_set = error
+ end
+ # Walks the `error.cause` chain (without mutating `error`), collecting up to
+ # `ERROR_CAUSES_LIMIT` causes. Returns the causes and whether the chain was
+ # truncated (the root cause is missing).
+ def _error_causes(error)
root_cause_missing = false
-
causes = []
- while error
- error = error.cause
-
- break unless error
-
+ cause = error
+ while (cause = cause.cause)
if causes.length >= ERROR_CAUSES_LIMIT
Appsignal.internal_logger.debug "Appsignal::Transaction#add_error: Error has more " \
"than #{ERROR_CAUSES_LIMIT} error causes. Only the first #{ERROR_CAUSES_LIMIT} " \
@@ -759,55 +852,10 @@ def _set_error(error)
break
end
- causes << error
+ causes << cause
end
- causes_sample_data = causes.map do |e|
- {
- :name => e.class.name,
- :message => cleaned_error_message(e),
- :first_line => first_formatted_backtrace_line(e)
- }
- end
-
- causes_sample_data.last[:is_root_cause] = false if root_cause_missing
-
- set_sample_data(
- "error_causes",
- causes_sample_data
- )
- end
-
- BACKTRACE_REGEX =
- %r{(?[\w-]+ \(.+\) )?(?:?/?\w+?.+?):(?:?\d+)(?::in `(?.+)')?$}.freeze
- private_constant :BACKTRACE_REGEX
-
- def first_formatted_backtrace_line(error)
- backtrace = cleaned_backtrace(error.backtrace)
- first_line = backtrace&.first
- return unless first_line
-
- captures = BACKTRACE_REGEX.match(first_line)
- return unless captures
-
- captures.named_captures
- .merge("original" => first_line)
- .tap do |c|
- config = Appsignal.config
- # Strip of whitespace at the end of the gem name
- c["gem"] = c["gem"]&.strip
- # Strip the app path from the path if present
- root_path = config.root_path
- if c["path"].start_with?(root_path)
- c["path"].delete_prefix!(root_path)
- # Relative paths shouldn't start with a slash
- c["path"].delete_prefix!("/")
- end
- # Add revision for linking to the repository from the UI
- c["revision"] = config[:revision]
- # Convert line number to an integer
- c["line"] = c["line"].to_i
- end
+ [causes, root_cause_missing]
end
def set_sample_data(key, data)
@@ -820,10 +868,11 @@ def set_sample_data(key, data)
return
end
- @ext.set_sample_data(
- key.to_s,
- Appsignal::Utils::Data.generate(data)
- )
+ # Pass raw Ruby through to the backend. ExtensionBackend serializes to a
+ # C-extension `Data` object; OpenTelemetryBackend reads the Hash/Array
+ # directly. The `RuntimeError` rescue still covers ExtensionBackend's
+ # `Data.generate`, which now runs inside the backend call.
+ @backend.set_sample_data(key.to_s, data)
rescue RuntimeError => e
begin
inspected_data = data.inspect
@@ -843,7 +892,6 @@ def sample_data
:environment => sanitized_request_headers,
:session_data => sanitized_session_data,
:tags => sanitized_tags,
- :breadcrumbs => breadcrumbs,
:custom_data => custom_data
}.each do |key, data|
set_sample_data(key, data)
@@ -855,12 +903,11 @@ def duplicate
self.class.new(
namespace,
:id => new_transaction_id,
- :ext => @ext.duplicate(new_transaction_id)
+ :backend => @backend.duplicate(new_transaction_id)
).tap do |transaction|
transaction.is_duplicate = true
transaction.tags = @tags.dup
transaction.custom_data = @custom_data.dup
- transaction.breadcrumbs = @breadcrumbs.dup
transaction.params = @params.dup
transaction.session_data = @session_data.dup
transaction.headers = @headers.dup
diff --git a/lib/appsignal/transaction/base_backend.rb b/lib/appsignal/transaction/base_backend.rb
new file mode 100644
index 000000000..9b4c23b53
--- /dev/null
+++ b/lib/appsignal/transaction/base_backend.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module Appsignal
+ class Transaction
+ # @!visibility private
+ #
+ # The interface every transaction backend implements. `Appsignal::Backends
+ # .transaction` picks a concrete backend per mode -- ExtensionBackend in
+ # agent mode, OpenTelemetryBackend in collector mode. This base documents the
+ # contract; a backend that leaves a method unimplemented raises here.
+ class BaseBackend
+ # Instrumented events.
+ def start_event(opentelemetry_kind: nil)
+ raise NotImplementedError
+ end
+
+ def finish_event(_name, _title, _body, _body_format)
+ raise NotImplementedError
+ end
+
+ def record_event(_name, _title, _body, _body_format, _duration, opentelemetry_kind: nil) # rubocop:disable Metrics/ParameterLists
+ raise NotImplementedError
+ end
+
+ # Transaction metadata.
+ def set_action(_action)
+ raise NotImplementedError
+ end
+
+ def set_namespace(_namespace)
+ raise NotImplementedError
+ end
+
+ def set_metadata(_key, _value)
+ raise NotImplementedError
+ end
+
+ def set_queue_start(_start)
+ raise NotImplementedError
+ end
+
+ # Sample data (params, session, tags, ...), breadcrumbs and errors.
+ def set_sample_data(_key, _data)
+ raise NotImplementedError
+ end
+
+ def add_breadcrumb(_breadcrumb)
+ raise NotImplementedError
+ end
+
+ def set_error(_class_name, _message, _backtrace, _causes, _root_cause_missing)
+ raise NotImplementedError
+ end
+
+ # Whether the backend records each error eagerly onto one trace, or relies
+ # on the Transaction duplicating itself per error.
+ def records_errors_eagerly?
+ raise NotImplementedError
+ end
+
+ # Lifecycle.
+ def finish
+ raise NotImplementedError
+ end
+
+ def complete
+ raise NotImplementedError
+ end
+
+ def discard
+ raise NotImplementedError
+ end
+
+ # Only used when `records_errors_eagerly?` is false (agent mode). Backends
+ # that record eagerly never duplicate and leave this unimplemented.
+ def duplicate(_new_transaction_id)
+ raise NotImplementedError
+ end
+
+ def to_json # rubocop:disable Lint/ToJSON
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/transaction/extension_backend.rb b/lib/appsignal/transaction/extension_backend.rb
new file mode 100644
index 000000000..934defbd3
--- /dev/null
+++ b/lib/appsignal/transaction/extension_backend.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+module Appsignal
+ class Transaction
+ # @!visibility private
+ #
+ # The transaction backend used in agent mode. Wraps a per-transaction
+ # handle on the C extension (`Appsignal::Extension::Transaction`) and
+ # forwards every call to it.
+ #
+ # In agent mode `Appsignal::Backends.transaction` returns this class;
+ # `Appsignal::Transaction#initialize` instantiates one and stores it in
+ # `@backend`.
+ class ExtensionBackend < BaseBackend
+ # rubocop:disable Layout/LineLength
+ BACKTRACE_REGEX =
+ %r{(?[\w-]+ \(.+\) )?(?:?/?\w+?.+?):(?:?\d+)(?::in `(?.+)')?$}.freeze
+ # rubocop:enable Layout/LineLength
+
+ # @!visibility private
+ attr_writer :breadcrumbs
+
+ # `opentelemetry_context` is an incoming trace context used only in
+ # collector mode; agent mode has no notion of it, so it's ignored here.
+ def initialize(transaction_id, namespace, handle: nil, opentelemetry_context: nil) # rubocop:disable Lint/UnusedMethodArgument
+ super()
+ @handle = handle ||
+ Appsignal::Extension.start_transaction(transaction_id, namespace, 0) ||
+ Appsignal::Extension::MockTransaction.new
+ @breadcrumbs = []
+ end
+
+ # Agent mode has no span kind; `opentelemetry_kind` is ignored here.
+ def start_event(opentelemetry_kind: nil) # rubocop:disable Lint/UnusedMethodArgument
+ @handle.start_event(0)
+ end
+
+ def finish_event(name, title, body, body_format)
+ @handle.finish_event(name, title, body, body_format, 0)
+ end
+
+ # Agent mode has no span kind; `opentelemetry_kind` is ignored here.
+ def record_event(name, title, body, body_format, duration, opentelemetry_kind: nil) # rubocop:disable Lint/UnusedMethodArgument, Metrics/ParameterLists
+ @handle.record_event(name, title, body, body_format, duration, 0)
+ end
+
+ def set_action(action)
+ @handle.set_action(action)
+ end
+
+ def set_namespace(namespace)
+ @handle.set_namespace(namespace)
+ end
+
+ def set_queue_start(start)
+ @handle.set_queue_start(start)
+ end
+
+ def set_metadata(key, value)
+ @handle.set_metadata(key, value)
+ end
+
+ # `data` is a raw Ruby Hash/Array; the C extension wants a `Data` object,
+ # so serialize it here (mirrors how `set_error` serializes its backtrace).
+ def set_sample_data(key, data)
+ @handle.set_sample_data(key, Appsignal::Utils::Data.generate(data))
+ end
+
+ # Buffer breadcrumbs, keeping the last `BREADCRUMB_LIMIT`, and flush them as
+ # sample data on completion.
+ def add_breadcrumb(breadcrumb)
+ @breadcrumbs.push(breadcrumb)
+ @breadcrumbs = @breadcrumbs.last(Appsignal::Transaction::BREADCRUMB_LIMIT)
+ end
+
+ # Serializes the backtrace to a C-extension `Data` object and records the
+ # error, then flushes the causes as `error_causes` sample data in the
+ # agent's first-line shape.
+ def set_error(class_name, message, backtrace, causes, root_cause_missing)
+ backtrace_data =
+ if backtrace
+ Appsignal::Utils::Data.generate(backtrace)
+ else
+ Appsignal::Extension.data_array_new
+ end
+ @handle.set_error(class_name, message, backtrace_data)
+
+ set_sample_data("error_causes", error_causes_sample_data(causes, root_cause_missing))
+ end
+
+ def finish
+ @handle.finish(0)
+ end
+
+ def complete
+ unless @breadcrumbs.empty?
+ @handle.set_sample_data("breadcrumbs", Appsignal::Utils::Data.generate(@breadcrumbs))
+ end
+ @handle.complete
+ end
+
+ # Discarding in agent mode drops the transaction: the extension handle is
+ # simply abandoned and never told to complete, so nothing is sent. There
+ # is no `ignore_subtrace` concept on the agent path. This mirrors the
+ # pre-backend behavior, where `Transaction#complete` returned before
+ # touching the handle on a discarded transaction.
+ def discard
+ end
+
+ # The extension transaction holds a single error, so the Transaction
+ # reports additional errors as duplicate transactions instead.
+ def records_errors_eagerly?
+ false
+ end
+
+ def duplicate(new_transaction_id)
+ self.class.new(
+ new_transaction_id, nil, :handle => @handle.duplicate(new_transaction_id)
+ ).tap { |backend| backend.breadcrumbs = @breadcrumbs.dup }
+ end
+
+ def to_json # rubocop:disable Lint/ToJSON
+ @handle.to_json
+ end
+
+ private
+
+ # Projects the neutral causes to the agent's first-line shape. A truncated
+ # chain marks its last entry as not the root cause.
+ def error_causes_sample_data(causes, root_cause_missing)
+ sample_data = causes.map do |cause|
+ {
+ :name => cause[:name],
+ :message => cause[:message],
+ :first_line => first_formatted_backtrace_line(cause[:backtrace])
+ }
+ end
+ sample_data.last[:is_root_cause] = false if root_cause_missing && sample_data.any?
+ sample_data
+ end
+
+ # Parses the first backtrace line into the fields the UI links on (gem,
+ # path, line, method), with the path made relative to the app root.
+ def first_formatted_backtrace_line(backtrace)
+ first_line = backtrace&.first
+ return unless first_line
+
+ captures = BACKTRACE_REGEX.match(first_line)
+ return unless captures
+
+ captures.named_captures
+ .merge("original" => first_line)
+ .tap do |c|
+ config = Appsignal.config
+ c["gem"] = c["gem"]&.strip
+ root_path = config.root_path
+ if c["path"].start_with?(root_path)
+ c["path"].delete_prefix!(root_path)
+ c["path"].delete_prefix!("/")
+ end
+ c["revision"] = config[:revision]
+ c["line"] = c["line"].to_i
+ end
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/transaction/opentelemetry_backend.rb b/lib/appsignal/transaction/opentelemetry_backend.rb
new file mode 100644
index 000000000..5fd3bc8c2
--- /dev/null
+++ b/lib/appsignal/transaction/opentelemetry_backend.rb
@@ -0,0 +1,454 @@
+# frozen_string_literal: true
+
+require "json"
+require "socket"
+
+module Appsignal
+ class Transaction
+ # @!visibility private
+ #
+ # The transaction backend used in collector mode. Emits an OpenTelemetry
+ # root span for the transaction, a child span per instrumented event, and
+ # queue timing as a metric. Errors and breadcrumbs attach to whichever span
+ # is current when they happen.
+ class OpenTelemetryBackend < BaseBackend
+ TRACER_NAME = "appsignal-ruby"
+
+ # Keys correspond to `Appsignal::Transaction::HTTP_REQUEST`,
+ # `ACTION_CABLE` and `BACKGROUND_JOB` respectively. Spelled as strings
+ # because this file is required (via `Backends`) before
+ # `lib/appsignal/transaction.rb`, so the constants are not yet defined
+ # at class-body evaluation time.
+ SPAN_KIND_BY_NAMESPACE = {
+ "http_request" => :server,
+ "action_cable" => :server,
+ "background_job" => :consumer
+ }.freeze
+
+ # Collector treats SERVER/CONSUMER spans as subtrace roots; SERVER is
+ # the safe default for user-defined namespaces (almost always
+ # external-triggered units of work).
+ DEFAULT_SPAN_KIND = :server
+
+ # The collector expects "web"/"background"; the agent's processor converts
+ # these internal namespaces in agent mode, but nothing does in collector
+ # mode. Other namespaces pass through unchanged.
+ DISPLAY_NAMESPACE = {
+ "http_request" => "web",
+ "background_job" => "background"
+ }.freeze
+
+ # Placeholder name an event span carries between `start_event` and
+ # `finish_event`. `finish_event` overwrites it with the AS::N event
+ # name; only surfaces if `complete` has to drain a span that was
+ # started but never finished.
+ EVENT_SPAN_PLACEHOLDER_NAME = "appsignal.event"
+
+ # Sentinel value the AppSignal collector recognizes as "a SQL system
+ # we don't know the specific dialect of" — sufficient to trigger SQL
+ # sanitization on `db.query.text`.
+ SQL_DB_SYSTEM = "other_sql"
+
+ # Epoch-ms floor (~year 2000) below which a queue start is ignored. Mirrors
+ # the agent's `set_queue_start` (ext `transaction.rs`), which only records a
+ # queue duration when `queue_start_ms > 946_681_200_000`.
+ QUEUE_START_MIN = 946_681_200_000
+
+ def initialize(transaction_id, namespace, opentelemetry_context: nil, **)
+ super()
+ @transaction_id = transaction_id
+ @namespace = namespace
+ @completed = false
+ @event_stack = []
+ @breadcrumb_count = 0
+ @queue_start = nil
+ @start_time = Time.now
+
+ kind = SPAN_KIND_BY_NAMESPACE.fetch(namespace, DEFAULT_SPAN_KIND)
+ @span = start_transaction_span(namespace, kind, opentelemetry_context)
+ @context_token = ::OpenTelemetry::Context.attach(
+ ::OpenTelemetry::Trace.context_with_span(@span)
+ )
+
+ # Transaction#initialize sets the namespace directly without calling
+ # set_namespace, so emit the attribute from here.
+ @span.set_attribute("appsignal.namespace", display_namespace(namespace)) if namespace
+ end
+
+ # `opentelemetry_kind` (e.g. `:client` for an outgoing HTTP request) is set
+ # at span creation because OTel span kind is immutable afterwards. `nil`
+ # leaves the SDK default (INTERNAL).
+ def start_event(opentelemetry_kind: nil)
+ span = tracer.start_span(EVENT_SPAN_PLACEHOLDER_NAME, :kind => opentelemetry_kind)
+ token = ::OpenTelemetry::Context.attach(
+ ::OpenTelemetry::Trace.context_with_span(span)
+ )
+ @event_stack.push([span, token])
+ end
+
+ def finish_event(name, title, body, body_format)
+ return if @event_stack.empty?
+
+ span, token = @event_stack.pop
+ write_event_name_attributes(span, name, title)
+ write_event_body_attributes(span, body, body_format)
+ ::OpenTelemetry::Context.detach(token)
+ span.finish
+ end
+
+ # `opentelemetry_kind` is set at span creation (kind is immutable in OTel),
+ # mirroring `start_event`. `nil` leaves the SDK default (INTERNAL).
+ def record_event(name, title, body, body_format, duration, opentelemetry_kind: nil) # rubocop:disable Metrics/ParameterLists
+ start_time = Time.now - (duration / 1_000_000_000.0)
+ span = tracer.start_span(
+ EVENT_SPAN_PLACEHOLDER_NAME,
+ :start_timestamp => start_time,
+ :kind => opentelemetry_kind
+ )
+ write_event_name_attributes(span, name, title)
+ write_event_body_attributes(span, body, body_format)
+ span.finish
+ end
+
+ def set_action(action)
+ # The collector reads the action from `appsignal.action_name`, not the
+ # span name. Set the name too so the OTel-native trace stays readable;
+ # the collector treats the span name as authoritative for display.
+ @span.name = action
+ @span.set_attribute("appsignal.action_name", action)
+ end
+
+ def set_namespace(namespace)
+ # Only the attribute can change here: SpanKind is fixed at span
+ # creation (immutable in OTel) from the initial namespace. A later
+ # namespace override updates `appsignal.namespace` but not the kind --
+ # the collector uses the attribute for the namespace and the kind only
+ # to pick the subtrace root, so this is fine for the rare late change.
+ @namespace = namespace
+ @span.set_attribute("appsignal.namespace", display_namespace(namespace))
+ end
+
+ # Queue start has no OTel-native home, so surface it two ways: an
+ # `appsignal.queue_start` event on the root span (per-trace timeline) and,
+ # at completion, a `transaction_queue_duration` metric (the aggregate
+ # graph). Like the agent, we record the delta and never shift span timing.
+ def set_queue_start(start)
+ return unless start && start > QUEUE_START_MIN
+
+ @queue_start = start
+ @span.add_event(
+ "appsignal.queue_start",
+ :timestamp => Time.at(start / 1000.0),
+ :attributes => { "appsignal.queue_start" => start }
+ )
+ end
+
+ # Transaction metadata (request path, method, ...) has no dedicated OTel
+ # attribute, but it is the same shape as tags and the collector/trace UI
+ # already surface `appsignal.tag.*`, so emit metadata as a tag.
+ def set_metadata(key, value)
+ @span.set_attribute("appsignal.tag.#{key}", value)
+ end
+
+ # Routes each sample-data category to the attribute the collector reads.
+ # The JSON-blob categories (params, session, custom data) are serialized as
+ # JSON; `environment` becomes request-header attributes; tags fan out to
+ # `appsignal.tag.*`. Unknown keys pass through as `appsignal.` JSON so
+ # nothing is lost. Breadcrumbs never reach here (the backend emits them as
+ # span events); causes ride on the exception event (see #set_error).
+ def set_sample_data(key, data)
+ case key
+ when "params"
+ @span.set_attribute(params_attribute, JSON.generate(data))
+ when "session_data"
+ @span.set_attribute("appsignal.request.session_data", JSON.generate(data))
+ when "custom_data"
+ @span.set_attribute("appsignal.custom_data", JSON.generate(data))
+ when "environment"
+ write_request_headers(data)
+ when "tags"
+ write_tags(data)
+ else
+ @span.set_attribute("appsignal.#{key}", JSON.generate(data))
+ end
+ end
+
+ # Records the error as an `exception` event on AppSignal's current span --
+ # the open event span, or the root -- so it attaches to the operation that
+ # raised it. Uses AppSignal's own span, not the OTel current span, which
+ # may belong to another instrumentation. `appsignal.alert_this_error` tells
+ # the collector to report it even on a child span; the collector computes
+ # the digest. Causes ride on one `appsignal.error_causes` JSON attribute
+ # (keys match the processor's `ErrorSubCause`); separate cause events would
+ # each become their own incident.
+ def set_error(class_name, message, backtrace, causes, _root_cause_missing)
+ span = current_span
+
+ attributes = {
+ "exception.type" => class_name,
+ "exception.message" => message.to_s,
+ "exception.stacktrace" => Array(backtrace).join("\n"),
+ "appsignal.alert_this_error" => true
+ }
+
+ unless causes.empty?
+ attributes["appsignal.error_causes"] = JSON.generate(
+ causes.map do |cause|
+ {
+ "name" => cause[:name],
+ "message" => cause[:message],
+ "lines" => cause[:backtrace] || []
+ }
+ end
+ )
+ end
+
+ span.add_event("exception", :attributes => attributes)
+ span.status = ::OpenTelemetry::Trace::Status.error
+ end
+
+ # Emits a breadcrumb as an `appsignal.breadcrumb` span event on AppSignal's
+ # current span -- the open event span, falling back to the root -- not the
+ # OTel current span, which may belong to another instrumentation.
+ # Breadcrumbs are emitted immediately (not flushed at completion) because by
+ # completion the event span has finished, and the OTel SDK drops events
+ # added to an ended span.
+ #
+ # The breadcrumb's recorded time becomes the event timestamp (events carry
+ # their own time), so it is not duplicated as an attribute;
+ # category/action/message are attributes and the metadata Hash is a JSON
+ # string (event attributes are flat).
+ #
+ # Capped at `BREADCRUMB_LIMIT` per transaction: streamed events can't be
+ # retracted, so we keep the first N rather than agent mode's last N.
+ def add_breadcrumb(breadcrumb)
+ return if @breadcrumb_count >= Appsignal::Transaction::BREADCRUMB_LIMIT
+
+ @breadcrumb_count += 1
+ current_span.add_event(
+ "appsignal.breadcrumb",
+ :timestamp => Time.at(breadcrumb[:time]),
+ :attributes => {
+ "category" => breadcrumb[:category],
+ "action" => breadcrumb[:action],
+ "message" => breadcrumb[:message],
+ "metadata" => JSON.generate(breadcrumb[:metadata] || {})
+ }
+ )
+ end
+
+ # Returns `true` so `Transaction#complete` runs `sample_data`, flushing the
+ # params/session/custom-data/tags/etc. onto the still-open root span before
+ # `complete` finishes it. The OTel SDK makes its own sampling decision; the
+ # gem always populates the span.
+ def finish
+ true
+ end
+
+ def complete
+ # `teardown` sets `@completed`, so this guard also makes the metric
+ # idempotent across a double `complete`, and skips it on `discard`.
+ emit_queue_duration_metric unless @completed
+ teardown
+ end
+
+ # Discarding does not mean "don't send" as it does in agent mode: the
+ # root span is still finished and exported, but flagged with
+ # `appsignal.ignore_subtrace = true` so the collector drops the whole
+ # subtrace (the attribute is hoisted to the subtrace root, last write
+ # wins). The flag must be written before the span finishes -- attributes
+ # set on an ended span are dropped. Tearing the span down here also
+ # detaches the context, so a discarded transaction can't leak its root
+ # span as the current OTel context onto the thread.
+ def discard
+ return if @completed
+
+ @span&.set_attribute("appsignal.ignore_subtrace", true)
+ teardown
+ end
+
+ # Each error is recorded eagerly as its own `exception` event on the span
+ # current when it was added, so the Transaction never duplicates itself --
+ # which is why `duplicate` is left unimplemented (see BaseBackend).
+ def records_errors_eagerly?
+ true
+ end
+
+ # Returned so `Transaction#to_h` (`JSON.parse(@backend.to_json)`) yields an
+ # empty Hash. Collector mode asserts on emitted spans, not `to_h`.
+ def to_json # rubocop:disable Lint/ToJSON
+ "{}"
+ end
+
+ private
+
+ # Detaches the OTel context and finishes the root span. Idempotent: the
+ # Transaction can complete directly and again via a cleanup path, and
+ # re-detaching/re-finishing an ended span would error.
+ def teardown
+ return if @completed
+
+ @completed = true
+ # Release any event span left unfinished by an aborted flow, so the
+ # root context can detach in LIFO order.
+ until @event_stack.empty?
+ span, token = @event_stack.pop
+ ::OpenTelemetry::Context.detach(token)
+ span.finish
+ end
+ ::OpenTelemetry::Context.detach(@context_token) if @context_token
+ @span&.finish
+ end
+
+ # Emits the queue duration as a distribution metric in both the
+ # per-namespace and per-namespace-and-host series the queue-time graph
+ # reads. Nothing downstream fans these out, so emit both ourselves.
+ def emit_queue_duration_metric
+ return unless @queue_start
+
+ duration_ms = (@start_time.to_f * 1000) - @queue_start
+ return if duration_ms.negative?
+
+ namespace = display_namespace(@namespace)
+ Appsignal::Metrics::OpenTelemetryBackend.add_distribution_value(
+ "transaction_queue_duration", duration_ms, :namespace => namespace
+ )
+ Appsignal::Metrics::OpenTelemetryBackend.add_distribution_value(
+ "transaction_queue_duration", duration_ms,
+ :namespace => namespace, :hostname => hostname
+ )
+ end
+
+ def hostname
+ Appsignal.config&.[](:hostname) || Socket.gethostname
+ end
+
+ def tracer
+ ::OpenTelemetry.tracer_provider.tracer(TRACER_NAME, Appsignal::VERSION)
+ end
+
+ # The open event span, or the root span when no event is open. Not the OTel
+ # current span, which may belong to another instrumentation.
+ def current_span
+ span, _token = @event_stack.last
+ span || @span
+ end
+
+ def placeholder_span_name(namespace)
+ "appsignal.transaction #{namespace}"
+ end
+
+ # Open the transaction's root span, relating it to any incoming trace
+ # context by the unit of work's kind:
+ #
+ # - SERVER (web): parent under the remote span so the transaction
+ # continues the upstream trace.
+ # - CONSUMER (jobs): start a fresh trace linked back to the remote span. A
+ # job is its own unit of work decoupled from the enqueuer, so it gets its
+ # own trace, with a link recording the causal relationship.
+ # - No context, an invalid remote span, or any other kind: a plain root
+ # span that ignores any ambient OTel context, as a transaction is its
+ # own unit of work.
+ def start_transaction_span(namespace, kind, opentelemetry_context)
+ name = placeholder_span_name(namespace)
+ remote = remote_span_context(opentelemetry_context)
+
+ if remote && kind == :server
+ tracer.start_span(name, :with_parent => opentelemetry_context, :kind => kind)
+ elsif remote && kind == :consumer
+ tracer.start_root_span(
+ name,
+ :kind => kind,
+ :links => [::OpenTelemetry::Trace::Link.new(remote)]
+ )
+ else
+ tracer.start_root_span(name, :kind => kind)
+ end
+ end
+
+ # The remote parent's SpanContext from an incoming OTel context, or nil
+ # when there is no context or the remote span is invalid -- in which case
+ # callers fall back to a plain root span.
+ def remote_span_context(opentelemetry_context)
+ return unless opentelemetry_context
+
+ context = ::OpenTelemetry::Trace.current_span(opentelemetry_context).context
+ context if context.valid?
+ end
+
+ def display_namespace(namespace)
+ DISPLAY_NAMESPACE.fetch(namespace, namespace)
+ end
+
+ # The collector exposes three params channels (query parameters, request
+ # payload, function parameters), each separately filtered and labeled in
+ # the trace UI. The gem only has a single merged params blob, so route it
+ # by namespace: message/job (CONSUMER-kind) transactions use the
+ # function-parameters channel, everything else (web-style, SERVER-kind)
+ # uses the request-payload channel.
+ def params_attribute
+ if SPAN_KIND_BY_NAMESPACE.fetch(@namespace, DEFAULT_SPAN_KIND) == :consumer
+ "appsignal.function.parameters"
+ else
+ "appsignal.request.payload"
+ end
+ end
+
+ # The transaction's "environment" sample data is a Rack/CGI env allowlist
+ # mixing true HTTP headers (HTTP_*, plus CONTENT_LENGTH/CONTENT_TYPE) with
+ # non-header CGI vars (REQUEST_METHOD, REQUEST_PATH, PATH_INFO, SERVER_*).
+ # Only the true headers map to the OTel `http.request.header.*` convention
+ # the collector and trace UI read, so emit those (normalized to lowercase,
+ # dashed header names) and drop everything else.
+ def write_request_headers(headers)
+ headers.each do |key, value|
+ name = otel_header_name(key)
+ @span.set_attribute("http.request.header.#{name}", value.to_s) if name
+ end
+ end
+
+ def otel_header_name(env_key)
+ if env_key.start_with?("HTTP_")
+ env_key.delete_prefix("HTTP_").downcase.tr("_", "-")
+ elsif env_key.start_with?("CONTENT_")
+ env_key.downcase.tr("_", "-")
+ end
+ end
+
+ # Each tag becomes its own `appsignal.tag.` attribute, which the
+ # collector hoists and the trace UI lists under "Tags". `sanitized_tags`
+ # already restricts values to String/Symbol/Integer/boolean; OTel
+ # attribute values must be primitives, so coerce the Symbol case to a
+ # string (the only non-primitive that survives sanitization).
+ def write_tags(tags)
+ tags.each do |key, value|
+ value = value.to_s if value.is_a?(Symbol)
+ @span.set_attribute("appsignal.tag.#{key}", value)
+ end
+ end
+
+ # The OTel span name is what the collector surfaces as the event's
+ # label in the trace UI, so prefer the human-readable `title` (e.g.
+ # "User Load", "GET https://example.com") and fall back to the AS::N
+ # `name` (e.g. "sql.active_record") when no formatter supplied a title.
+ # The machine name still rides along in `appsignal.category` so it is
+ # not lost once the title wins the span name -- it keeps the event's
+ # grouping key available for later filtering.
+ def write_event_name_attributes(span, name, title)
+ span.name = title && !title.empty? ? title : name
+ span.set_attribute("appsignal.category", name)
+ end
+
+ def write_event_body_attributes(span, body, body_format)
+ return if body.nil? || body.empty?
+
+ if body_format == Appsignal::EventFormatter::SQL_BODY_FORMAT
+ span.set_attribute("db.query.text", body)
+ span.set_attribute("db.system.name", SQL_DB_SYSTEM)
+ else
+ span.set_attribute("appsignal.body", body)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/appsignal/utils/stdout_and_logger_message.rb b/lib/appsignal/utils/stdout_and_logger_message.rb
index f26bcf2d8..e0c94958c 100644
--- a/lib/appsignal/utils/stdout_and_logger_message.rb
+++ b/lib/appsignal/utils/stdout_and_logger_message.rb
@@ -8,9 +8,18 @@ def self.warning(message, logger = Appsignal.internal_logger)
logger.warn message
end
+ def self.error(message, logger = Appsignal.internal_logger)
+ Kernel.warn "appsignal ERROR: #{message}"
+ logger.error message
+ end
+
def stdout_and_logger_warning(message, logger = Appsignal.internal_logger)
Appsignal::Utils::StdoutAndLoggerMessage.warning(message, logger)
end
+
+ def stdout_and_logger_error(message, logger = Appsignal.internal_logger)
+ Appsignal::Utils::StdoutAndLoggerMessage.error(message, logger)
+ end
end
end
end
diff --git a/sig/appsignal.rbi b/sig/appsignal.rbi
index ac31143d8..c7ea4c8a6 100644
--- a/sig/appsignal.rbi
+++ b/sig/appsignal.rbi
@@ -833,7 +833,7 @@ module Appsignal
# Breadcrumbs can be used to trace what path a user has taken
# before encountering an error.
#
- # Only the last 20 added breadcrumbs will be saved.
+ # At most 20 of the added breadcrumbs will be saved.
#
# _@param_ `category` — category of breadcrumb e.g. "UI", "Network", "Navigation", "Console".
#
@@ -922,10 +922,11 @@ module Appsignal
title: T.nilable(String),
body: T.nilable(String),
body_format: Integer,
+ opentelemetry_kind: T.untyped,
block: T.untyped
).returns(Object)
end
- def self.instrument(name, title = nil, body = nil, body_format = Appsignal::EventFormatter::DEFAULT, &block); end
+ def self.instrument(name, title = nil, body = nil, body_format = Appsignal::EventFormatter::DEFAULT, opentelemetry_kind: nil, &block); end
# Instrumentation helper for SQL queries.
#
@@ -1041,6 +1042,38 @@ module Appsignal
sig { returns(T::Boolean) }
def active?; end
+ # Check if collector mode is configured.
+ #
+ # Returns true when a non-empty `collector_endpoint` is set and the
+ # running Ruby version is at least {MIN_RUBY_VERSION_FOR_COLLECTOR_MODE}.
+ # On older Rubies, `collector_endpoint` is ignored (with a warning) and
+ # the AppSignal agent is used instead.
+ #
+ # This is the *intent* check — it answers "did the user ask for
+ # collector mode, and could we honor it?". It does not say whether the
+ # OpenTelemetry SDK actually booted. See {#collector_mode?} for that.
+ #
+ # Memoised: the result is cached on first call so hot paths avoid
+ # re-running the string-strip predicate, and so the unsupported-Ruby
+ # warning is emitted at most once per `Config` instance.
+ #
+ # _@return_ — True if collector mode is configured.
+ sig { returns(T::Boolean) }
+ def collector_mode_configured?; end
+
+ # Check if AppSignal is actively running in collector mode.
+ #
+ # True only if collector mode is {#collector_mode_configured? configured}
+ # *and* `Appsignal::OpenTelemetry.configure` has successfully booted the
+ # SDK in this process. Use this for backend dispatch on hot paths
+ # (metric and log emits): if the OTel boot failed, callers fall back to
+ # the agent backend rather than silently dropping data into no-op
+ # providers.
+ #
+ # _@return_ — True if collector mode is configured and started.
+ sig { returns(T::Boolean) }
+ def collector_mode?; end
+
sig { returns(T::Boolean) }
def yml_config_file?; end
@@ -1626,8 +1659,8 @@ module Appsignal
# transaction.
#
# _@param_ `namespace` — Namespace of the to be created transaction.
- sig { params(namespace: String).returns(Transaction) }
- def self.create(namespace); end
+ sig { params(namespace: String, opentelemetry_context: T.untyped).returns(Transaction) }
+ def self.create(namespace, opentelemetry_context: nil); end
# Returns currently active transaction or a {NilTransaction} if none is
# active.
@@ -2453,7 +2486,7 @@ module Appsignal
# Breadcrumbs can be used to trace what path a user has taken
# before encountering an error.
#
- # Only the last 20 added breadcrumbs will be saved.
+ # At most 20 of the added breadcrumbs will be saved.
#
# _@param_ `category` — category of breadcrumb e.g. "UI", "Network", "Navigation", "Console".
#
@@ -2542,10 +2575,11 @@ module Appsignal
title: T.nilable(String),
body: T.nilable(String),
body_format: Integer,
+ opentelemetry_kind: T.untyped,
block: T.untyped
).returns(Object)
end
- def instrument(name, title = nil, body = nil, body_format = Appsignal::EventFormatter::DEFAULT, &block); end
+ def instrument(name, title = nil, body = nil, body_format = Appsignal::EventFormatter::DEFAULT, opentelemetry_kind: nil, &block); end
# Instrumentation helper for SQL queries.
#
@@ -2629,6 +2663,9 @@ module Appsignal
sig { returns(String) }
def message; end
end
+
+ module Metrics
+ end
end
# Extensions to Object for AppSignal method instrumentation.
diff --git a/sig/appsignal.rbs b/sig/appsignal.rbs
index b2d4a0a91..e5eea8673 100644
--- a/sig/appsignal.rbs
+++ b/sig/appsignal.rbs
@@ -778,7 +778,7 @@ module Appsignal
# Breadcrumbs can be used to trace what path a user has taken
# before encountering an error.
#
- # Only the last 20 added breadcrumbs will be saved.
+ # At most 20 of the added breadcrumbs will be saved.
#
# _@param_ `category` — category of breadcrumb e.g. "UI", "Network", "Navigation", "Console".
#
@@ -862,7 +862,8 @@ module Appsignal
String name,
?String? title,
?String? body,
- ?Integer body_format
+ ?Integer body_format,
+ ?opentelemetry_kind: untyped
) -> Object
# Instrumentation helper for SQL queries.
@@ -991,6 +992,36 @@ module Appsignal
# _@return_ — True if valid and active for the current environment.
def active?: () -> bool
+ # Check if collector mode is configured.
+ #
+ # Returns true when a non-empty `collector_endpoint` is set and the
+ # running Ruby version is at least {MIN_RUBY_VERSION_FOR_COLLECTOR_MODE}.
+ # On older Rubies, `collector_endpoint` is ignored (with a warning) and
+ # the AppSignal agent is used instead.
+ #
+ # This is the *intent* check — it answers "did the user ask for
+ # collector mode, and could we honor it?". It does not say whether the
+ # OpenTelemetry SDK actually booted. See {#collector_mode?} for that.
+ #
+ # Memoised: the result is cached on first call so hot paths avoid
+ # re-running the string-strip predicate, and so the unsupported-Ruby
+ # warning is emitted at most once per `Config` instance.
+ #
+ # _@return_ — True if collector mode is configured.
+ def collector_mode_configured?: () -> bool
+
+ # Check if AppSignal is actively running in collector mode.
+ #
+ # True only if collector mode is {#collector_mode_configured? configured}
+ # *and* `Appsignal::OpenTelemetry.configure` has successfully booted the
+ # SDK in this process. Use this for backend dispatch on hot paths
+ # (metric and log emits): if the OTel boot failed, callers fall back to
+ # the agent backend rather than silently dropping data into no-op
+ # providers.
+ #
+ # _@return_ — True if collector mode is configured and started.
+ def collector_mode?: () -> bool
+
def yml_config_file?: () -> bool
# Configuration DSL for use in configuration blocks.
@@ -1499,7 +1530,7 @@ module Appsignal
# transaction.
#
# _@param_ `namespace` — Namespace of the to be created transaction.
- def self.create: (String namespace) -> Transaction
+ def self.create: (String namespace, ?opentelemetry_context: untyped) -> Transaction
# Returns currently active transaction or a {NilTransaction} if none is
# active.
@@ -2291,7 +2322,7 @@ module Appsignal
# Breadcrumbs can be used to trace what path a user has taken
# before encountering an error.
#
- # Only the last 20 added breadcrumbs will be saved.
+ # At most 20 of the added breadcrumbs will be saved.
#
# _@param_ `category` — category of breadcrumb e.g. "UI", "Network", "Navigation", "Console".
#
@@ -2375,7 +2406,8 @@ module Appsignal
String name,
?String? title,
?String? body,
- ?Integer body_format
+ ?Integer body_format,
+ ?opentelemetry_kind: untyped
) -> Object
# Instrumentation helper for SQL queries.
@@ -2450,6 +2482,9 @@ module Appsignal
class NotStartedError < Appsignal::InternalError
def message: () -> String
end
+
+ module Metrics
+ end
end
# Extensions to Object for AppSignal method instrumentation.
diff --git a/spec/integration/collector_mode_fork_spec.rb b/spec/integration/collector_mode_fork_spec.rb
new file mode 100644
index 000000000..404df3b2d
--- /dev/null
+++ b/spec/integration/collector_mode_fork_spec.rb
@@ -0,0 +1,40 @@
+# Skipped on JRuby because `Process.fork` raises NotImplementedError there,
+# so the runner script exits before emitting anything. JRuby's collector
+# mode still works for non-forking workloads (covered by the other
+# collector_mode_*_spec files).
+if DependencyHelper.opentelemetry_present? && !DependencyHelper.running_jruby?
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry/proto/collector/metrics/v1/metrics_service_pb"
+
+ describe "Collector mode under fork" do
+ before { OTLPCollectorServer.clear }
+
+ it "exports metrics emitted by a forked child without explicit re-init" do
+ # The OTel metrics SDK ships fork hooks (ForkHooks in
+ # `opentelemetry-metrics-sdk`) that restart the PeriodicMetricReader
+ # in the child after a fork. Those hooks are attached when
+ # `OpenTelemetry::SDK.configure` runs, which happens in
+ # `Appsignal::OpenTelemetry.configure` at boot. This spec locks
+ # down that chain: if a future refactor drops the `SDK.configure`
+ # call (or otherwise disconnects the fork hooks), the child's
+ # metric would queue inside a dead reader and never arrive.
+ Runner.new("collector_mode_fork", :env => OTLPCollectorServer.env).run
+
+ metric_names = []
+ loop do
+ req = OTLPCollectorServer.listen_to("/v1/metrics", :timeout => 2)
+ msg = Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest
+ .decode(req[:body])
+ metric_names.concat(
+ msg.resource_metrics.flat_map do |rm|
+ rm.scope_metrics.flat_map { |sm| sm.metrics.map(&:name) }
+ end
+ )
+ rescue RuntimeError
+ break
+ end
+
+ expect(metric_names).to include("forked_child_counter")
+ end
+ end
+end
diff --git a/spec/integration/collector_mode_log_trace_correlation_spec.rb b/spec/integration/collector_mode_log_trace_correlation_spec.rb
new file mode 100644
index 000000000..3fa339794
--- /dev/null
+++ b/spec/integration/collector_mode_log_trace_correlation_spec.rb
@@ -0,0 +1,46 @@
+if DependencyHelper.opentelemetry_present?
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry/proto/collector/trace/v1/trace_service_pb"
+ require "opentelemetry/proto/collector/logs/v1/logs_service_pb"
+
+ describe "AppSignal collector mode log/trace correlation" do
+ before { OTLPCollectorServer.clear }
+
+ it "stamps log records with the trace_id and span_id of the active span" do
+ runner = Runner.new("collector_mode_log_trace_correlation",
+ :env => OTLPCollectorServer.env)
+ runner.run
+
+ trace_req = OTLPCollectorServer.listen_to("/v1/traces")
+ trace_msg = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest
+ .decode(trace_req[:body])
+
+ log_req = OTLPCollectorServer.listen_to("/v1/logs")
+ log_msg = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest
+ .decode(log_req[:body])
+
+ spans = trace_msg.resource_spans.flat_map { |rs| rs.scope_spans.flat_map(&:spans) }
+ root = spans.find { |s| s.parent_span_id.empty? }
+ event = spans.find { |s| s.name == "test.event" }
+ expect(root).not_to be_nil
+ expect(event).not_to be_nil
+ expect(event.parent_span_id).to eq(root.span_id)
+
+ logs_by_body = log_msg.resource_logs
+ .flat_map { |rl| rl.scope_logs.flat_map(&:log_records) }
+ .to_h { |lr| [lr.body.string_value, lr] }
+
+ expect(logs_by_body.keys).to match_array(["before event", "inside event", "after event"])
+
+ # Logs emitted outside any event span carry the root span's ids.
+ expect(logs_by_body["before event"].trace_id).to eq(root.trace_id)
+ expect(logs_by_body["before event"].span_id).to eq(root.span_id)
+ expect(logs_by_body["after event"].trace_id).to eq(root.trace_id)
+ expect(logs_by_body["after event"].span_id).to eq(root.span_id)
+
+ # The log emitted inside `instrument` carries the event span's ids.
+ expect(logs_by_body["inside event"].trace_id).to eq(event.trace_id)
+ expect(logs_by_body["inside event"].span_id).to eq(event.span_id)
+ end
+ end
+end
diff --git a/spec/integration/collector_mode_logs_spec.rb b/spec/integration/collector_mode_logs_spec.rb
new file mode 100644
index 000000000..ed0a164e0
--- /dev/null
+++ b/spec/integration/collector_mode_logs_spec.rb
@@ -0,0 +1,45 @@
+if DependencyHelper.opentelemetry_present?
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry/proto/collector/logs/v1/logs_service_pb"
+
+ describe "AppSignal collector mode log helpers" do
+ before { OTLPCollectorServer.clear }
+
+ it "emits OTLP log records through Appsignal::Logger" do
+ runner = Runner.new("collector_mode_logs", :env => OTLPCollectorServer.env)
+ runner.run
+
+ log_req = OTLPCollectorServer.listen_to("/v1/logs")
+ log_msg = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest
+ .decode(log_req[:body])
+
+ scope_logs = log_msg.resource_logs.flat_map(&:scope_logs)
+ expect(scope_logs.map { |sl| sl.scope.name }).to include("appsignal-logger")
+
+ records = scope_logs.flat_map(&:log_records)
+ by_body = records.to_h { |record| [record.body.string_value, record] }
+
+ expect(by_body.keys).to include("info line", "warn line", "error line")
+
+ info_record = by_body.fetch("info line")
+ expect(info_record.severity_number).to eq(:SEVERITY_NUMBER_INFO)
+ expect(info_record.severity_text).to eq("INFO")
+ expect(attribute_value(info_record, "appsignal.group").string_value).to eq("my-group")
+ expect(attribute_value(info_record, "appsignal.format").string_value).to eq("json")
+ expect(attribute_value(info_record, "service").string_value).to eq("runner")
+ expect(attribute_value(info_record, "tag").string_value).to eq("value")
+
+ warn_record = by_body.fetch("warn line")
+ expect(warn_record.severity_number).to eq(:SEVERITY_NUMBER_WARN)
+ expect(warn_record.severity_text).to eq("WARN")
+
+ error_record = by_body.fetch("error line")
+ expect(error_record.severity_number).to eq(:SEVERITY_NUMBER_ERROR)
+ expect(error_record.severity_text).to eq("ERROR")
+ end
+
+ def attribute_value(record, key)
+ record.attributes.find { |kv| kv.key == key }&.value
+ end
+ end
+end
diff --git a/spec/integration/collector_mode_metrics_spec.rb b/spec/integration/collector_mode_metrics_spec.rb
new file mode 100644
index 000000000..fa40a4be4
--- /dev/null
+++ b/spec/integration/collector_mode_metrics_spec.rb
@@ -0,0 +1,50 @@
+if DependencyHelper.opentelemetry_present?
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry/proto/collector/metrics/v1/metrics_service_pb"
+
+ describe "AppSignal collector mode metric helpers" do
+ before { OTLPCollectorServer.clear }
+
+ it "emits OTLP metrics for set_gauge, increment_counter and add_distribution_value" do
+ runner = Runner.new("collector_mode_metrics", :env => OTLPCollectorServer.env)
+ runner.run
+
+ metric_req = OTLPCollectorServer.listen_to("/v1/metrics")
+ metric_msg = Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest
+ .decode(metric_req[:body])
+
+ scope_metrics = metric_msg.resource_metrics.flat_map(&:scope_metrics)
+ expect(scope_metrics.map { |sm| sm.scope.name }).to include("appsignal-helpers")
+
+ metrics_by_name = scope_metrics
+ .flat_map(&:metrics)
+ .to_h { |metric| [metric.name, metric] }
+
+ expect(metrics_by_name.keys).to include("test_counter", "test_gauge", "test_distribution")
+
+ counter = metrics_by_name.fetch("test_counter")
+ expect(counter.data).to eq(:sum)
+ counter_point = counter.sum.data_points.first
+ expect(counter_point.as_double).to eq(1.0)
+ expect(attribute_value(counter_point, "tag")).to eq("value")
+
+ gauge = metrics_by_name.fetch("test_gauge")
+ expect(gauge.data).to eq(:gauge)
+ gauge_point = gauge.gauge.data_points.first
+ expect(gauge_point.as_double).to eq(42.5)
+ expect(attribute_value(gauge_point, "tag")).to eq("value")
+
+ histogram = metrics_by_name.fetch("test_distribution")
+ expect(histogram.data).to eq(:histogram)
+ histogram_point = histogram.histogram.data_points.first
+ expect(histogram_point.count).to eq(1)
+ expect(histogram_point.sum).to be_within(0.0001).of(0.123)
+ expect(attribute_value(histogram_point, "tag")).to eq("value")
+ end
+
+ def attribute_value(data_point, key)
+ pair = data_point.attributes.find { |attr| attr.key == key }
+ pair&.value&.string_value
+ end
+ end
+end
diff --git a/spec/integration/collector_mode_mixed_api_spec.rb b/spec/integration/collector_mode_mixed_api_spec.rb
new file mode 100644
index 000000000..81089d79f
--- /dev/null
+++ b/spec/integration/collector_mode_mixed_api_spec.rb
@@ -0,0 +1,54 @@
+if DependencyHelper.opentelemetry_present?
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry/proto/collector/trace/v1/trace_service_pb"
+
+ describe "AppSignal collector mode mixing OTel and AppSignal APIs" do
+ before { OTLPCollectorServer.clear }
+
+ it "nests instrument and monitor spans correctly relative to OTel context" do
+ runner = Runner.new("collector_mode_mixed_api", :env => OTLPCollectorServer.env)
+ runner.run
+
+ trace_req = OTLPCollectorServer.listen_to("/v1/traces")
+ trace_msg = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest
+ .decode(trace_req[:body])
+
+ spans = trace_msg.resource_spans.flat_map { |rs| rs.scope_spans.flat_map(&:spans) }
+ by_name = spans.to_h { |s| [s.name, s] }
+
+ outer = by_name.fetch("outer.otel")
+ # `Appsignal.monitor` renames its root span to the action, so look it
+ # up by SpanKind (SERVER is the subtrace root the collector keys on)
+ # rather than by name.
+ monitor_root = spans.find { |s| s.kind == :SPAN_KIND_SERVER }
+ expect(monitor_root).not_to be_nil
+ event_with_otel_child = by_name.fetch("event.with.otel.child")
+ inner_otel = by_name.fetch("inner.otel.inside_instrument")
+ manual_otel = by_name.fetch("manual.otel.in_monitor")
+ event_under_manual = by_name.fetch("event.under.manual.otel")
+
+ # Appsignal.monitor ignores the ambient OTel context and starts a
+ # fresh trace.
+ expect(monitor_root.parent_span_id).to be_empty
+ expect(monitor_root.trace_id).not_to eq(outer.trace_id)
+
+ # Events created via Appsignal.instrument nest under whichever
+ # span is current at the time -- the monitor root in the simple
+ # case.
+ expect(event_with_otel_child.parent_span_id).to eq(monitor_root.span_id)
+ expect(event_with_otel_child.trace_id).to eq(monitor_root.trace_id)
+
+ # A raw OTel span created inside an Appsignal.instrument block is
+ # a child of the event span.
+ expect(inner_otel.parent_span_id).to eq(event_with_otel_child.span_id)
+ expect(inner_otel.trace_id).to eq(monitor_root.trace_id)
+
+ # In reverse: when a raw OTel span is current, an
+ # Appsignal.instrument inside it nests under that OTel span
+ # (not under the monitor root directly).
+ expect(manual_otel.parent_span_id).to eq(monitor_root.span_id)
+ expect(event_under_manual.parent_span_id).to eq(manual_otel.span_id)
+ expect(event_under_manual.trace_id).to eq(monitor_root.trace_id)
+ end
+ end
+end
diff --git a/spec/integration/collector_mode_spec.rb b/spec/integration/collector_mode_spec.rb
new file mode 100644
index 000000000..03f42e127
--- /dev/null
+++ b/spec/integration/collector_mode_spec.rb
@@ -0,0 +1,105 @@
+# Collector mode is gated on Ruby 3.1+ (see
+# `Appsignal::Config::MIN_RUBY_VERSION_FOR_COLLECTOR_MODE`). On older
+# Rubies the config gate forces collector_mode? to false; that path is
+# covered by unit tests in `spec/lib/appsignal/config_spec.rb`.
+if DependencyHelper.opentelemetry_present?
+ # Use the OTLP proto Ruby stubs shipped inside the
+ # `opentelemetry-exporter-otlp` gem to decode the bodies that the runner
+ # script posts to the mock collector server.
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry/proto/collector/trace/v1/trace_service_pb"
+ require "opentelemetry/proto/collector/metrics/v1/metrics_service_pb"
+ require "opentelemetry/proto/collector/logs/v1/logs_service_pb"
+
+ describe "AppSignal collector mode" do
+ before { OTLPCollectorServer.clear }
+
+ # Asserts that the OTLP Resource (proto message) carries every AppSignal
+ # config attribute the runner script sets, with the right types, plus the
+ # `telemetry.sdk.*` attributes from the OTel SDK's default resource. Used
+ # for traces, metrics and logs alike so all three signal types are checked.
+ def expect_appsignal_resource(resource)
+ attrs = resource.attributes.to_h { |kv| [kv.key, kv.value] }
+ defaults = Runner::DEFAULT_ENV
+
+ expect(attrs["service.name"].string_value).to eq("collector-mode-test-service")
+ expect(attrs["host.name"].string_value).to eq("test-host")
+ expect(attrs["appsignal.config.name"].string_value)
+ .to eq(defaults.fetch("APPSIGNAL_APP_NAME"))
+ expect(attrs["appsignal.config.environment"].string_value)
+ .to eq(defaults.fetch("APPSIGNAL_APP_ENV"))
+ expect(attrs["appsignal.config.push_api_key"].string_value)
+ .to eq(defaults.fetch("APPSIGNAL_PUSH_API_KEY"))
+ expect(attrs["appsignal.config.revision"].string_value).to eq("abc1234")
+ expect(attrs["appsignal.config.language_integration"].string_value).to eq("ruby")
+
+ expect(attrs["appsignal.config.filter_attributes"].array_value.values.map(&:string_value))
+ .to eq(["password", "secret"])
+ expect(
+ attrs["appsignal.config.filter_request_payload"].array_value.values.map(&:string_value)
+ ).to eq(["payload-key"])
+ expect(attrs["appsignal.config.ignore_actions"].array_value.values.map(&:string_value))
+ .to eq(["IgnoredController#action"])
+ expect(attrs["appsignal.config.ignore_namespaces"].array_value.values.map(&:string_value))
+ .to eq(["background"])
+ expect(attrs["appsignal.config.send_request_payload"].bool_value).to eq(false)
+
+ # AppSignal defaults that still route into the resource.
+ expect(attrs["appsignal.config.request_headers"].array_value.values.map(&:string_value))
+ .to include("HTTP_ACCEPT")
+ expect(attrs["appsignal.config.send_request_session_data"].bool_value).to eq(true)
+
+ # OTel SDK metadata, kept by merging the AppSignal resource with `Resource.default`.
+ expect(attrs["telemetry.sdk.name"].string_value).to eq("opentelemetry")
+ expect(attrs["telemetry.sdk.language"].string_value).to eq("ruby")
+
+ # Attributes that default to nil or [] are omitted so the collector applies defaults.
+ %w[
+ appsignal.config.filter_function_parameters
+ appsignal.config.filter_request_query_parameters
+ appsignal.config.filter_request_session_data
+ appsignal.config.ignore_errors
+ appsignal.config.response_headers
+ appsignal.config.send_function_parameters
+ appsignal.config.send_request_query_parameters
+ ].each do |key|
+ expect(attrs).to_not have_key(key),
+ "expected #{key.inspect} to be omitted from the resource, got #{attrs[key].inspect}"
+ end
+ end
+
+ it "configures collector mode and emits OTLP traces, metrics, and logs" do
+ runner = Runner.new("collector_mode_emit", :env => OTLPCollectorServer.env)
+ runner.run
+
+ # Config wiring: the child process saw the value and computed the predicate.
+ expect(runner.output).to include("collector_endpoint=#{OTLPCollectorServer.endpoint}")
+ expect(runner.output).to include("collector_mode?=true")
+
+ trace_req = OTLPCollectorServer.listen_to("/v1/traces")
+ trace_msg = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest
+ .decode(trace_req[:body])
+ span_names = trace_msg.resource_spans
+ .flat_map { |rs| rs.scope_spans.flat_map { |ss| ss.spans.map(&:name) } }
+ expect(span_names).to include("test-span")
+ expect_appsignal_resource(trace_msg.resource_spans.first.resource)
+
+ metric_req = OTLPCollectorServer.listen_to("/v1/metrics")
+ metric_msg = Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest
+ .decode(metric_req[:body])
+ metric_names = metric_msg.resource_metrics
+ .flat_map { |rm| rm.scope_metrics.flat_map { |sm| sm.metrics.map(&:name) } }
+ expect(metric_names).to include("test_counter")
+ expect_appsignal_resource(metric_msg.resource_metrics.first.resource)
+
+ log_req = OTLPCollectorServer.listen_to("/v1/logs")
+ log_msg = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest
+ .decode(log_req[:body])
+ log_bodies = log_msg.resource_logs.flat_map do |rl|
+ rl.scope_logs.flat_map { |sl| sl.log_records.map { |lr| lr.body.string_value } }
+ end
+ expect(log_bodies).to include("test-log-line")
+ expect_appsignal_resource(log_msg.resource_logs.first.resource)
+ end
+ end
+end
diff --git a/spec/integration/collector_mode_stop_flush_spec.rb b/spec/integration/collector_mode_stop_flush_spec.rb
new file mode 100644
index 000000000..0a384820d
--- /dev/null
+++ b/spec/integration/collector_mode_stop_flush_spec.rb
@@ -0,0 +1,31 @@
+if DependencyHelper.opentelemetry_present?
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry/proto/collector/metrics/v1/metrics_service_pb"
+ require "opentelemetry/proto/collector/logs/v1/logs_service_pb"
+
+ describe "AppSignal.stop in collector mode" do
+ before { OTLPCollectorServer.clear }
+
+ it "flushes buffered OTel telemetry by shutting the providers down" do
+ runner = Runner.new("collector_mode_stop_flush", :env => OTLPCollectorServer.env)
+ runner.run
+
+ metric_req = OTLPCollectorServer.listen_to("/v1/metrics")
+ metric_msg = Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest
+ .decode(metric_req[:body])
+
+ metric_names = metric_msg.resource_metrics
+ .flat_map { |rm| rm.scope_metrics.flat_map { |sm| sm.metrics.map(&:name) } }
+ expect(metric_names).to include("stop_counter")
+
+ log_req = OTLPCollectorServer.listen_to("/v1/logs")
+ log_msg = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest
+ .decode(log_req[:body])
+
+ log_bodies = log_msg.resource_logs.flat_map do |rl|
+ rl.scope_logs.flat_map { |sl| sl.log_records.map { |lr| lr.body.string_value } }
+ end
+ expect(log_bodies).to include("stop log line")
+ end
+ end
+end
diff --git a/spec/integration/collector_mode_traces_spec.rb b/spec/integration/collector_mode_traces_spec.rb
new file mode 100644
index 000000000..81a314c69
--- /dev/null
+++ b/spec/integration/collector_mode_traces_spec.rb
@@ -0,0 +1,55 @@
+if DependencyHelper.opentelemetry_present?
+ require "opentelemetry/exporter/otlp"
+ require "opentelemetry/proto/collector/trace/v1/trace_service_pb"
+
+ describe "AppSignal collector mode trace API" do
+ before { OTLPCollectorServer.clear }
+
+ it "emits OTLP spans for Appsignal.monitor with nested Appsignal.instrument" do
+ runner = Runner.new("collector_mode_traces", :env => OTLPCollectorServer.env)
+ runner.run
+
+ trace_req = OTLPCollectorServer.listen_to("/v1/traces")
+ trace_msg = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest
+ .decode(trace_req[:body])
+
+ spans = trace_msg.resource_spans.flat_map { |rs| rs.scope_spans.flat_map(&:spans) }
+ by_name = spans.to_h { |s| [s.name, s] }
+
+ # Root span: SERVER kind from `monitor` (http_request namespace), no parent.
+ root = spans.find { |s| s.parent_span_id.empty? }
+ expect(root).not_to be_nil
+ expect(root.kind).to eq(:SPAN_KIND_SERVER)
+
+ # The "http_request" namespace is converted to "web" on the way out.
+ expect(attribute_value(root, "appsignal.namespace")).to eq("web")
+
+ # Event spans for each instrumented block are present. The title-less
+ # events keep the event name as the span name; the SQL event has a
+ # human-readable title ("Find user"), which becomes the span name, with
+ # the event name carried in the `appsignal.category` attribute.
+ expect(by_name.keys).to include("template.render", "partial.render")
+ sql = spans.find { |s| attribute_value(s, "appsignal.category") == "active_record.sql" }
+ expect(sql).not_to be_nil
+ expect(sql.name).to eq("Find user")
+
+ # Nested instrument calls produce a parent/child chain rooted at the monitor span.
+ expect(by_name["partial.render"].parent_span_id).to eq(by_name["template.render"].span_id)
+ expect(by_name["template.render"].parent_span_id).to eq(root.span_id)
+ expect(sql.parent_span_id).to eq(root.span_id)
+
+ # All spans share one trace id.
+ expect(spans.map(&:trace_id).uniq.size).to eq(1)
+
+ # SQL formatter applied at the OTel backend: body becomes `db.query.text` and
+ # `db.system.name` is set so the collector can sanitize.
+ expect(attribute_value(sql, "db.query.text")).to eq("SELECT * FROM users")
+ expect(attribute_value(sql, "db.system.name")).to eq("other_sql")
+ end
+
+ def attribute_value(span, key)
+ pair = span.attributes.find { |attr| attr.key == key }
+ pair&.value&.string_value
+ end
+ end
+end
diff --git a/spec/integration/diagnose b/spec/integration/diagnose
index 1e98e7d5e..69b23b08b 160000
--- a/spec/integration/diagnose
+++ b/spec/integration/diagnose
@@ -1 +1 @@
-Subproject commit 1e98e7d5ef86f0bf3631d1b45fdfdc86f9ced736
+Subproject commit 69b23b08bd80de5971d9d803a7e7244558c1c234
diff --git a/spec/integration/runner.rb b/spec/integration/runner.rb
index aa1d9d7be..b337ba392 100644
--- a/spec/integration/runner.rb
+++ b/spec/integration/runner.rb
@@ -1,9 +1,36 @@
+require "fileutils"
+require "tmpdir"
+
class Runner
+ # Env key the Runner sets itself (see `run`). Callers can't pass it via
+ # `env:` — the Runner owns the per-run working directory.
+ WORKING_DIRECTORY_ENV = "APPSIGNAL_WORKING_DIRECTORY_PATH".freeze
+
+ # Config every runner script needs, supplied as env vars so the scripts
+ # don't each hardcode it; `Appsignal.start` reads it from the environment.
+ # Specs assert against these values via this constant instead of repeating
+ # the literals. Overridable per run by passing the same key in `env:`.
+ DEFAULT_ENV = {
+ "APPSIGNAL_APP_NAME" => "integration-runner",
+ "APPSIGNAL_APP_ENV" => "test",
+ "APPSIGNAL_PUSH_API_KEY" => "abc"
+ }.freeze
+
attr_reader :pid, :output, :status
- def initialize(name)
+ # @param env [Hash] Extra environment variables to set in the spawned
+ # child process, e.g. `"APPSIGNAL_COLLECTOR_ENDPOINT"` to run against the
+ # mock collector. Merged over {DEFAULT_ENV}; must not overlap with the
+ # Runner-managed keys.
+ def initialize(name, env: {})
+ if env.key?(WORKING_DIRECTORY_ENV)
+ raise ArgumentError,
+ "#{WORKING_DIRECTORY_ENV} is managed by Runner and can't be passed via `env:`"
+ end
+
@script_name = name
@script_file = "#{@script_name}.rb"
+ @env = DEFAULT_ENV.merge(env)
@pid = nil
@output = nil
@status = nil
@@ -12,6 +39,14 @@ def initialize(name)
@read, @write = IO.pipe
@has_run = false
@finished = false
+ # Per-run working directory. Passed to the subprocess via
+ # `APPSIGNAL_WORKING_DIRECTORY_PATH` so runner scripts don't have to
+ # manage one themselves. Created under `/tmp` rather than the default
+ # `$TMPDIR` because macOS's default tmpdir lives under
+ # `/var/folders/...` and the resulting agent socket path exceeds the
+ # 104-char macOS unix-socket limit, which would hang
+ # `Appsignal::Extension.stop`. Cleaned up after the process exits.
+ @working_dir = Dir.mktmpdir("appsignal-runner-", "/tmp")
end
def has_run?
@@ -29,6 +64,7 @@ def run
executable = jruby? ? "jruby" : "ruby"
directory = File.join(__dir__, "runners")
@pid = spawn(
+ @env.merge(WORKING_DIRECTORY_ENV => @working_dir),
"#{executable} #{@script_file}",
{
[:out, :err] => @write,
@@ -48,6 +84,13 @@ def run
end
read_output
@finished = true
+
+ return if @status.exitstatus.zero?
+
+ raise "Runner '#{@script_file}' exited with status #{@status.exitstatus}.\n" \
+ "Output:\n#{@output}"
+ ensure
+ FileUtils.remove_entry(@working_dir) if @working_dir && File.exist?(@working_dir)
end
private
diff --git a/spec/integration/runners/collector_mode_emit.rb b/spec/integration/runners/collector_mode_emit.rb
new file mode 100644
index 000000000..c07be6cf8
--- /dev/null
+++ b/spec/integration/runners/collector_mode_emit.rb
@@ -0,0 +1,42 @@
+PROJECT_ROOT = "../../../".freeze
+$LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT))
+$LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT))
+
+require "appsignal"
+
+# Name, environment and push API key come from the env vars the Runner
+# injects (see `Runner::DEFAULT_ENV`). The options below are specific to
+# this test's resource-attribute assertions.
+Appsignal.configure do |config|
+ config.service_name = "collector-mode-test-service"
+ config.hostname = "test-host"
+ config.revision = "abc1234"
+ config.filter_attributes = ["password", "secret"]
+ config.filter_request_payload = ["payload-key"]
+ config.send_request_payload = false
+ config.ignore_actions = ["IgnoredController#action"]
+ config.ignore_namespaces = ["background"]
+end
+
+Appsignal.start
+
+# Print config state so the spec can verify the option round-trips end-to-end.
+puts "collector_endpoint=#{Appsignal.config[:collector_endpoint]}"
+puts "collector_mode?=#{Appsignal.config.collector_mode?}"
+
+# Emit one of each OTLP signal through the OpenTelemetry SDK that
+# `Appsignal::OpenTelemetry.configure` has just set up.
+tracer = OpenTelemetry.tracer_provider.tracer("collector-mode-runner")
+tracer.in_span("test-span") { |span| span.set_attribute("test.key", "test.value") }
+
+meter = OpenTelemetry.meter_provider.meter("collector-mode-runner")
+meter.create_counter("test_counter").add(1)
+
+logger = OpenTelemetry.logger_provider.logger(:name => "collector-mode-runner")
+logger.on_emit(:severity_text => "INFO", :body => "test-log-line")
+
+# Shut AppSignal down so the OTel providers drain their buffers and the
+# spec sees the queued requests deterministically.
+Appsignal.stop("integration test")
+
+puts "DONE"
diff --git a/spec/integration/runners/collector_mode_fork.rb b/spec/integration/runners/collector_mode_fork.rb
new file mode 100644
index 000000000..f12364ff1
--- /dev/null
+++ b/spec/integration/runners/collector_mode_fork.rb
@@ -0,0 +1,33 @@
+# A short export interval so the spec can wait a couple of seconds and
+# see the periodic export tick after fork. The OTel SDK reads this env
+# var when constructing the `PeriodicMetricReader`, so it has to be set
+# before `Appsignal.start` boots the OTel providers.
+ENV["OTEL_METRIC_EXPORT_INTERVAL"] = "500" # ms
+
+PROJECT_ROOT = "../../../".freeze
+$LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT))
+$LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT))
+
+require "appsignal"
+
+# Name, environment and push API key come from the env vars the Runner
+# injects (see `Runner::DEFAULT_ENV`); `Appsignal.start` loads them.
+Appsignal.start
+
+# In the child: emit a metric and wait long enough for the periodic
+# exporter to tick. We deliberately do NOT call any fork-aware code
+# (no `Appsignal.forked`, no `force_flush`, no `Appsignal.stop`) — the
+# OTel SDK's built-in fork hooks should restart the background reader
+# thread on its own, triggered by `Appsignal::OpenTelemetry.configure`
+# having called `OpenTelemetry::SDK.configure` at boot time.
+child_pid = Process.fork do
+ Appsignal.increment_counter("forked_child_counter", 1)
+ sleep 2
+rescue => e
+ warn "child failed: #{e.class}: #{e.message}"
+ warn e.backtrace
+ exit!(1)
+end
+
+_, status = Process.waitpid2(child_pid)
+exit(status.exitstatus || 1)
diff --git a/spec/integration/runners/collector_mode_log_trace_correlation.rb b/spec/integration/runners/collector_mode_log_trace_correlation.rb
new file mode 100644
index 000000000..3e140385c
--- /dev/null
+++ b/spec/integration/runners/collector_mode_log_trace_correlation.rb
@@ -0,0 +1,27 @@
+PROJECT_ROOT = "../../../".freeze
+$LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT))
+$LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT))
+
+require "appsignal"
+
+# Name, environment and push API key come from the env vars the Runner
+# injects (see `Runner::DEFAULT_ENV`); `Appsignal.start` loads them.
+Appsignal.start
+
+logger = Appsignal::Logger.new("correlation-group")
+
+# Emit one log under the root span (no active event), one nested inside
+# an `Appsignal.instrument` event, and one after the event closes. Each
+# should carry the trace_id/span_id of whichever span was current at
+# emit time.
+Appsignal.monitor(:action => "TestAction") do
+ logger.info("before event")
+ Appsignal.instrument("test.event") do
+ logger.info("inside event")
+ end
+ logger.info("after event")
+end
+
+Appsignal.stop("integration test")
+
+puts "DONE"
diff --git a/spec/integration/runners/collector_mode_logs.rb b/spec/integration/runners/collector_mode_logs.rb
new file mode 100644
index 000000000..61dbb0850
--- /dev/null
+++ b/spec/integration/runners/collector_mode_logs.rb
@@ -0,0 +1,29 @@
+PROJECT_ROOT = "../../../".freeze
+$LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT))
+$LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT))
+
+require "appsignal"
+
+# Name, environment and push API key come from the env vars the Runner
+# injects (see `Runner::DEFAULT_ENV`); `Appsignal.start` loads them.
+Appsignal.start
+
+# Exercise Appsignal::Logger under collector mode: in this mode each emit
+# should route through Appsignal::Logger::OpenTelemetryBackend and reach
+# the mock collector at the configured `/v1/logs` endpoint.
+logger = Appsignal::Logger.new(
+ "my-group",
+ :level => ::Logger::DEBUG,
+ :format => Appsignal::Logger::JSON,
+ :attributes => { "service" => "runner" }
+)
+
+logger.info("info line", :tag => "value")
+logger.warn("warn line")
+logger.error("error line")
+
+# Shut AppSignal down so the OTel providers drain their buffers and the
+# spec sees the queued request deterministically.
+Appsignal.stop("integration test")
+
+puts "DONE"
diff --git a/spec/integration/runners/collector_mode_metrics.rb b/spec/integration/runners/collector_mode_metrics.rb
new file mode 100644
index 000000000..7ec06e79b
--- /dev/null
+++ b/spec/integration/runners/collector_mode_metrics.rb
@@ -0,0 +1,22 @@
+PROJECT_ROOT = "../../../".freeze
+$LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT))
+$LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT))
+
+require "appsignal"
+
+# Name, environment and push API key come from the env vars the Runner
+# injects (see `Runner::DEFAULT_ENV`); `Appsignal.start` loads them.
+Appsignal.start
+
+# Exercise the public AppSignal metric helpers; in collector mode these
+# should route through the OpenTelemetry backend and reach the mock
+# collector at the configured `/v1/metrics` endpoint.
+Appsignal.increment_counter("test_counter", 1, :tag => "value")
+Appsignal.set_gauge("test_gauge", 42.5, :tag => "value")
+Appsignal.add_distribution_value("test_distribution", 0.123, :tag => "value")
+
+# Shut AppSignal down so the OTel providers drain their buffers and the
+# spec sees the queued request deterministically.
+Appsignal.stop("integration test")
+
+puts "DONE"
diff --git a/spec/integration/runners/collector_mode_mixed_api.rb b/spec/integration/runners/collector_mode_mixed_api.rb
new file mode 100644
index 000000000..9ef3eaaca
--- /dev/null
+++ b/spec/integration/runners/collector_mode_mixed_api.rb
@@ -0,0 +1,38 @@
+PROJECT_ROOT = "../../../".freeze
+$LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT))
+$LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT))
+
+require "appsignal"
+
+# Name, environment and push API key come from the env vars the Runner
+# injects (see `Runner::DEFAULT_ENV`); `Appsignal.start` loads them.
+Appsignal.start
+
+tracer = OpenTelemetry.tracer_provider.tracer("integration-test")
+
+# Outer raw OTel span. `Appsignal.monitor` is called while this span is
+# current; the monitor's root must NOT inherit this as its parent.
+tracer.in_span("outer.otel") do
+ Appsignal.monitor(:action => "MonitoredAction") do
+ # A raw OTel span created inside an `Appsignal.instrument` block
+ # should be a child of the event span.
+ Appsignal.instrument("event.with.otel.child") do
+ tracer.in_span("inner.otel.inside_instrument") do
+ # No body; the span's parentage is what the spec asserts on.
+ end
+ end
+
+ # In reverse: when a raw OTel span is current, an
+ # `Appsignal.instrument` invoked inside it should produce an event
+ # span that is a child of that OTel span (not of the monitor root).
+ tracer.in_span("manual.otel.in_monitor") do
+ Appsignal.instrument("event.under.manual.otel") do
+ # No body; the span's parentage is what the spec asserts on.
+ end
+ end
+ end
+end
+
+Appsignal.stop("integration test")
+
+puts "DONE"
diff --git a/spec/integration/runners/collector_mode_stop_flush.rb b/spec/integration/runners/collector_mode_stop_flush.rb
new file mode 100644
index 000000000..94db756a9
--- /dev/null
+++ b/spec/integration/runners/collector_mode_stop_flush.rb
@@ -0,0 +1,23 @@
+PROJECT_ROOT = "../../../".freeze
+$LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT))
+$LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT))
+
+require "appsignal"
+
+# Name, environment and push API key come from the env vars the Runner
+# injects (see `Runner::DEFAULT_ENV`); `Appsignal.start` loads them.
+Appsignal.start
+
+# Emit one of each signal but deliberately do NOT call `force_flush`
+# anywhere. The PeriodicMetricReader and BatchLogRecordProcessor buffer
+# data for export at their configured interval, so anything arriving at
+# the mock collector before the runner exits has to be because
+# `Appsignal.stop` shut the OTel providers down (which flushes them).
+Appsignal.increment_counter("stop_counter", 1)
+
+logger = Appsignal::Logger.new("stop-group")
+logger.info("stop log line")
+
+Appsignal.stop("integration test")
+
+puts "DONE"
diff --git a/spec/integration/runners/collector_mode_traces.rb b/spec/integration/runners/collector_mode_traces.rb
new file mode 100644
index 000000000..d19ca88c7
--- /dev/null
+++ b/spec/integration/runners/collector_mode_traces.rb
@@ -0,0 +1,30 @@
+PROJECT_ROOT = "../../../".freeze
+$LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT))
+$LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT))
+
+require "appsignal"
+
+# Name, environment and push API key come from the env vars the Runner
+# injects (see `Runner::DEFAULT_ENV`); `Appsignal.start` loads them.
+Appsignal.start
+
+# Exercise the public AppSignal tracing helpers; in collector mode these
+# should route through the OpenTelemetry transaction backend and produce
+# a root span plus nested event spans reaching the mock collector at
+# `/v1/traces`.
+Appsignal.monitor(:action => "MyController#index") do
+ Appsignal.instrument_sql("active_record.sql", "Find user", "SELECT * FROM users") do
+ # No body; the SQL event span itself is what the spec asserts on.
+ end
+ Appsignal.instrument("template.render") do
+ Appsignal.instrument("partial.render") do
+ # No body; the nested event span itself is what the spec asserts on.
+ end
+ end
+end
+
+# Shut AppSignal down so the OTel providers drain their buffers and the
+# spec sees the queued request deterministically.
+Appsignal.stop("integration test")
+
+puts "DONE"
diff --git a/spec/integration/runners/stop_with_trap.rb b/spec/integration/runners/stop_with_trap.rb
index adb9e5938..73aaa3c6b 100644
--- a/spec/integration/runners/stop_with_trap.rb
+++ b/spec/integration/runners/stop_with_trap.rb
@@ -2,7 +2,6 @@
$LOAD_PATH.unshift(File.expand_path("ext", PROJECT_ROOT))
$LOAD_PATH.unshift(File.expand_path("lib", PROJECT_ROOT))
-require "fileutils"
require "appsignal"
Signal.trap("USR1") do
@@ -12,20 +11,8 @@
exit 0
end
-# Dummy config
-Appsignal.configure(:test) do |config|
- config.active = true
- config.push_api_key = "abc"
- config.name = "Signal app"
-
- # Use a working directory in the runner's tmp dir to avoid conflicts with the
- # host's /tmp dir
- working_directory = "tmp/appsignal"
- FileUtils.rm_f(working_directory)
- FileUtils.mkdir_p(working_directory)
- config.working_directory_path = File.join(__dir__, working_directory)
-end
-
+# Name, environment and push API key come from the env vars the Runner
+# injects (see `Runner::DEFAULT_ENV`); `Appsignal.start` loads them.
Appsignal.start
puts "Waiting for USR1 signal..."
diff --git a/spec/integration/stop_spec.rb b/spec/integration/stop_spec.rb
index 9b66410d0..b40a9e079 100644
--- a/spec/integration/stop_spec.rb
+++ b/spec/integration/stop_spec.rb
@@ -15,9 +15,6 @@
output = runner.output
- # Make sure the app exited properly
- expect(runner.status.exitstatus).to eq(0)
-
# Assert the output has no errors
expect(output).to_not include("ERROR: ")
# Assert the app has started as expected
diff --git a/spec/lib/appsignal/backends_spec.rb b/spec/lib/appsignal/backends_spec.rb
new file mode 100644
index 000000000..41f582dc2
--- /dev/null
+++ b/spec/lib/appsignal/backends_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+describe Appsignal::Backends do
+ describe ".metrics" do
+ context "when no config is loaded" do
+ before { allow(Appsignal).to receive(:config).and_return(nil) }
+
+ it "returns the extension backend" do
+ expect(described_class.metrics).to eq(Appsignal::Metrics::ExtensionBackend)
+ end
+ end
+
+ context "when collector mode is not active" do
+ before do
+ config = instance_double(Appsignal::Config, :collector_mode? => false)
+ allow(Appsignal).to receive(:config).and_return(config)
+ end
+
+ it "returns the extension backend" do
+ expect(described_class.metrics).to eq(Appsignal::Metrics::ExtensionBackend)
+ end
+ end
+
+ context "when collector mode is active" do
+ before do
+ config = instance_double(Appsignal::Config, :collector_mode? => true)
+ allow(Appsignal).to receive(:config).and_return(config)
+ end
+
+ it "returns the OpenTelemetry backend" do
+ expect(described_class.metrics).to eq(Appsignal::Metrics::OpenTelemetryBackend)
+ end
+ end
+ end
+
+ describe ".logger" do
+ context "when no config is loaded" do
+ before { allow(Appsignal).to receive(:config).and_return(nil) }
+
+ it "returns the extension backend" do
+ expect(described_class.logger).to eq(Appsignal::Logger::ExtensionBackend)
+ end
+ end
+
+ context "when collector mode is not active" do
+ before do
+ config = instance_double(Appsignal::Config, :collector_mode? => false)
+ allow(Appsignal).to receive(:config).and_return(config)
+ end
+
+ it "returns the extension backend" do
+ expect(described_class.logger).to eq(Appsignal::Logger::ExtensionBackend)
+ end
+ end
+
+ context "when collector mode is active" do
+ before do
+ config = instance_double(Appsignal::Config, :collector_mode? => true)
+ allow(Appsignal).to receive(:config).and_return(config)
+ end
+
+ it "returns the OpenTelemetry backend" do
+ expect(described_class.logger).to eq(Appsignal::Logger::OpenTelemetryBackend)
+ end
+ end
+ end
+
+ describe ".transaction" do
+ context "when no config is loaded" do
+ before { allow(Appsignal).to receive(:config).and_return(nil) }
+
+ it "returns the extension backend" do
+ expect(described_class.transaction).to eq(Appsignal::Transaction::ExtensionBackend)
+ end
+ end
+
+ context "when collector mode is not active" do
+ before do
+ config = instance_double(Appsignal::Config, :collector_mode? => false)
+ allow(Appsignal).to receive(:config).and_return(config)
+ end
+
+ it "returns the extension backend" do
+ expect(described_class.transaction).to eq(Appsignal::Transaction::ExtensionBackend)
+ end
+ end
+
+ context "when collector mode is active" do
+ before do
+ config = instance_double(Appsignal::Config, :collector_mode? => true)
+ allow(Appsignal).to receive(:config).and_return(config)
+ end
+
+ it "returns the OpenTelemetry backend" do
+ expect(described_class.transaction).to eq(Appsignal::Transaction::OpenTelemetryBackend)
+ end
+ end
+ end
+end
diff --git a/spec/lib/appsignal/config_spec.rb b/spec/lib/appsignal/config_spec.rb
index c5a2fb8c7..08df217dd 100644
--- a/spec/lib/appsignal/config_spec.rb
+++ b/spec/lib/appsignal/config_spec.rb
@@ -709,6 +709,7 @@ def on_load
:activejob_report_errors => "all",
:bind_address => "0.0.0.0",
:ca_file_path => "/some/path",
+ :collector_endpoint => "http://collector.example.test:4318",
:cpu_count => 1.5,
:dns_servers => ["8.8.8.8", "8.8.4.4"],
:enable_allocation_tracking => false,
@@ -725,8 +726,12 @@ def on_load
:enable_statsd => false,
:endpoint => "https://test.appsignal.com",
:files_world_accessible => false,
+ :filter_attributes => ["attr1", "attr2"],
+ :filter_function_parameters => ["fn1", "fn2"],
:filter_metadata => ["key1", "key2"],
:filter_parameters => ["param1", "param2"],
+ :filter_request_payload => ["payload1", "payload2"],
+ :filter_request_query_parameters => ["query1", "query2"],
:filter_session_data => ["session1", "session2"],
:host_role => "my host role",
:hostname => "my hostname",
@@ -750,11 +755,16 @@ def on_load
:ownership_set_namespace => true,
:push_api_key => "aaa-bbb-ccc",
:request_headers => ["accept", "accept-charset"],
+ :response_headers => ["x-response-1", "x-response-2"],
:revision => "v2.5.1",
:running_in_container => true,
:send_environment_metadata => false,
+ :send_function_parameters => true,
:send_params => false,
+ :send_request_payload => true,
+ :send_request_query_parameters => true,
:send_session_data => false,
+ :service_name => "my-service",
:sidekiq_report_errors => "all",
:statsd_port => "7890",
:working_directory_path => working_directory_path,
@@ -768,6 +778,7 @@ def on_load
"APPSIGNAL_APP_NAME" => "App name",
"APPSIGNAL_BIND_ADDRESS" => "0.0.0.0",
"APPSIGNAL_CA_FILE_PATH" => "/some/path",
+ "APPSIGNAL_COLLECTOR_ENDPOINT" => "http://collector.example.test:4318",
"APPSIGNAL_ENABLE_AT_EXIT_HOOK" => "never",
"APPSIGNAL_HOSTNAME" => "my hostname",
"APPSIGNAL_HOST_ROLE" => "my host role",
@@ -778,6 +789,7 @@ def on_load
"APPSIGNAL_LOG_PATH" => "/tmp/something",
"APPSIGNAL_PUSH_API_ENDPOINT" => "https://test.appsignal.com",
"APPSIGNAL_PUSH_API_KEY" => "aaa-bbb-ccc",
+ "APPSIGNAL_SERVICE_NAME" => "my-service",
"APPSIGNAL_SIDEKIQ_REPORT_ERRORS" => "all",
"APPSIGNAL_STATSD_PORT" => "7890",
"APPSIGNAL_NGINX_PORT" => "4321",
@@ -808,19 +820,27 @@ def on_load
"APPSIGNAL_OWNERSHIP_SET_NAMESPACE" => "true",
"APPSIGNAL_RUNNING_IN_CONTAINER" => "true",
"APPSIGNAL_SEND_ENVIRONMENT_METADATA" => "false",
+ "APPSIGNAL_SEND_FUNCTION_PARAMETERS" => "true",
"APPSIGNAL_SEND_PARAMS" => "false",
+ "APPSIGNAL_SEND_REQUEST_PAYLOAD" => "true",
+ "APPSIGNAL_SEND_REQUEST_QUERY_PARAMETERS" => "true",
"APPSIGNAL_SEND_SESSION_DATA" => "false",
# Arrays
"APPSIGNAL_DNS_SERVERS" => "8.8.8.8,8.8.4.4",
+ "APPSIGNAL_FILTER_ATTRIBUTES" => "attr1,attr2",
+ "APPSIGNAL_FILTER_FUNCTION_PARAMETERS" => "fn1,fn2",
"APPSIGNAL_FILTER_METADATA" => "key1,key2",
"APPSIGNAL_FILTER_PARAMETERS" => "param1,param2",
+ "APPSIGNAL_FILTER_REQUEST_PAYLOAD" => "payload1,payload2",
+ "APPSIGNAL_FILTER_REQUEST_QUERY_PARAMETERS" => "query1,query2",
"APPSIGNAL_FILTER_SESSION_DATA" => "session1,session2",
"APPSIGNAL_IGNORE_ACTIONS" => "action1,action2",
"APPSIGNAL_IGNORE_ERRORS" => "ExampleStandardError,AnotherError",
"APPSIGNAL_IGNORE_LOGS" => "^start$,^Completed 2.* in .*ms (.*)",
"APPSIGNAL_IGNORE_NAMESPACES" => "admin,private_namespace",
"APPSIGNAL_REQUEST_HEADERS" => "accept,accept-charset",
+ "APPSIGNAL_RESPONSE_HEADERS" => "x-response-1,x-response-2",
# Floats
"APPSIGNAL_CPU_COUNT" => "1.5"
@@ -919,6 +939,7 @@ def on_load
:active => true,
:activejob_report_errors => "all",
:ca_file_path => File.join(resources_dir, "cacert.pem"),
+ :collector_endpoint => nil,
:dns_servers => [],
:enable_allocation_tracking => true,
:enable_at_exit_hook => "on_error",
@@ -934,8 +955,12 @@ def on_load
:enable_rake_performance_instrumentation => false,
:endpoint => "https://push.appsignal.com",
:files_world_accessible => true,
+ :filter_attributes => [],
+ :filter_function_parameters => [],
:filter_metadata => [],
:filter_parameters => [],
+ :filter_request_payload => [],
+ :filter_request_query_parameters => [],
:filter_session_data => [],
:ignore_actions => [],
:ignore_errors => [],
@@ -954,10 +979,15 @@ def on_load
:ownership_set_namespace => false,
:push_api_key => "abc",
:request_headers => [],
+ :response_headers => [],
:revision => "v2.5.1",
:send_environment_metadata => true,
+ :send_function_parameters => nil,
:send_params => true,
+ :send_request_payload => nil,
+ :send_request_query_parameters => nil,
:send_session_data => true,
+ :service_name => nil,
:sidekiq_report_errors => "all",
:default_tags => {}
)
@@ -1609,6 +1639,197 @@ def log_file_path
end
end
+ describe "#collector_mode_configured?" do
+ let(:options) { {} }
+ let(:config) { build_config(:root_path => "", :env => nil, :options => options) }
+ subject { config.collector_mode_configured? }
+ # Stub to the gate's minimum so the "happy path" contexts pass on Ruby < 3.1.
+ # The "when running on Ruby older..." context below stubs to an older version
+ # to exercise the gate path on every CI Ruby.
+ before { stub_const("RUBY_VERSION", Appsignal::Config::MIN_RUBY_VERSION_FOR_COLLECTOR_MODE) }
+
+ context "when :collector_endpoint is not set" do
+ it { is_expected.to be(false) }
+ end
+
+ context "when :collector_endpoint is nil" do
+ let(:options) { { :collector_endpoint => nil } }
+ it { is_expected.to be(false) }
+ end
+
+ context "when :collector_endpoint is an empty string" do
+ let(:options) { { :collector_endpoint => "" } }
+ it { is_expected.to be(false) }
+ end
+
+ context "when :collector_endpoint is whitespace only" do
+ let(:options) { { :collector_endpoint => " " } }
+ it { is_expected.to be(false) }
+ end
+
+ context "when :collector_endpoint is set" do
+ let(:options) { { :collector_endpoint => "http://127.0.0.1:9090" } }
+ it { is_expected.to be(true) }
+ end
+
+ context "when :collector_endpoint is set via APPSIGNAL_COLLECTOR_ENDPOINT" do
+ let(:config) do
+ ENV["APPSIGNAL_COLLECTOR_ENDPOINT"] = "http://127.0.0.1:9090"
+ build_config(:root_path => "", :env => nil, :options => {})
+ end
+
+ it { is_expected.to be(true) }
+ end
+
+ context "when running on Ruby older than the minimum supported version" do
+ let(:options) { { :collector_endpoint => "http://127.0.0.1:9090" } }
+ let(:err_stream) { std_stream }
+ before { stub_const("RUBY_VERSION", "3.0.7") }
+
+ it "forces collector mode off and warns the user" do
+ logs =
+ capture_logs do
+ capture_std_streams(std_stream, err_stream) do
+ expect(config.collector_mode_configured?).to be(false)
+ end
+ end
+
+ message =
+ "Collector mode requires Ruby " \
+ "#{Appsignal::Config::MIN_RUBY_VERSION_FOR_COLLECTOR_MODE} or higher " \
+ "(running Ruby 3.0.7)"
+ expect(logs).to include(message)
+ expect(err_stream.read).to include("appsignal WARNING: #{message}")
+ end
+
+ it "memoizes the result so the warning is emitted at most once" do
+ logs =
+ capture_logs do
+ capture_std_streams(std_stream, err_stream) do
+ 3.times { config.collector_mode_configured? }
+ end
+ end
+
+ expect(logs.scan("Collector mode requires").length).to eq(1)
+ end
+ end
+ end
+
+ describe "#collector_mode?" do
+ let(:options) { { :collector_endpoint => "http://127.0.0.1:9090" } }
+ let(:config) { build_config(:root_path => "", :env => nil, :options => options) }
+ subject { config.collector_mode? }
+ before { stub_const("RUBY_VERSION", Appsignal::Config::MIN_RUBY_VERSION_FOR_COLLECTOR_MODE) }
+
+ context "when collector mode is configured and OpenTelemetry has started" do
+ before { allow(Appsignal::OpenTelemetry).to receive(:started?).and_return(true) }
+
+ it { is_expected.to be(true) }
+ end
+
+ context "when collector mode is configured but OpenTelemetry hasn't started" do
+ before { allow(Appsignal::OpenTelemetry).to receive(:started?).and_return(false) }
+
+ it { is_expected.to be(false) }
+ end
+
+ context "when collector mode is not configured" do
+ let(:options) { {} }
+ it { is_expected.to be(false) }
+ end
+ end
+
+ describe "#warn_for_mode_mismatch" do
+ let(:options) { {} }
+ let(:config) { build_config(:options => options) }
+
+ context "when in collector mode" do
+ let(:collector_options) do
+ { :collector_endpoint => "http://127.0.0.1:9090" }
+ end
+ before { stub_const("RUBY_VERSION", Appsignal::Config::MIN_RUBY_VERSION_FOR_COLLECTOR_MODE) }
+
+ it "warns when filter_parameters is set" do
+ logs =
+ capture_logs do
+ build_config(:options => collector_options.merge(:filter_parameters => ["password"]))
+ end
+ expect(logs).to include("filter_parameters")
+ expect(logs).to include("only used by the agent")
+ end
+
+ it "warns when send_params is set" do
+ logs =
+ capture_logs do
+ build_config(:options => collector_options.merge(:send_params => false))
+ end
+ expect(logs).to include("send_params")
+ expect(logs).to include("only used by the agent")
+ end
+
+ it "does not warn when only filter_attributes is set" do
+ logs =
+ capture_logs do
+ build_config(:options => collector_options.merge(:filter_attributes => ["password"]))
+ end
+ expect(logs).to_not include("only used by the agent")
+ expect(logs).to_not include("only used by the collector")
+ end
+
+ it "does not warn when an agent-only option is explicitly set to its default" do
+ # send_params defaults to true; setting it to true is a no-op and
+ # shouldn't trigger a mode-mismatch warning.
+ logs =
+ capture_logs do
+ build_config(:options => collector_options.merge(:send_params => true))
+ end
+ expect(logs).to_not include("only used by the agent")
+ expect(logs).to_not include("only used by the collector")
+ end
+ end
+
+ context "when not in collector mode" do
+ it "warns when a collector-only option is set" do
+ logs =
+ capture_logs do
+ build_config(:options => { :filter_attributes => ["password"] })
+ end
+ expect(logs).to include("filter_attributes")
+ expect(logs).to include("only used by the collector")
+ end
+
+ it "warns when service_name is set" do
+ logs =
+ capture_logs do
+ build_config(:options => { :service_name => "my-service" })
+ end
+ expect(logs).to include("service_name")
+ expect(logs).to include("only used by the collector")
+ end
+
+ it "does not warn when only filter_parameters is set" do
+ logs =
+ capture_logs do
+ build_config(:options => { :filter_parameters => ["password"] })
+ end
+ expect(logs).to_not include("only used by the agent")
+ expect(logs).to_not include("only used by the collector")
+ end
+
+ it "does not warn when a collector-only option is explicitly set to its default" do
+ # filter_attributes defaults to []; setting it to [] is a no-op and
+ # shouldn't trigger a mode-mismatch warning even though the source
+ # dictionary recorded the assignment.
+ logs =
+ capture_logs do
+ build_config(:options => { :filter_attributes => [] })
+ end
+ expect(logs).to_not include("only used by the agent")
+ expect(logs).to_not include("only used by the collector")
+ end
+ end
+ end
+
describe Appsignal::Config::ConfigDSL do
let(:env) { :production }
let(:options) { {} }
diff --git a/spec/lib/appsignal/hooks/action_cable_spec.rb b/spec/lib/appsignal/hooks/action_cable_spec.rb
index a1b67e936..b5b4e4041 100644
--- a/spec/lib/appsignal/hooks/action_cable_spec.rb
+++ b/spec/lib/appsignal/hooks/action_cable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
describe Appsignal::Hooks::ActionCableHook do
if DependencyHelper.action_cable_present?
context "with ActionCable" do
@@ -52,8 +54,6 @@ def self.to_s
let(:request_id) { SecureRandom.uuid }
let(:instance) { channel.new(connection, identifier, params) }
before do
- start_agent
-
# Stub transmit call for subscribe/unsubscribe tests
allow(connection).to receive(:websocket)
.and_return(instance_double("ActionCable::Connection::WebSocket", :transmit => nil))
@@ -61,132 +61,104 @@ def self.to_s
around { |example| keep_transactions { example.run } }
describe "#perform_action" do
- it "creates a transaction for an action" do
- instance.perform_action("message" => "foo", "action" => "speak")
-
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
- expect(transaction).to have_action("MyChannel#speak")
- expect(transaction).to_not have_error
- expect(transaction).to include_metadata(
- "method" => "websocket",
- "path" => "/blog"
- )
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "perform_action.action_cable",
- "title" => ""
- )
- expect(transaction).to include_params(
- "action" => "speak",
- "message" => "foo"
- )
- expect(transaction).to include_session_data(
- "user_id" => "123",
- "session" => "yes"
- )
- expect(transaction).to include_tags("request_id" => request_id)
- expect(transaction).to_not have_queue_start
- expect(transaction).to be_completed
- end
-
- context "without request_id (standalone server)" do
- let(:request_id) { nil }
-
- it "sets a generated request ID" do
- # Subscribe action, sets the request_id
- instance.subscribe_to_channel
-
+ describe "creates a transaction for an action" do
+ def perform
instance.perform_action("message" => "foo", "action" => "speak")
- expect(last_transaction).to include_tags("request_id" => kind_of(String))
end
- end
- context "with an error in the action" do
- let(:channel) do
- Class.new(ActionCable::Channel::Base) do
- def speak(_data)
- raise ExampleException, "oh no!"
- end
-
- def self.to_s
- "MyChannel"
- end
- end
- end
-
- it "registers an error on the transaction" do
- expect do
- instance.perform_action("message" => "foo", "action" => "speak")
- end.to raise_error(ExampleException)
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction = last_transaction
expect(transaction).to have_id
- expect(transaction).to have_action("MyChannel#speak")
expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
- expect(transaction).to have_error("ExampleException", "oh no!")
+ expect(transaction).to have_action("MyChannel#speak")
+ expect(transaction).to_not have_error
expect(transaction).to include_metadata(
"method" => "websocket",
"path" => "/blog"
)
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "perform_action.action_cable",
+ "title" => ""
+ )
expect(transaction).to include_params(
"action" => "speak",
"message" => "foo"
)
+ expect(transaction).to include_session_data(
+ "user_id" => "123",
+ "session" => "yes"
+ )
+ expect(transaction).to include_tags("request_id" => request_id)
expect(transaction).to_not have_queue_start
expect(transaction).to be_completed
end
- end
- end
-
- describe "subscribe callback" do
- let(:params) { { "internal" => true } }
- it "creates a transaction for a subscription" do
- instance.subscribe_to_channel
-
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_action("MyChannel#subscribed")
- expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
- expect(transaction).to_not have_error
- expect(transaction).to include_metadata(
- "method" => "websocket",
- "path" => "/blog"
- )
- expect(transaction).to include_params("internal" => "true")
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "subscribed.action_cable",
- "title" => ""
- )
- expect(transaction).to include_session_data(
- "user_id" => "123",
- "session" => "yes"
- )
- expect(transaction).to include_tags("request_id" => request_id)
- expect(transaction).to_not have_queue_start
- expect(transaction).to be_completed
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.name).to eq("MyChannel#speak")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq(Appsignal::Transaction::ACTION_CABLE)
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MyChannel#speak")
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("websocket")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/blog")
+ span = event_spans.find { |s| s.name == "perform_action.action_cable" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("action" => "speak", "message" => "foo")
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("user_id" => "123", "session" => "yes")
+ expect(root_span.attributes["appsignal.tag.request_id"]).to eq(request_id)
+ expect(root_span.attributes).not_to have_key("queue_start")
+ expect(last_transaction).to be_completed
+ end
end
context "without request_id (standalone server)" do
let(:request_id) { nil }
- before { instance.subscribe_to_channel }
- it "sets a generated request ID" do
- expect(last_transaction).to include_tags("request_id" => kind_of(String))
+ describe "sets a generated request ID" do
+ def perform
+ # Subscribe action sets the request_id in the env
+ instance.subscribe_to_channel
+ instance.perform_action("message" => "foo", "action" => "speak")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ expect(last_transaction).to include_tags("request_id" => kind_of(String))
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ # Two server spans: one for subscribe, one for perform_action.
+ # The last one is the perform_action span.
+ perform_span = span_exporter.finished_spans
+ .select { |s| [:server, :consumer].include?(s.kind) }
+ .last
+ expect(perform_span.attributes["appsignal.tag.request_id"])
+ .to be_a(String)
+ end
end
end
- context "with an error in the callback" do
+ context "with an error in the action" do
let(:channel) do
Class.new(ActionCable::Channel::Base) do
- def subscribed
+ def speak(_data)
raise ExampleException, "oh no!"
end
@@ -196,124 +168,289 @@ def self.to_s
end
end
- it "registers an error on the transaction" do
- expect do
- instance.subscribe_to_channel
- end.to raise_error(ExampleException)
+ describe "registers an error on the transaction" do
+ def perform
+ expect do
+ instance.perform_action("message" => "foo", "action" => "speak")
+ end.to raise_error(ExampleException)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_action("MyChannel#speak")
+ expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
+ expect(transaction).to have_error("ExampleException", "oh no!")
+ expect(transaction).to include_metadata(
+ "method" => "websocket",
+ "path" => "/blog"
+ )
+ expect(transaction).to include_params(
+ "action" => "speak",
+ "message" => "foo"
+ )
+ expect(transaction).to_not have_queue_start
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MyChannel#speak")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq(Appsignal::Transaction::ACTION_CABLE)
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("oh no!")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("websocket")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/blog")
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("action" => "speak", "message" => "foo")
+ expect(root_span.attributes).not_to have_key("queue_start")
+ expect(last_transaction).to be_completed
+ end
+ end
+ end
+ end
+
+ describe "subscribe callback" do
+ let(:params) { { "internal" => true } }
+
+ describe "creates a transaction for a subscription" do
+ def perform
+ instance.subscribe_to_channel
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction = last_transaction
expect(transaction).to have_id
expect(transaction).to have_action("MyChannel#subscribed")
expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
- expect(transaction).to have_error("ExampleException", "oh no!")
+ expect(transaction).to_not have_error
expect(transaction).to include_metadata(
"method" => "websocket",
"path" => "/blog"
)
expect(transaction).to include_params("internal" => "true")
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "subscribed.action_cable",
+ "title" => ""
+ )
expect(transaction).to include_session_data(
"user_id" => "123",
"session" => "yes"
)
+ expect(transaction).to include_tags("request_id" => request_id)
expect(transaction).to_not have_queue_start
expect(transaction).to be_completed
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.name).to eq("MyChannel#subscribed")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("MyChannel#subscribed")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq(Appsignal::Transaction::ACTION_CABLE)
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("websocket")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/blog")
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("internal" => "true")
+ span = event_spans.find { |s| s.name == "subscribed.action_cable" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("user_id" => "123", "session" => "yes")
+ expect(root_span.attributes["appsignal.tag.request_id"]).to eq(request_id)
+ expect(root_span.attributes).not_to have_key("queue_start")
+ expect(last_transaction).to be_completed
+ end
end
- if DependencyHelper.rails6_present?
- context "with ConnectionStub" do
- let(:connection) { ActionCable::Channel::ConnectionStub.new }
+ context "without request_id (standalone server)" do
+ let(:request_id) { nil }
- it "does not fail on missing `#env` method on `ConnectionStub`" do
+ describe "sets a generated request ID" do
+ def perform
instance.subscribe_to_channel
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ expect(last_transaction).to include_tags("request_id" => kind_of(String))
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ end
+ end
+ end
+
+ context "with an error in the callback" do
+ let(:channel) do
+ Class.new(ActionCable::Channel::Base) do
+ def subscribed
+ raise ExampleException, "oh no!"
+ end
+
+ def self.to_s
+ "MyChannel"
+ end
+ end
+ end
+
+ describe "registers an error on the transaction" do
+ def perform
+ expect do
+ instance.subscribe_to_channel
+ end.to raise_error(ExampleException)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction = last_transaction
expect(transaction).to have_id
expect(transaction).to have_action("MyChannel#subscribed")
expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
- expect(transaction).to_not have_error
+ expect(transaction).to have_error("ExampleException", "oh no!")
expect(transaction).to include_metadata(
"method" => "websocket",
- "path" => "" # No path as the ConnectionStub doesn't have the real request env
+ "path" => "/blog"
)
- expect(transaction).to_not include_params
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "subscribed.action_cable",
- "title" => ""
+ expect(transaction).to include_params("internal" => "true")
+ expect(transaction).to include_session_data(
+ "user_id" => "123",
+ "session" => "yes"
)
expect(transaction).to_not have_queue_start
expect(transaction).to be_completed
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("MyChannel#subscribed")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq(Appsignal::Transaction::ACTION_CABLE)
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("oh no!")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("websocket")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/blog")
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("internal" => "true")
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("user_id" => "123", "session" => "yes")
+ expect(root_span.attributes).not_to have_key("queue_start")
+ expect(last_transaction).to be_completed
+ end
end
end
- end
- describe "unsubscribe callback" do
- let(:params) { { "internal" => true } }
+ if DependencyHelper.rails6_present?
+ context "with ConnectionStub" do
+ let(:connection) { ActionCable::Channel::ConnectionStub.new }
- it "creates a transaction for a subscription" do
- instance.unsubscribe_from_channel
-
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_action("MyChannel#unsubscribed")
- expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
- expect(transaction).to_not have_error
- expect(transaction).to include_metadata(
- "method" => "websocket",
- "path" => "/blog"
- )
- expect(transaction).to include_params("internal" => "true")
- expect(transaction).to include_session_data(
- "user_id" => "123",
- "session" => "yes"
- )
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "unsubscribed.action_cable",
- "title" => ""
- )
- expect(transaction).to_not have_queue_start
- expect(transaction).to be_completed
- end
+ describe "does not fail on missing `#env` on `ConnectionStub`" do
+ def perform
+ instance.subscribe_to_channel
+ end
- context "without request_id (standalone server)" do
- let(:request_id) { nil }
- before { instance.unsubscribe_from_channel }
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_action("MyChannel#subscribed")
+ expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
+ expect(transaction).to_not have_error
+ expect(transaction).to include_metadata(
+ "method" => "websocket",
+ "path" => "" # No path as the ConnectionStub doesn't have the real request env
+ )
+ expect(transaction).to_not include_params
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "subscribed.action_cable",
+ "title" => ""
+ )
+ expect(transaction).to_not have_queue_start
+ expect(transaction).to be_completed
+ end
- it "sets a generated request ID" do
- expect(last_transaction).to include_tags("request_id" => kind_of(String))
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("MyChannel#subscribed")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq(Appsignal::Transaction::ACTION_CABLE)
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("websocket")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("")
+ # ConnectionStub has no request env; params are empty in OTel
+ expect(JSON.parse(root_span.attributes.fetch("appsignal.request.payload", "{}")))
+ .to eq({})
+ span = event_spans.find { |s| s.name == "subscribed.action_cable" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(root_span.attributes).not_to have_key("queue_start")
+ expect(last_transaction).to be_completed
+ end
+ end
end
end
+ end
- context "with an error in the callback" do
- let(:channel) do
- Class.new(ActionCable::Channel::Base) do
- def unsubscribed
- raise ExampleException, "oh no!"
- end
+ describe "unsubscribe callback" do
+ let(:params) { { "internal" => true } }
- def self.to_s
- "MyChannel"
- end
- end
+ describe "creates a transaction for an unsubscription" do
+ def perform
+ instance.unsubscribe_from_channel
end
- it "registers an error on the transaction" do
- expect do
- instance.unsubscribe_from_channel
- end.to raise_error(ExampleException)
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction = last_transaction
expect(transaction).to have_id
expect(transaction).to have_action("MyChannel#unsubscribed")
expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
- expect(transaction).to have_error("ExampleException", "oh no!")
+ expect(transaction).to_not have_error
expect(transaction).to include_metadata(
"method" => "websocket",
"path" => "/blog"
@@ -323,38 +460,186 @@ def self.to_s
"user_id" => "123",
"session" => "yes"
)
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "unsubscribed.action_cable",
+ "title" => ""
+ )
expect(transaction).to_not have_queue_start
expect(transaction).to be_completed
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("MyChannel#unsubscribed")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq(Appsignal::Transaction::ACTION_CABLE)
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("websocket")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/blog")
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("internal" => "true")
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("user_id" => "123", "session" => "yes")
+ span = event_spans.find { |s| s.name == "unsubscribed.action_cable" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(root_span.attributes).not_to have_key("queue_start")
+ expect(last_transaction).to be_completed
+ end
end
- if DependencyHelper.rails6_present?
- context "with ConnectionStub" do
- let(:connection) { ActionCable::Channel::ConnectionStub.new }
+ context "without request_id (standalone server)" do
+ let(:request_id) { nil }
- it "does not fail on missing `#env` method on `ConnectionStub`" do
+ describe "sets a generated request ID" do
+ def perform
instance.unsubscribe_from_channel
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ expect(last_transaction).to include_tags("request_id" => kind_of(String))
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ end
+ end
+ end
+
+ context "with an error in the callback" do
+ let(:channel) do
+ Class.new(ActionCable::Channel::Base) do
+ def unsubscribed
+ raise ExampleException, "oh no!"
+ end
+
+ def self.to_s
+ "MyChannel"
+ end
+ end
+ end
+
+ describe "registers an error on the transaction" do
+ def perform
+ expect do
+ instance.unsubscribe_from_channel
+ end.to raise_error(ExampleException)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction = last_transaction
expect(transaction).to have_id
expect(transaction).to have_action("MyChannel#unsubscribed")
expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
- expect(transaction).to_not have_error
+ expect(transaction).to have_error("ExampleException", "oh no!")
expect(transaction).to include_metadata(
"method" => "websocket",
- "path" => "" # No path as the ConnectionStub doesn't have the real request env
+ "path" => "/blog"
)
- expect(transaction).to_not include_params
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "unsubscribed.action_cable",
- "title" => ""
+ expect(transaction).to include_params("internal" => "true")
+ expect(transaction).to include_session_data(
+ "user_id" => "123",
+ "session" => "yes"
)
expect(transaction).to_not have_queue_start
expect(transaction).to be_completed
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("MyChannel#unsubscribed")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq(Appsignal::Transaction::ACTION_CABLE)
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("oh no!")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("websocket")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/blog")
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("internal" => "true")
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("user_id" => "123", "session" => "yes")
+ expect(root_span.attributes).not_to have_key("queue_start")
+ expect(last_transaction).to be_completed
+ end
+ end
+ end
+
+ if DependencyHelper.rails6_present?
+ context "with ConnectionStub" do
+ let(:connection) { ActionCable::Channel::ConnectionStub.new }
+
+ describe "does not fail on missing `#env` on `ConnectionStub`" do
+ def perform
+ instance.unsubscribe_from_channel
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_action("MyChannel#unsubscribed")
+ expect(transaction).to have_namespace(Appsignal::Transaction::ACTION_CABLE)
+ expect(transaction).to_not have_error
+ expect(transaction).to include_metadata(
+ "method" => "websocket",
+ "path" => "" # No path as the ConnectionStub doesn't have the real request env
+ )
+ expect(transaction).to_not include_params
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "unsubscribed.action_cable",
+ "title" => ""
+ )
+ expect(transaction).to_not have_queue_start
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("MyChannel#unsubscribed")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq(Appsignal::Transaction::ACTION_CABLE)
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("websocket")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("")
+ # ConnectionStub has no request env; params are empty in OTel
+ expect(JSON.parse(root_span.attributes.fetch("appsignal.request.payload", "{}")))
+ .to eq({})
+ span = event_spans.find { |s| s.name == "unsubscribed.action_cable" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(root_span.attributes).not_to have_key("queue_start")
+ expect(last_transaction).to be_completed
+ end
+ end
end
end
end
diff --git a/spec/lib/appsignal/hooks/action_mailer_spec.rb b/spec/lib/appsignal/hooks/action_mailer_spec.rb
index 299f760b2..4e08f7aab 100644
--- a/spec/lib/appsignal/hooks/action_mailer_spec.rb
+++ b/spec/lib/appsignal/hooks/action_mailer_spec.rb
@@ -25,12 +25,10 @@ def welcome
end
describe ".install" do
- before do
+ it "in agent mode", :agent_mode do
start_agent
- expect(Appsignal.active?).to be_truthy
- end
- it "is subscribed to 'process.action_mailer' and processes instrumentation" do
+ expect(Appsignal.active?).to be_truthy
expect(Appsignal).to receive(:increment_counter).with(
:action_mailer_process,
1,
@@ -39,6 +37,20 @@ def welcome
UserMailer.welcome.deliver_now
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ expect(Appsignal.active?).to be_truthy
+ UserMailer.welcome.deliver_now
+
+ snapshot = metric_snapshot("action_mailer_process")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.data_points.first.value).to eq(1.0)
+ expect(snapshot.data_points.first.attributes).to eq(
+ "mailer" => "UserMailer", "action" => "welcome"
+ )
+ end
end
end
else
diff --git a/spec/lib/appsignal/hooks/active_support_notifications/finish_with_state_shared_examples.rb b/spec/lib/appsignal/hooks/active_support_notifications/finish_with_state_shared_examples.rb
index 1c8c0098c..aba435cd8 100644
--- a/spec/lib/appsignal/hooks/active_support_notifications/finish_with_state_shared_examples.rb
+++ b/spec/lib/appsignal/hooks/active_support_notifications/finish_with_state_shared_examples.rb
@@ -1,23 +1,76 @@
shared_examples "activesupport finish_with_state override" do
let(:instrumenter) { as.instrumenter }
- it "instruments an ActiveSupport::Notifications.start/finish event with payload on finish" do
- listeners_state = instrumenter.start("sql.active_record", {})
- instrumenter.finish_with_state(listeners_state, "sql.active_record", :sql => "SQL")
-
- expect(transaction).to include_event(
- "body" => "SQL",
- "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
- "count" => 1,
- "name" => "sql.active_record",
- "title" => ""
- )
+ describe "a finish_with_state event" do
+ def perform
+ listeners_state = instrumenter.start("sql.active_record", {})
+ instrumenter.finish_with_state(listeners_state, "sql.active_record", :sql => "SQL")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+
+ expect(transaction).to include_event(
+ "body" => "SQL",
+ "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
+ "count" => 1,
+ "name" => "sql.active_record",
+ "title" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.find { |s| s.name == "sql.active_record" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ # A database query is an outgoing call, so it carries CLIENT kind.
+ expect(span.kind).to eq(:client)
+ expect(span.attributes["db.query.text"]).to eq("SQL")
+ expect(span.attributes["db.system.name"]).to eq("other_sql")
+ end
end
- it "does not instrument events whose name starts with a bang" do
- listeners_state = instrumenter.start("!sql.active_record", {})
- instrumenter.finish_with_state(listeners_state, "!sql.active_record", :sql => "SQL")
+ describe "an event whose name starts with a bang" do
+ def perform
+ listeners_state = instrumenter.start("!sql.active_record", {})
+ instrumenter.finish_with_state(listeners_state, "!sql.active_record", :sql => "SQL")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+
+ expect(transaction).to_not include_events
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+ Appsignal::Transaction.complete_current!
- expect(transaction).to_not include_events
+ expect(event_spans).to be_empty
+ end
end
end
diff --git a/spec/lib/appsignal/hooks/active_support_notifications/instrument_shared_examples.rb b/spec/lib/appsignal/hooks/active_support_notifications/instrument_shared_examples.rb
index 5cddcaf12..a52dc028e 100644
--- a/spec/lib/appsignal/hooks/active_support_notifications/instrument_shared_examples.rb
+++ b/spec/lib/appsignal/hooks/active_support_notifications/instrument_shared_examples.rb
@@ -1,72 +1,200 @@
shared_examples "activesupport instrument override" do
- it "instruments an ActiveSupport::Notifications.instrument event" do
- return_value = as.instrument("sql.active_record", :sql => "SQL") do
- "value"
- end
-
- expect(return_value).to eq "value"
- expect(transaction).to include_event(
- "body" => "SQL",
- "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
- "count" => 1,
- "name" => "sql.active_record",
- "title" => ""
- )
+ describe "an event with a registered formatter" do
+ def perform
+ as.instrument("sql.active_record", :sql => "SQL") { "value" }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ expect(perform).to eq "value"
+ expect(transaction).to include_event(
+ "body" => "SQL",
+ "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
+ "count" => 1,
+ "name" => "sql.active_record",
+ "title" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ expect(perform).to eq "value"
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.find { |s| s.name == "sql.active_record" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ # A database query is an outgoing call, so it carries CLIENT kind.
+ expect(span.kind).to eq(:client)
+ expect(span.attributes["db.query.text"]).to eq("SQL")
+ expect(span.attributes["db.system.name"]).to eq("other_sql")
+ expect(span.attributes["appsignal.category"]).to eq("sql.active_record")
+ expect(span.attributes).not_to have_key("appsignal.body")
+ end
end
- it "instruments an ActiveSupport::Notifications.instrument event with no registered formatter" do
- return_value = as.instrument("no-registered.formatter", :key => "something") do
- "value"
+ describe "an event with no registered formatter" do
+ def perform
+ as.instrument("no-registered.formatter", :key => "something") { "value" }
end
- expect(return_value).to eq "value"
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "no-registered.formatter",
- "title" => ""
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ expect(perform).to eq "value"
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "no-registered.formatter",
+ "title" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ expect(perform).to eq "value"
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.find { |s| s.name == "no-registered.formatter" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ # A plain event is not an outgoing call, so it keeps the default kind.
+ expect(span.kind).to eq(:internal)
+ expect(span.attributes).not_to have_key("appsignal.body")
+ expect(span.attributes["appsignal.category"]).to eq("no-registered.formatter")
+ expect(span.attributes).not_to have_key("db.query.text")
+ expect(span.attributes).not_to have_key("db.system.name")
+ end
end
- it "converts non-string names to strings" do
- as.instrument(:not_a_string) {} # rubocop:disable Lint/EmptyBlock
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "not_a_string",
- "title" => ""
- )
+ describe "an event with a non-string name" do
+ def perform
+ as.instrument(:not_a_string) {} # rubocop:disable Lint/EmptyBlock
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "not_a_string",
+ "title" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ expect(event_spans.map(&:name)).to include("not_a_string")
+ span = event_spans.find { |s| s.name == "not_a_string" }
+ expect(span.attributes["appsignal.category"]).to eq("not_a_string")
+ end
end
- it "does not instrument events whose name starts with a bang" do
- return_value = as.instrument("!sql.active_record", :sql => "SQL") do
- "value"
+ describe "an event whose name starts with a bang" do
+ def perform
+ as.instrument("!sql.active_record", :sql => "SQL") { "value" }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ expect(perform).to eq "value"
+ expect(transaction).to_not include_events
end
- expect(return_value).to eq "value"
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
- expect(transaction).to_not include_events
+ expect(perform).to eq "value"
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans).to be_empty
+ end
end
- it "does not instrument suppressed events, recorded by a dedicated integration" do
- return_value = as.instrument("request.faraday", :method => :get) do
- "value"
+ describe "a suppressed event, recorded by a dedicated integration" do
+ def perform
+ as.instrument("request.faraday", :method => :get) { "value" }
end
- expect(return_value).to eq "value"
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ expect(perform).to eq "value"
+ expect(transaction).to_not include_events
+ end
- expect(transaction).to_not include_events
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ expect(perform).to eq "value"
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans).to be_empty
+ end
end
- context "when an error is raised in an instrumented block" do
- it "instruments an ActiveSupport::Notifications.instrument event" do
+ describe "when an error is raised in an instrumented block" do
+ def perform
expect do
as.instrument("sql.active_record", :sql => "SQL") do
raise ExampleException, "foo"
end
end.to raise_error(ExampleException, "foo")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
expect(transaction).to include_event(
"body" => "SQL",
@@ -76,15 +204,41 @@
"title" => ""
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.find { |s| s.name == "sql.active_record" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ # A database query is an outgoing call, so it carries CLIENT kind.
+ expect(span.kind).to eq(:client)
+ expect(span.attributes["db.query.text"]).to eq("SQL")
+ expect(span.attributes["db.system.name"]).to eq("other_sql")
+ end
end
- context "when a message is thrown in an instrumented block" do
- it "instruments an ActiveSupport::Notifications.instrument event" do
+ describe "when a message is thrown in an instrumented block" do
+ def perform
expect do
- as.instrument("sql.active_record", :sql => "SQL") do
- throw :foo
- end
+ as.instrument("sql.active_record", :sql => "SQL") { throw :foo }
end.to throw_symbol(:foo)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
expect(transaction).to include_event(
"body" => "SQL",
@@ -94,16 +248,56 @@
"title" => ""
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.find { |s| s.name == "sql.active_record" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ # A database query is an outgoing call, so it carries CLIENT kind.
+ expect(span.kind).to eq(:client)
+ expect(span.attributes["db.query.text"]).to eq("SQL")
+ expect(span.attributes["db.system.name"]).to eq("other_sql")
+ end
end
- context "when a transaction is completed in an instrumented block" do
- it "does not complete the ActiveSupport::Notifications.instrument event" do
+ describe "when the transaction is completed inside an instrumented block" do
+ def perform
as.instrument("sql.active_record", :sql => "SQL") do
Appsignal::Transaction.complete_current!
end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
expect(transaction).to_not include_events
expect(transaction).to be_completed
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+
+ expect(transaction).to be_completed
+ expect(event_spans.map(&:name)).not_to include("sql.active_record")
+ end
end
end
diff --git a/spec/lib/appsignal/hooks/active_support_notifications/start_finish_shared_examples.rb b/spec/lib/appsignal/hooks/active_support_notifications/start_finish_shared_examples.rb
index 4342e228a..efad02d38 100644
--- a/spec/lib/appsignal/hooks/active_support_notifications/start_finish_shared_examples.rb
+++ b/spec/lib/appsignal/hooks/active_support_notifications/start_finish_shared_examples.rb
@@ -1,47 +1,153 @@
shared_examples "activesupport start finish override" do
let(:instrumenter) { as.instrumenter }
- it "instruments start/finish events with payload on start ignores payload" do
- instrumenter.start("sql.active_record", :sql => "SQL")
- instrumenter.finish("sql.active_record", {})
-
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
- "count" => 1,
- "name" => "sql.active_record",
- "title" => ""
- )
+ describe "a start/finish event whose payload is provided at start" do
+ def perform
+ instrumenter.start("sql.active_record", :sql => "SQL")
+ instrumenter.finish("sql.active_record", {})
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
+ "count" => 1,
+ "name" => "sql.active_record",
+ "title" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.find { |s| s.name == "sql.active_record" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ # A database query is an outgoing call, so it carries CLIENT kind.
+ expect(span.kind).to eq(:client)
+ # The formatter received an empty finish payload, so body is empty —
+ # the OTel backend skips writing db.query.text / db.system.name.
+ expect(span.attributes).not_to have_key("db.query.text")
+ expect(span.attributes).not_to have_key("db.system.name")
+ end
end
- it "instruments an ActiveSupport::Notifications.start/finish event with payload on finish" do
- instrumenter.start("sql.active_record", {})
- instrumenter.finish("sql.active_record", :sql => "SQL")
-
- expect(transaction).to include_event(
- "body" => "SQL",
- "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
- "count" => 1,
- "name" => "sql.active_record",
- "title" => ""
- )
+ describe "a start/finish event whose payload is provided at finish" do
+ def perform
+ instrumenter.start("sql.active_record", {})
+ instrumenter.finish("sql.active_record", :sql => "SQL")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+
+ expect(transaction).to include_event(
+ "body" => "SQL",
+ "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
+ "count" => 1,
+ "name" => "sql.active_record",
+ "title" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.find { |s| s.name == "sql.active_record" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ # A database query is an outgoing call, so it carries CLIENT kind.
+ expect(span.kind).to eq(:client)
+ expect(span.attributes["db.query.text"]).to eq("SQL")
+ expect(span.attributes["db.system.name"]).to eq("other_sql")
+ end
end
- it "does not instrument events whose name starts with a bang" do
- instrumenter.start("!sql.active_record", {})
- instrumenter.finish("!sql.active_record", {})
+ describe "an event whose name starts with a bang" do
+ def perform
+ instrumenter.start("!sql.active_record", {})
+ instrumenter.finish("!sql.active_record", {})
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+
+ expect(transaction).to_not include_events
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+ Appsignal::Transaction.complete_current!
- expect(transaction).to_not include_events
+ expect(event_spans).to be_empty
+ end
end
- context "when a transaction is completed in an instrumented block" do
- it "does not complete the ActiveSupport::Notifications.instrument event" do
+ describe "when the transaction is completed between start and finish" do
+ def perform
instrumenter.start("sql.active_record", {})
Appsignal::Transaction.complete_current!
instrumenter.finish("sql.active_record", {})
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
expect(transaction).to_not include_events
expect(transaction).to be_completed
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ as.notifier = notifier
+
+ perform
+
+ expect(transaction).to be_completed
+ expect(event_spans.map(&:name)).not_to include("sql.active_record")
+ end
end
end
diff --git a/spec/lib/appsignal/hooks/active_support_notifications_spec.rb b/spec/lib/appsignal/hooks/active_support_notifications_spec.rb
index 7161c4d97..505ff55ba 100644
--- a/spec/lib/appsignal/hooks/active_support_notifications_spec.rb
+++ b/spec/lib/appsignal/hooks/active_support_notifications_spec.rb
@@ -4,13 +4,18 @@
if active_support_present?
let(:notifier) { ActiveSupport::Notifications::Fanout.new }
let(:as) { ActiveSupport::Notifications }
- let(:transaction) { http_request_transaction }
- before do
- start_agent
- set_current_transaction(transaction)
- as.notifier = notifier
+
+ # The shared examples swap in a fresh notifier (`as.notifier = notifier`) to
+ # control which subscriptions are active. Restore the original afterwards so
+ # the swap doesn't leak into later specs -- e.g. ActionMailer's
+ # instrumentation, which subscribes on the default notifier and would
+ # otherwise fire into the stale, subscription-less notifier left behind.
+ around do |example|
+ original_notifier = ActiveSupport::Notifications.notifier
+ example.run
+ ensure
+ ActiveSupport::Notifications.notifier = original_notifier
end
- around { |example| keep_transactions { example.run } }
# The before hook swaps in a fresh notifier (`as.notifier = notifier`) to
# control which subscriptions are active. Restore the original afterwards so
diff --git a/spec/lib/appsignal/hooks/activejob_spec.rb b/spec/lib/appsignal/hooks/activejob_spec.rb
index 9d2a8e8ff..2d7699d5b 100644
--- a/spec/lib/appsignal/hooks/activejob_spec.rb
+++ b/spec/lib/appsignal/hooks/activejob_spec.rb
@@ -73,10 +73,10 @@
end
end
let(:options) { {} }
+ let(:start_agent_args) { { :options => options } }
before do
ActiveJob::Base.queue_adapter = :inline
- start_agent(:options => options)
stub_const("ActiveJobTestJob", Class.new(ActiveJob::Base) do
def perform(*_args)
end
@@ -103,41 +103,90 @@ def perform(*_args)
end
end)
end
- around { |example| keep_transactions { example.run } }
- it "reports the name from the ActiveJob integration" do
- tags = { :queue => queue }
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
-
- queue_job(ActiveJobTestJob)
-
- transaction = last_transaction
- expect(transaction).to have_namespace(namespace)
- expect(transaction).to have_action("ActiveJobTestJob#perform")
- expect(transaction).to_not have_error
- expect(transaction).to_not include_metadata
- expect(transaction).to include_params([])
- expect(transaction).to include_tags(
- "active_job_id" => kind_of(String),
- "request_id" => kind_of(String),
- "queue" => queue,
- "executions" => 1
- )
- events = transaction.to_h["events"]
- .sort_by { |e| e["start"] }
- .map { |event| event["name"] }
- expect(events).to eq(expected_perform_events)
+ describe "reports action, namespace, tags and params" do
+ def perform
+ queue_job(ActiveJobTestJob)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ perform
+
+ transaction = last_transaction
+ transaction._sample
+ expect(transaction).to have_namespace(namespace)
+ expect(transaction).to have_action("ActiveJobTestJob#perform")
+ expect(transaction).to_not have_error
+ expect(transaction).to_not include_metadata
+ expect(transaction).to include_params([])
+ expect(transaction).to include_tags(
+ "active_job_id" => kind_of(String),
+ "request_id" => kind_of(String),
+ "queue" => queue,
+ "executions" => 1
+ )
+ events = transaction.to_h["events"]
+ .sort_by { |e| e["start"] }
+ .map { |event| event["name"] }
+ expect(events).to eq(expected_perform_events)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("ActiveJobTestJob#perform")
+ expect(exception_events).to be_empty
+ expect(root_span.attributes).to_not have_key("appsignal.metadata")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"])).to eq([])
+ expect(root_span.attributes["appsignal.tag.active_job_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ # The agent sibling asserts the perform_*.active_job events; mirror that
+ # here by checking the event spans exist and nest under the root span.
+ expect(event_spans.map(&:name)).to include(*expected_perform_events)
+ perform_span = event_spans.find { |s| s.name == "perform.active_job" }
+ expect(perform_span).not_to be_nil
+ expect(perform_span.parent_span_id).to eq(root_span.span_id)
+ end
end
context "with custom queue" do
- it "reports the custom queue as tag on the transaction" do
- tags = { :queue => "custom_queue" }
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
- queue_job(ActiveJobCustomQueueTestJob)
+ describe "reports the custom queue as tag" do
+ def perform
+ queue_job(ActiveJobCustomQueueTestJob)
+ end
- expect(last_transaction).to include_tags("queue" => "custom_queue")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ perform
+ expect(last_transaction).to include_tags("queue" => "custom_queue")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("custom_queue")
+ end
end
end
@@ -152,67 +201,121 @@ def perform(*_args)
end)
end
- it "reports the priority as tag on the transaction" do
- tags = { :queue => queue }
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_priority_job_count", 1, tags.merge(:priority => 10,
- :status => :processed))
+ describe "reports the priority as tag" do
+ def perform
+ queue_job(ActiveJobPriorityTestJob)
+ end
- queue_job(ActiveJobPriorityTestJob)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
- expect(last_transaction).to include_tags("queue" => queue, "priority" => 10)
+ allow(Appsignal).to receive(:increment_counter)
+
+ perform
+ expect(last_transaction).to include_tags("queue" => queue, "priority" => 10)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ expect(root_span.attributes["appsignal.tag.priority"]).to eq(10)
+ end
end
end
end
context "with error" do
- it "reports the error on the transaction from the ActiveRecord integration" do
- allow(Appsignal).to receive(:increment_counter) # Other calls we're testing in another test
- tags = { :queue => queue }
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_job_count", 1, tags.merge(:status => :failed))
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
-
- expect do
+ describe "reports the error on the transaction" do
+ def perform
queue_job(ActiveJobErrorTestJob)
- end.to raise_error(RuntimeError, "uh oh")
+ end
- transaction = last_transaction
- expect(transaction).to have_namespace(namespace)
- expect(transaction).to have_action("ActiveJobErrorTestJob#perform")
- expect(transaction).to have_error("RuntimeError", "uh oh")
- expect(transaction).to_not include_metadata
- expect(transaction).to include_params([])
- expect(transaction).to include_tags(
- "active_job_id" => kind_of(String),
- "request_id" => kind_of(String),
- "queue" => queue,
- "executions" => 1
- )
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
- events = transaction.to_h["events"]
- .sort_by { |e| e["start"] }
- .map { |event| event["name"] }
- expect(events).to eq(expected_perform_events)
+ allow(Appsignal).to receive(:increment_counter)
+
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+
+ transaction = last_transaction
+ transaction._sample
+ expect(transaction).to have_namespace(namespace)
+ expect(transaction).to have_action("ActiveJobErrorTestJob#perform")
+ expect(transaction).to have_error("RuntimeError", "uh oh")
+ expect(transaction).to_not include_metadata
+ expect(transaction).to include_params([])
+ expect(transaction).to include_tags(
+ "active_job_id" => kind_of(String),
+ "request_id" => kind_of(String),
+ "queue" => queue,
+ "executions" => 1
+ )
+ events = transaction.to_h["events"]
+ .sort_by { |e| e["start"] }
+ .map { |event| event["name"] }
+ expect(events).to eq(expected_perform_events)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActiveJobErrorTestJob#perform")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("RuntimeError")
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes).to_not have_key("appsignal.metadata")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"])).to eq([])
+ expect(root_span.attributes["appsignal.tag.active_job_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ end
end
context "with activejob_report_errors set to none" do
let(:options) { { :activejob_report_errors => "none" } }
- it "does not report the error" do
- allow(Appsignal).to receive(:increment_counter)
- tags = { :queue => queue }
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_job_count", 1, tags.merge(:status => :failed))
-
- expect do
+ describe "does not report the error" do
+ def perform
queue_job(ActiveJobErrorTestJob)
- end.to raise_error(RuntimeError, "uh oh")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ allow(Appsignal).to receive(:increment_counter)
- expect(last_transaction).to_not have_error
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+ last_transaction.complete
+
+ expect(exception_events).to be_empty
+ end
end
end
@@ -220,35 +323,74 @@ def perform(*_args)
context "with activejob_report_errors set to discard" do
let(:options) { { :activejob_report_errors => "discard" } }
- it "does not report error on first failure" do
- with_test_adapter do
- # Prevent the job from being instantly retried so we can test
- # what happens before it's retried
- allow_any_instance_of(ActiveJobErrorWithRetryTestJob).to receive(:retry_job)
+ describe "does not report error on first failure" do
+ def perform
+ with_test_adapter do
+ # Prevent the job from being instantly retried so we can test
+ # what happens before it's retried
+ allow_any_instance_of(ActiveJobErrorWithRetryTestJob).to receive(:retry_job)
- queue_job(ActiveJobErrorWithRetryTestJob)
+ queue_job(ActiveJobErrorWithRetryTestJob)
+ end
end
- transaction = last_transaction
- expect(transaction).to_not have_error
- expect(transaction).to include_tags("executions" => 1)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ perform
+
+ transaction = last_transaction
+ transaction._sample
+ expect(transaction).to_not have_error
+ expect(transaction).to include_tags("executions" => 1)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ last_transaction.complete
+
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ end
end
- it "reports error when discarding the job" do
- allow(Appsignal).to receive(:increment_counter)
- tags = { :queue => queue }
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_job_count", 1, tags.merge(:status => :failed))
+ describe "reports error when discarding the job" do
+ def perform
+ allow(Appsignal).to receive(:increment_counter)
- with_test_adapter do
- expect do
+ with_test_adapter do
queue_job(ActiveJobErrorWithRetryTestJob)
- end.to raise_error(RuntimeError, "uh oh")
+ end
end
- transaction = last_transaction
- expect(transaction).to have_error("RuntimeError", "uh oh")
- expect(transaction).to include_tags("executions" => 2)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+
+ transaction = last_transaction
+ transaction._sample
+ expect(transaction).to have_error("RuntimeError", "uh oh")
+ expect(transaction).to include_tags("executions" => 2)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+ last_transaction.complete
+
+ event = exception_events.find do |e|
+ e.attributes["exception.type"] == "RuntimeError"
+ end
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(2)
+ end
end
end
end
@@ -265,147 +407,327 @@ def perform(*_args)
end)
end
- it "reports the priority as tag on the transaction" do
- tags = { :queue => queue }
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_job_count", 1, tags.merge(:status => :failed))
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_priority_job_count", 1, tags.merge(:priority => 10,
- :status => :processed))
- expect(Appsignal).to receive(:increment_counter)
- .with("active_job_queue_priority_job_count", 1, tags.merge(:priority => 10,
- :status => :failed))
-
- expect do
+ describe "reports the priority as tag" do
+ def perform
queue_job(ActiveJobErrorPriorityTestJob)
- end.to raise_error(RuntimeError, "uh oh")
+ end
- expect(last_transaction).to include_tags("queue" => queue, "priority" => 10)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+ expect(last_transaction).to include_tags("queue" => queue, "priority" => 10)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+ last_transaction.complete
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("RuntimeError")
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ expect(root_span.attributes["appsignal.tag.priority"]).to eq(10)
+ end
end
end
end
end
context "with retries" do
- it "reports the number of retries as executions" do
- with_test_adapter do
- expect do
+ describe "reports the number of retries as executions" do
+ def perform
+ with_test_adapter do
queue_job(ActiveJobErrorWithRetryTestJob)
- end.to raise_error(RuntimeError, "uh oh")
+ end
end
- expect(last_transaction).to include_tags("executions" => 2)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+ expect(last_transaction).to include_tags("executions" => 2)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ expect { perform }.to raise_error(RuntimeError, "uh oh")
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(2)
+ end
end
end
context "when wrapped in another transaction" do
- it "does not create a new transaction or close the currently open one" do
- current_transaction = background_job_transaction
- set_current_transaction current_transaction
+ describe "does not create a new transaction or close the currently open one" do
+ def perform(current_transaction)
+ set_current_transaction current_transaction
+ queue_job(ActiveJobTestJob)
+ end
- queue_job(ActiveJobTestJob)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
- expect(created_transactions.count).to eql(1)
+ allow(Appsignal).to receive(:increment_counter)
- transaction = current_transaction
- expect(transaction).to_not be_completed
- transaction._sample
- # It does set data on the transaction
- expect(transaction).to have_namespace(namespace)
- expect(transaction).to have_id(current_transaction.transaction_id)
- expect(transaction).to have_action("ActiveJobTestJob#perform")
- expect(transaction).to_not have_error
- expect(transaction).to_not include_metadata
- expect(transaction).to include_params([])
- expect(transaction).to include_tags(
- "active_job_id" => kind_of(String),
- "request_id" => kind_of(String),
- "queue" => queue,
- "executions" => 1
- )
+ current_transaction = background_job_transaction
+ perform(current_transaction)
+
+ expect(created_transactions.count).to eql(1)
+
+ transaction = current_transaction
+ expect(transaction).to_not be_completed
+ transaction._sample
+ # It does set data on the transaction
+ expect(transaction).to have_namespace(namespace)
+ expect(transaction).to have_id(current_transaction.transaction_id)
+ expect(transaction).to have_action("ActiveJobTestJob#perform")
+ expect(transaction).to_not have_error
+ expect(transaction).to_not include_metadata
+ expect(transaction).to include_params([])
+ expect(transaction).to include_tags(
+ "active_job_id" => kind_of(String),
+ "request_id" => kind_of(String),
+ "queue" => queue,
+ "executions" => 1
+ )
- events = transaction.to_h["events"]
- .reject { |e| e["name"] == "enqueue.active_job" }
- .sort_by { |e| e["start"] }
- .map { |event| event["name"] }
- expect(events).to eq(expected_perform_events)
+ events = transaction.to_h["events"]
+ .reject { |e| e["name"] == "enqueue.active_job" }
+ .sort_by { |e| e["start"] }
+ .map { |event| event["name"] }
+ expect(events).to eq(expected_perform_events)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ allow(Appsignal).to receive(:increment_counter)
+
+ current_transaction = background_job_transaction
+ perform(current_transaction)
+
+ expect(created_transactions.count).to eql(1)
+ expect(current_transaction).to_not be_completed
+
+ current_transaction.complete
+
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("ActiveJobTestJob#perform")
+ expect(exception_events).to be_empty
+ expect(root_span.attributes).to_not have_key("appsignal.metadata")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"])).to eq([])
+ expect(root_span.attributes["appsignal.tag.active_job_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ end
end
end
- context "when enqueuing a job" do
- before { ActiveJob::Base.queue_adapter = :test }
+ context "with distributed trace context" do
+ let(:trace_id_hex) { "0af7651916cd43dd8448eb211c80319c" }
+ let(:span_id_hex) { "b7ad6b7169203331" }
+ let(:traceparent) { "00-#{trace_id_hex}-#{span_id_hex}-01" }
+
+ describe "serializing context onto the job" do
+ it "round-trips __otel_headers through serialize/deserialize in collector mode",
+ :collector_mode do
+ start_collector_agent
+
+ job = ActiveJobTestJob.new
+ job.__otel_headers = { "traceparent" => traceparent }
+ data = job.serialize
+
+ # Wire-compatible with OpenTelemetry: headers ride as an array of
+ # [key, value] pairs (ActiveJob's argument-serializer output), not a
+ # hash.
+ expect(data["__otel_headers"]).to eq([["traceparent", traceparent]])
+ expect(ActiveJobTestJob.deserialize(data).__otel_headers)
+ .to eq("traceparent" => traceparent)
+ end
+
+ it "leaves the job untouched outside collector mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ job = ActiveJobTestJob.new
+ job.__otel_headers = { "traceparent" => traceparent }
- context "with an active transaction" do
- it "records a single enqueue.active_job event on the transaction" do
+ expect(job.serialize).to_not have_key("__otel_headers")
+ end
+ end
+
+ describe "injecting context on enqueue" do
+ before { ActiveJob::Base.queue_adapter = :test }
+
+ # Returns the enqueuing transaction so the example can read its events.
+ def enqueue_within_transaction
transaction = http_request_transaction
set_current_transaction(transaction)
-
ActiveJobTestJob.perform_later
+ transaction
+ end
+
+ it "writes the producer span's context onto the job in collector mode",
+ :collector_mode do
+ start_collector_agent
+ enqueue_within_transaction
+ Appsignal::Transaction.complete_current!
+
+ # The enqueue is a producer event span under the enqueuing
+ # transaction, named after the job being enqueued.
+ producer = event_spans.find { |s| s.name == "enqueue ActiveJobTestJob job" }
+ expect(producer.attributes["appsignal.category"]).to eq("enqueue.active_job")
+ expect(producer.kind).to eq(:producer)
+ expect(producer.parent_span_id).to eq(root_span.span_id)
+
+ # The serialized job carries that span's context, so the performed job
+ # links back to it.
+ enqueued = ActiveJob::Base.queue_adapter.enqueued_jobs.first
+ expect(enqueued["__otel_headers"]).to eq(
+ [["traceparent", "00-#{producer.hex_trace_id}-#{producer.hex_span_id}-01"]]
+ )
+ end
- # Exactly one enqueue event: ours. Rails' native `enqueue.active_job`
+ it "records an enqueue event without wire context in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ transaction = enqueue_within_transaction
+
+ # Exactly one enqueue event: ours. The native `enqueue.active_job`
# notification is suppressed so it isn't recorded a second time.
enqueue_events =
transaction.to_h["events"].select { |event| event["name"] == "enqueue.active_job" }
expect(enqueue_events.size).to eq(1)
# The event is titled after the job being enqueued.
expect(enqueue_events.first["title"]).to eq("enqueue ActiveJobTestJob job")
- end
- end
-
- context "without an active transaction" do
- it "is a transparent pass-through that still enqueues the job" do
- expect do
- ActiveJobTestJob.perform_later
- end.to_not(change { created_transactions.count })
- expect(ActiveJob::Base.queue_adapter.enqueued_jobs.count).to eq(1)
+ enqueued = ActiveJob::Base.queue_adapter.enqueued_jobs.first
+ expect(enqueued).to_not have_key("__otel_headers")
end
end
- context "with an active transaction" do
- it "suppresses nested adapter enqueue events while enqueuing" do
- transaction = http_request_transaction
- set_current_transaction(transaction)
+ describe "suppressing nested adapter enqueue events" do
+ before { ActiveJob::Base.queue_adapter = :test }
- # The window in which a nested adapter integration (Sidekiq, Resque,
- # ...) would record its own event, which Active Job suppresses so the
- # enqueue is recorded once.
- suppressed_during_enqueue = nil
+ # Records whether job enqueue events were suppressed at the moment the
+ # adapter enqueued the job -- the window in which a nested adapter
+ # integration (Sidekiq, Resque, ...) would record its own event, and
+ # which Active Job suppresses so the enqueue is recorded once.
+ def suppressed_during_enqueue
+ captured = nil
adapter = ActiveJob::Base.queue_adapter
allow(adapter).to receive(:enqueue).and_wrap_original do |method, *args|
- suppressed_during_enqueue =
- Appsignal::Transaction.current.job_enqueue_events_suppressed?
+ captured = Appsignal::Transaction.current.job_enqueue_events_suppressed?
method.call(*args)
end
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
ActiveJobTestJob.perform_later
+ captured
+ end
+
+ it "suppresses them while the adapter enqueues in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
expect(suppressed_during_enqueue).to be(true)
end
+
+ it "suppresses them while the adapter enqueues in collector mode",
+ :collector_mode do
+ start_collector_agent
+ expect(suppressed_during_enqueue).to be(true)
+ end
+ end
+
+ describe "linking a performed job back to the enqueuer" do
+ # A job arrives with OpenTelemetry's serialized array-of-pairs carrier.
+ def perform_with_incoming_context
+ job_data = ActiveJobTestJob.new.serialize
+ .merge("__otel_headers" => [["traceparent", traceparent]])
+ perform_active_job { ActiveJob::Base.execute(job_data) }
+ end
+
+ it "starts a linked trace in collector mode", :collector_mode do
+ start_collector_agent
+ perform_with_incoming_context
+
+ # A job is its own unit of work: new trace, linked back to the enqueuer.
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.hex_trace_id).to_not eq(trace_id_hex)
+ expect(root_span.links.size).to eq(1)
+ link = root_span.links.first.span_context
+ expect(link.hex_trace_id).to eq(trace_id_hex)
+ expect(link.hex_span_id).to eq(span_id_hex)
+ end
+
+ it "does not leak the trace context as metadata in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform_with_incoming_context
+
+ expect(last_transaction.to_h["metadata"].keys).to_not include("__otel_headers")
+ end
end
end
context "with params" do
let(:options) { { :filter_parameters => ["foo"] } }
- it "filters the configured params" do
- queue_job(ActiveJobTestJob, method_given_args)
+ describe "filters the configured params" do
+ def perform
+ queue_job(ActiveJobTestJob, method_given_args)
+ end
- transaction = last_transaction
- transaction_hash = transaction.to_h
- expect(transaction_hash["sample_data"]["params"]).to include(
- [
- "foo",
- {
- "_aj_symbol_keys" => ["foo"],
- "foo" => "[FILTERED]",
- "bar" => "Bar",
- "baz" => { "_aj_symbol_keys" => [], "1" => "foo" }
- }
- ]
- )
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ perform
+
+ transaction = last_transaction
+ transaction_hash = transaction.to_h
+ expect(transaction_hash["sample_data"]["params"]).to include(
+ [
+ "foo",
+ {
+ "_aj_symbol_keys" => ["foo"],
+ "foo" => "[FILTERED]",
+ "bar" => "Bar",
+ "baz" => { "_aj_symbol_keys" => [], "1" => "foo" }
+ }
+ ]
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ last_transaction.complete
+
+ params = JSON.parse(root_span.attributes["appsignal.function.parameters"])
+ expect(params).to include(
+ [
+ "foo",
+ {
+ "_aj_symbol_keys" => ["foo"],
+ "foo" => "[FILTERED]",
+ "bar" => "Bar",
+ "baz" => { "_aj_symbol_keys" => [], "1" => "foo" }
+ }
+ ]
+ )
+ end
end
end
@@ -436,12 +758,29 @@ def perform(*_args)
end)
end
- it "sets provider_job_id as tag" do
- queue_job(ProviderWrappedActiveJobTestJob)
+ describe "sets provider_job_id as tag" do
+ def perform
+ queue_job(ProviderWrappedActiveJobTestJob)
+ end
- expect(last_transaction).to include_tags(
- "provider_job_id" => "my_provider_job_id"
- )
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ perform
+ expect(last_transaction).to include_tags(
+ "provider_job_id" => "my_provider_job_id"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.provider_job_id"])
+ .to eq("my_provider_job_id")
+ end
end
end
@@ -473,22 +812,17 @@ def perform(*_args)
end)
end
- it "sets queue time on transaction" do
+ # `have_queue_start` reads agent-only backend state, so this stays
+ # agent-only. In collector mode the queue start surfaces as a span event
+ # and `transaction_queue_duration` metric (covered in the transaction spec).
+ it "sets queue time on transaction", :agent_mode do
+ start_agent(**start_agent_args)
+
queue_job(ProviderWrappedActiveJobTestJob)
queue_time = Time.parse("2001-01-01T09:00:00.000000000Z")
expect(last_transaction).to have_queue_start((queue_time.to_f * 1_000).to_i)
end
-
- it "reports the queue time" do
- allow(Appsignal).to receive(:add_distribution_value)
-
- queue_job(ProviderWrappedActiveJobTestJob)
-
- # Asserts 1 hour queue time
- expect(Appsignal).to have_received(:add_distribution_value)
- .with("active_job_queue_time", 3_600_000.0, :queue => queue)
- end
end
context "with ActionMailer job" do
@@ -502,55 +836,67 @@ def welcome(_first_arg = nil, _second_arg = nil)
end
context "without params" do
- it "sets the Action mailer data on the transaction" do
- perform_mailer(ActionMailerTestJob, :welcome)
+ describe "sets the Action mailer data on the transaction" do
+ def perform
+ perform_mailer(ActionMailerTestJob, :welcome)
+ end
- transaction = last_transaction
- expect(transaction).to have_action("ActionMailerTestJob#welcome")
- expect(transaction).to include_params(
- ["ActionMailerTestJob", "welcome", "deliver_now"] + active_job_args_wrapper
- )
- expect(transaction).to include_tags(
- "active_job_id" => kind_of(String),
- "request_id" => kind_of(String),
- "queue" => "mailers",
- "executions" => 1
- )
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ perform
+
+ transaction = last_transaction
+ transaction._sample
+ expect(transaction).to have_action("ActionMailerTestJob#welcome")
+ expect(transaction).to include_params(
+ ["ActionMailerTestJob", "welcome", "deliver_now"] + active_job_args_wrapper
+ )
+ expect(transaction).to include_tags(
+ "active_job_id" => kind_of(String),
+ "request_id" => kind_of(String),
+ "queue" => "mailers",
+ "executions" => 1
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActionMailerTestJob#welcome")
+ expected_params =
+ ["ActionMailerTestJob", "welcome", "deliver_now"] + active_job_args_wrapper
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(expected_params)
+ expect(root_span.attributes["appsignal.tag.active_job_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("mailers")
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ end
end
end
context "with multiple arguments" do
- it "sets the arguments on the transaction" do
- perform_mailer(ActionMailerTestJob, :welcome, method_given_args)
+ describe "sets the arguments on the transaction" do
+ def perform
+ perform_mailer(ActionMailerTestJob, :welcome, method_given_args)
+ end
- transaction = last_transaction
- expect(transaction).to have_action("ActionMailerTestJob#welcome")
- expect(transaction).to include_params(
- ["ActionMailerTestJob", "welcome",
- "deliver_now"] + active_job_args_wrapper(:args => method_expected_args)
- )
- expect(transaction).to include_tags(
- "active_job_id" => kind_of(String),
- "request_id" => kind_of(String),
- "queue" => "mailers",
- "executions" => 1
- )
- end
- end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
- if DependencyHelper.rails_version >= Gem::Version.new("5.2.0")
- context "with parameterized arguments" do
- it "sets the arguments on the transaction" do
- perform_mailer(ActionMailerTestJob, :welcome, parameterized_given_args)
+ perform
transaction = last_transaction
+ transaction._sample
expect(transaction).to have_action("ActionMailerTestJob#welcome")
expect(transaction).to include_params(
- [
- "ActionMailerTestJob",
- "welcome",
- "deliver_now"
- ] + active_job_args_wrapper(:params => parameterized_expected_args)
+ ["ActionMailerTestJob", "welcome",
+ "deliver_now"] + active_job_args_wrapper(:args => method_expected_args)
)
expect(transaction).to include_tags(
"active_job_id" => kind_of(String),
@@ -559,6 +905,80 @@ def welcome(_first_arg = nil, _second_arg = nil)
"executions" => 1
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActionMailerTestJob#welcome")
+ expected_params =
+ ["ActionMailerTestJob", "welcome",
+ "deliver_now"] + active_job_args_wrapper(:args => method_expected_args)
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(expected_params)
+ expect(root_span.attributes["appsignal.tag.active_job_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("mailers")
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ end
+ end
+ end
+
+ if DependencyHelper.rails_version >= Gem::Version.new("5.2.0")
+ context "with parameterized arguments" do
+ describe "sets the arguments on the transaction" do
+ def perform
+ perform_mailer(ActionMailerTestJob, :welcome, parameterized_given_args)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ perform
+
+ transaction = last_transaction
+ transaction._sample
+ expect(transaction).to have_action("ActionMailerTestJob#welcome")
+ expect(transaction).to include_params(
+ [
+ "ActionMailerTestJob",
+ "welcome",
+ "deliver_now"
+ ] + active_job_args_wrapper(:params => parameterized_expected_args)
+ )
+ expect(transaction).to include_tags(
+ "active_job_id" => kind_of(String),
+ "request_id" => kind_of(String),
+ "queue" => "mailers",
+ "executions" => 1
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActionMailerTestJob#welcome")
+ expected_params =
+ [
+ "ActionMailerTestJob",
+ "welcome",
+ "deliver_now"
+ ] + active_job_args_wrapper(:params => parameterized_expected_args)
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(expected_params)
+ expect(root_span.attributes["appsignal.tag.active_job_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("mailers")
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ end
+ end
end
end
end
@@ -576,42 +996,25 @@ def welcome(*_args)
end)
end
- it "sets the Action mailer data on the transaction" do
- perform_mailer(ActionMailerTestMailDeliveryJob, :welcome)
+ describe "sets the Action mailer data on the transaction" do
+ def perform
+ perform_mailer(ActionMailerTestMailDeliveryJob, :welcome)
+ end
- transaction = last_transaction
- expect(transaction).to have_action("ActionMailerTestMailDeliveryJob#welcome")
- expect(transaction).to include_params(
- [
- "ActionMailerTestMailDeliveryJob",
- "welcome",
- "deliver_now",
- { active_job_internal_key => ["args"], "args" => [] }
- ]
- )
- expect(transaction).to include_tags(
- "active_job_id" => kind_of(String),
- "request_id" => kind_of(String),
- "queue" => "mailers",
- "executions" => 1
- )
- end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
- context "with method arguments" do
- it "sets the Action mailer data on the transaction" do
- perform_mailer(ActionMailerTestMailDeliveryJob, :welcome, method_given_args)
+ perform
transaction = last_transaction
+ transaction._sample
expect(transaction).to have_action("ActionMailerTestMailDeliveryJob#welcome")
expect(transaction).to include_params(
[
"ActionMailerTestMailDeliveryJob",
"welcome",
"deliver_now",
- {
- active_job_internal_key => ["args"],
- "args" => method_expected_args
- }
+ { active_job_internal_key => ["args"], "args" => [] }
]
)
expect(transaction).to include_tags(
@@ -621,15 +1024,103 @@ def welcome(*_args)
"executions" => 1
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActionMailerTestMailDeliveryJob#welcome")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq([
+ "ActionMailerTestMailDeliveryJob",
+ "welcome",
+ "deliver_now",
+ { active_job_internal_key => ["args"], "args" => [] }
+ ])
+ expect(root_span.attributes["appsignal.tag.active_job_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("mailers")
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ end
+ end
+
+ context "with method arguments" do
+ describe "sets the Action mailer data on the transaction" do
+ def perform
+ perform_mailer(ActionMailerTestMailDeliveryJob, :welcome, method_given_args)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ perform
+
+ transaction = last_transaction
+ transaction._sample
+ expect(transaction).to have_action("ActionMailerTestMailDeliveryJob#welcome")
+ expect(transaction).to include_params(
+ [
+ "ActionMailerTestMailDeliveryJob",
+ "welcome",
+ "deliver_now",
+ {
+ active_job_internal_key => ["args"],
+ "args" => method_expected_args
+ }
+ ]
+ )
+ expect(transaction).to include_tags(
+ "active_job_id" => kind_of(String),
+ "request_id" => kind_of(String),
+ "queue" => "mailers",
+ "executions" => 1
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActionMailerTestMailDeliveryJob#welcome")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq([
+ "ActionMailerTestMailDeliveryJob",
+ "welcome",
+ "deliver_now",
+ {
+ active_job_internal_key => ["args"],
+ "args" => method_expected_args
+ }
+ ])
+ expect(root_span.attributes["appsignal.tag.active_job_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("mailers")
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ end
+ end
end
context "with parameterized arguments" do
- it "sets the Action mailer data on the transaction" do
- perform_mailer(ActionMailerTestMailDeliveryJob, :welcome, parameterized_given_args)
+ describe "sets the Action mailer data on the transaction" do
+ def perform
+ perform_mailer(ActionMailerTestMailDeliveryJob, :welcome, parameterized_given_args)
+ end
- transaction = last_transaction
- expect(transaction).to have_action("ActionMailerTestMailDeliveryJob#welcome")
- expect(transaction).to include_params(
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+
+ perform
+
+ transaction = last_transaction
+ transaction._sample
+ expect(transaction).to have_action("ActionMailerTestMailDeliveryJob#welcome")
+ expect(transaction).to include_params(
[
"ActionMailerTestMailDeliveryJob",
"welcome",
@@ -641,12 +1132,38 @@ def welcome(*_args)
}
]
)
- expect(transaction).to include_tags(
- "active_job_id" => kind_of(String),
- "request_id" => kind_of(String),
- "queue" => "mailers",
- "executions" => 1
- )
+ expect(transaction).to include_tags(
+ "active_job_id" => kind_of(String),
+ "request_id" => kind_of(String),
+ "queue" => "mailers",
+ "executions" => 1
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ last_transaction.complete
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActionMailerTestMailDeliveryJob#welcome")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq([
+ "ActionMailerTestMailDeliveryJob",
+ "welcome",
+ "deliver_now",
+ {
+ active_job_internal_key => ["params", "args"],
+ "args" => [],
+ "params" => parameterized_expected_args
+ }
+ ])
+ expect(root_span.attributes["appsignal.tag.active_job_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to be_a(String)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("mailers")
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ end
end
end
end
@@ -688,4 +1205,198 @@ def active_job_internal_key
end
end
end
+
+ # The agent has no in-memory metric readout, so agent mode keeps the
+ # `increment_counter` mock while collector mode asserts the same metric
+ # reaches the OpenTelemetry backend. Only the metric is asserted here — the
+ # transaction-shape coverage stays agent-only (in the instrumentation describe
+ # above), since action/namespace/tags aren't implemented in collector mode
+ # yet. Self-contained so it doesn't inherit the `ActiveJobClassInstrumentation`
+ # group's parameterized `start_agent`; `start_agent` comes from the mode
+ # contexts.
+ describe "emitting the queue job count metric" do
+ before do
+ ActiveJob::Base.queue_adapter = :inline
+ stub_const("ActiveJobTestJob", Class.new(ActiveJob::Base) do
+ def perform(*_args)
+ end
+ end)
+ end
+
+ def perform
+ ActiveJobTestJob.perform_later
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ expect(Appsignal).to receive(:increment_counter)
+ .with("active_job_queue_job_count", 1, { :queue => "default", :status => :processed })
+
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ snapshot = metric_snapshot("active_job_queue_job_count")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.data_points.first.value).to eq(1.0)
+ expect(snapshot.data_points.first.attributes).to include(
+ "queue" => "default",
+ "status" => "processed"
+ )
+ end
+ end
+
+ # A failing job emits the job count metric a second time, tagged
+ # `status: failed`. Self-contained, same rationale as the describe above.
+ describe "emitting the failed job count metric" do
+ before do
+ ActiveJob::Base.queue_adapter = :inline
+ stub_const("ActiveJobFailingJob", Class.new(ActiveJob::Base) do
+ def perform(*_args)
+ raise "uh oh"
+ end
+ end)
+ end
+
+ def perform
+ ActiveJobFailingJob.perform_later
+ rescue RuntimeError
+ # The inline adapter re-raises the job's error; swallow it so the
+ # example can assert on the metric the hook emits in its `ensure`.
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ allow(Appsignal).to receive(:increment_counter) # the `processed` call
+ expect(Appsignal).to receive(:increment_counter)
+ .with("active_job_queue_job_count", 1, { :queue => "default", :status => :failed })
+
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ snapshot = metric_snapshot("active_job_queue_job_count")
+ expect(snapshot).not_to be_nil
+ failed = snapshot.data_points.find { |point| point.attributes["status"] == "failed" }
+ expect(failed).not_to be_nil
+ expect(failed.value).to eq(1.0)
+ expect(failed.attributes).to include("queue" => "default", "status" => "failed")
+ end
+ end
+
+ # A job with a priority emits an additional `priority_job_count` metric.
+ if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
+ describe "emitting the priority job count metric" do
+ before do
+ ActiveJob::Base.queue_adapter = :inline
+ stub_const("ActiveJobPriorityJob", Class.new(ActiveJob::Base) do
+ queue_with_priority 10
+
+ def perform(*_args)
+ end
+ end)
+ end
+
+ def perform
+ ActiveJobPriorityJob.perform_later
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ allow(Appsignal).to receive(:increment_counter) # the queue_job_count call
+ expect(Appsignal).to receive(:increment_counter).with(
+ "active_job_queue_priority_job_count",
+ 1,
+ { :queue => "default", :priority => 10, :status => :processed }
+ )
+
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ snapshot = metric_snapshot("active_job_queue_priority_job_count")
+ expect(snapshot).not_to be_nil
+ point = snapshot.data_points.first
+ expect(point.value).to eq(1.0)
+ expect(point.attributes).to include(
+ "queue" => "default",
+ "priority" => 10,
+ "status" => "processed"
+ )
+ end
+ end
+ end
+
+ # A job carrying an `enqueued_at` reports its queue time as a distribution.
+ context "with enqueued_at",
+ :skip => DependencyHelper.rails_version < Gem::Version.new("6.0.0") do
+ describe "emitting the queue time metric" do
+ before do
+ stub_const(
+ "ActiveJob::QueueAdapters::AppsignalTestAdapter",
+ Class.new(ActiveJob::QueueAdapters::InlineAdapter) do
+ # Inject an `enqueued_at` an hour before the frozen "now" below.
+ def enqueue(job)
+ ActiveJob::Base.execute(
+ job.serialize.merge("enqueued_at" => "2001-01-01T09:00:00.000000000Z")
+ )
+ end
+ end
+ )
+ stub_const("ActiveJobQueueTimeJob", Class.new(ActiveJob::Base) do
+ self.queue_adapter = :appsignal_test
+
+ def perform(*_args)
+ end
+ end)
+ end
+
+ def perform
+ Timecop.freeze(Time.parse("2001-01-01T10:00:00.000000000Z")) do
+ ActiveJobQueueTimeJob.perform_later
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ allow(Appsignal).to receive(:add_distribution_value)
+
+ perform
+
+ # One hour of queue time, in milliseconds.
+ expect(Appsignal).to have_received(:add_distribution_value)
+ .with("active_job_queue_time", 3_600_000.0, :queue => "default")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ snapshot = metric_snapshot("active_job_queue_time")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.instrument_kind).to eq(:histogram)
+ point = snapshot.data_points.first
+ expect(point.count).to eq(1)
+ expect(point.sum).to eq(3_600_000.0)
+ expect(point.attributes).to include("queue" => "default")
+ end
+ end
+ end
end
diff --git a/spec/lib/appsignal/hooks/at_exit_spec.rb b/spec/lib/appsignal/hooks/at_exit_spec.rb
index 9d4b11cd6..8d8da81c1 100644
--- a/spec/lib/appsignal/hooks/at_exit_spec.rb
+++ b/spec/lib/appsignal/hooks/at_exit_spec.rb
@@ -1,11 +1,12 @@
describe Appsignal::Hooks::AtExit do
describe ".install" do
- before { start_agent(:options => options) }
+ # The mode contexts run `start_agent`; thread the at_exit options through.
+ let(:start_agent_args) { { :options => options } }
context "with :enable_at_exit_reporter == true" do
let(:options) { { :enable_at_exit_reporter => true } }
- it "installs the at_exit hook" do
+ it_in_both_modes "installs the at_exit hook" do
expect(Appsignal::Hooks::AtExit::AtExitCallback).to receive(:call)
expect(Kernel).to receive(:at_exit).with(no_args) do |*_args, &block|
@@ -19,7 +20,7 @@
context "with :enable_at_exit_reporter == false" do
let(:options) { { :enable_at_exit_reporter => false } }
- it "doesn't install the at_exit hook" do
+ it_in_both_modes "doesn't install the at_exit hook" do
expect(Kernel).to_not receive(:at_exit)
end
end
@@ -27,8 +28,7 @@
end
describe Appsignal::Hooks::AtExit::AtExitCallback do
- around { |example| keep_transactions { example.run } }
- before { start_agent(:options => options) }
+ let(:start_agent_args) { { :options => options } }
def with_error(error_class, error_message)
raise error_class, error_message
@@ -48,7 +48,7 @@ def call_callback
}
end
- it "reports no transaction if the process didn't exit with an error" do
+ it_in_both_modes "reports no transaction if the process didn't exit with an error" do
expect(Appsignal).to_not receive(:stop)
logs =
capture_logs do
@@ -60,20 +60,40 @@ def call_callback
expect(logs).to_not contains_log(:error, "Appsignal.report_error: Cannot add error.")
end
- it "reports an error if there's an unhandled error" do
- expect(Appsignal).to receive(:stop).with("at_exit")
- expect do
+ describe "reports an error if there's an unhandled error" do
+ def perform
with_error(ExampleException, "error message") do
call_callback
end
- end.to change { created_transactions.count }.by(1)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect(Appsignal).to receive(:stop).with("at_exit")
+ expect { perform }.to change { created_transactions.count }.by(1)
- transaction = last_transaction
- expect(transaction).to have_namespace("unhandled")
- expect(transaction).to have_error("ExampleException", "error message")
+ transaction = last_transaction
+ expect(transaction).to have_namespace("unhandled")
+ expect(transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal).to receive(:stop).with("at_exit")
+ expect { perform }.to change { created_transactions.count }.by(1)
+
+ expect(root_span.attributes["appsignal.namespace"]).to eq("unhandled")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
- it "doesn't report the error if it is also the last error reported" do
+ it_in_both_modes "doesn't report the error if it is also the last error reported" do
expect(Appsignal).to_not receive(:stop)
with_error(ExampleException, "error message") do |error|
Appsignal.report_error(error)
@@ -85,7 +105,7 @@ def call_callback
end
end
- it "doesn't report the error if it is a SystemExit exception" do
+ it_in_both_modes "doesn't report the error if it is a SystemExit exception" do
expect(Appsignal).to_not receive(:stop)
with_error(SystemExit, "error message") do |error|
Appsignal.report_error(error)
@@ -97,7 +117,7 @@ def call_callback
end
end
- it "doesn't report the error if it is a SignalException exception" do
+ it_in_both_modes "doesn't report the error if it is a SignalException exception" do
expect(Appsignal).to_not receive(:stop)
with_error(SignalException, "TERM") do |error|
Appsignal.report_error(error)
@@ -118,7 +138,7 @@ def call_callback
}
end
- it "reports no error if the process didn't exit with an error" do
+ it_in_both_modes "reports no error if the process didn't exit with an error" do
expect(Appsignal).to_not receive(:stop)
logs =
capture_logs do
@@ -130,7 +150,7 @@ def call_callback
expect(logs).to_not contains_log(:error, "Appsignal.report_error: Cannot add error.")
end
- it "reports no error if there's an unhandled error" do
+ it_in_both_modes "reports no error if there's an unhandled error" do
expect(Appsignal).to_not receive(:stop)
logs =
capture_logs do
@@ -148,7 +168,7 @@ def call_callback
context "when enable_at_exit_hook is true" do
let(:options) { { :enable_at_exit_hook => "always" } }
- it "calls Appsignal.stop" do
+ it_in_both_modes "calls Appsignal.stop" do
expect(Appsignal).to receive(:stop).with("at_exit")
call_callback
end
@@ -157,7 +177,7 @@ def call_callback
context "when enable_at_exit_hook is false" do
let(:options) { { :enable_at_exit_hook => false } }
- it "does not call Appsignal.stop" do
+ it_in_both_modes "does not call Appsignal.stop" do
expect(Appsignal).to_not receive(:stop).with("at_exit")
call_callback
end
diff --git a/spec/lib/appsignal/hooks/dry_monitor_spec.rb b/spec/lib/appsignal/hooks/dry_monitor_spec.rb
index 43e183058..c51798cbf 100644
--- a/spec/lib/appsignal/hooks/dry_monitor_spec.rb
+++ b/spec/lib/appsignal/hooks/dry_monitor_spec.rb
@@ -33,13 +33,8 @@
describe "Dry Monitor Integration" do
let(:notifications) { Dry::Monitor::Notifications.new(:test) }
- let(:transaction) { http_request_transaction }
- before do
- start_agent
- set_current_transaction(transaction)
- end
- context "when is a dry-sql event" do
+ describe "a SQL event" do
let(:event_id) { :sql }
let(:payload) do
{
@@ -48,8 +43,15 @@
}
end
- it "creates an sql event" do
+ def perform
notifications.instrument(event_id, payload)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
expect(transaction).to include_event(
"body" => "SELECT * FROM users",
"body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
@@ -58,18 +60,42 @@
"title" => "query.postgres"
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("query.postgres")
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ # ROM emits its queries as dry-monitor `sql` events; a query is an
+ # outgoing call, so it carries CLIENT kind.
+ expect(span.kind).to eq(:client)
+ attrs = span.attributes
+ expect(attrs["db.query.text"]).to eq("SELECT * FROM users")
+ expect(attrs["db.system.name"]).to eq("other_sql")
+ expect(attrs["appsignal.category"]).to eq("query.postgres")
+ expect(attrs).not_to have_key("appsignal.body")
+ end
end
- context "when is an unregistered formatter event" do
+ describe "an unregistered formatter event" do
let(:event_id) { :foo }
- let(:payload) do
- {
- :name => "foo"
- }
- end
+ let(:payload) { { :name => "foo" } }
- it "creates a generic event" do
+ def perform
notifications.instrument(event_id, payload)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
expect(transaction).to include_event(
"body" => "",
"body_format" => Appsignal::EventFormatter::DEFAULT,
@@ -78,6 +104,26 @@
"title" => ""
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("foo")
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ # A non-SQL dry event is not an outgoing call, so it keeps the default kind.
+ expect(span.kind).to eq(:internal)
+ attrs = span.attributes
+ expect(attrs["appsignal.category"]).to eq("foo")
+ expect(attrs).not_to have_key("appsignal.body")
+ expect(attrs).not_to have_key("db.query.text")
+ expect(attrs).not_to have_key("db.system.name")
+ end
end
end
end
diff --git a/spec/lib/appsignal/hooks/excon_spec.rb b/spec/lib/appsignal/hooks/excon_spec.rb
index bf18e522a..de8654ccc 100644
--- a/spec/lib/appsignal/hooks/excon_spec.rb
+++ b/spec/lib/appsignal/hooks/excon_spec.rb
@@ -1,17 +1,31 @@
describe Appsignal::Hooks::ExconHook do
- before { start_agent }
-
context "with Excon" do
before do
stub_const("Excon", Class.new do
def self.defaults
- @defaults ||= {}
+ # Mock is the innermost default middleware; the hook inserts ours
+ # before it. Referenced lazily so it's resolved after the stub below.
+ @defaults ||= { :middlewares => [Excon::Middleware::Mock] }
end
end)
+ stub_const("Excon::Middleware", Module.new)
+ stub_const("Excon::Middleware::Base", Class.new do
+ def initialize(stack = nil)
+ @stack = stack
+ end
+
+ # The default `request_call` just returns the datum, standing in for the
+ # rest of the (empty) middleware stack.
+ def request_call(datum)
+ datum
+ end
+ end)
+ stub_const("Excon::Middleware::Mock", Class.new(Excon::Middleware::Base))
Appsignal::Hooks::ExconHook.new.install
end
describe "#dependencies_present?" do
+ before { start_agent }
subject { described_class.new.dependencies_present? }
it { is_expected.to be_truthy }
@@ -21,37 +35,121 @@ def self.defaults
it "adds the AppSignal instrumentor to Excon" do
expect(Excon.defaults[:instrumentor]).to eql(Appsignal::Integrations::ExconIntegration)
end
+
+ it "adds the AppSignal middleware to Excon, before the Mock middleware" do
+ middlewares = Excon.defaults[:middlewares]
+ expect(middlewares).to include(Appsignal::Integrations::ExconMiddleware)
+ expect(middlewares.index(Appsignal::Integrations::ExconMiddleware))
+ .to be < middlewares.index(Excon::Middleware::Mock)
+ end
+
+ it "does not add the middleware twice when installed again" do
+ Appsignal::Hooks::ExconHook.new.install
+ expect(
+ Excon.defaults[:middlewares].count(Appsignal::Integrations::ExconMiddleware)
+ ).to eq(1)
+ end
end
describe "instrumentation" do
- let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
- around { |example| keep_transactions { example.run } }
-
- it "instruments a http request" do
- data = {
- :host => "www.google.com",
- :method => :get,
- :scheme => "http"
- }
- Excon.defaults[:instrumentor].instrument("excon.request", data) {} # rubocop:disable Lint/EmptyBlock
-
- expect(transaction).to include_event(
- "name" => "request.excon",
- "title" => "GET http://www.google.com",
- "body" => ""
- )
+ describe "a http request" do
+ def perform
+ data = {
+ :host => "www.google.com",
+ :method => :get,
+ :scheme => "http"
+ }
+ Excon.defaults[:instrumentor].instrument("excon.request", data) do
+ # The middleware injects from whatever span is current. The
+ # instrumentor's event span is active here, mirroring a real
+ # request where the middleware runs inside the instrumented block.
+ datum = inject_with_middleware
+ ensure
+ @injected_headers = datum && datum[:headers]
+ end
+ end
+
+ # Runs the AppSignal Excon middleware's `request_call` over an empty
+ # datum, returning the datum so we can read the injected headers.
+ def inject_with_middleware
+ # Excon middlewares wrap the next one in the stack and forward to it,
+ # so pass a tail that just returns the datum. Excon's Middleware::Base
+ # requires this argument.
+ tail = Class.new do
+ def request_call(datum)
+ datum
+ end
+ end.new
+ middleware = Appsignal::Integrations::ExconMiddleware.new(tail)
+ middleware.request_call({})
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "request.excon",
+ "title" => "GET http://www.google.com",
+ "body" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(http_request_transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET http://www.google.com")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.category"]).to eq("request.excon")
+ expect(span.attributes).not_to have_key("appsignal.body")
+
+ # The middleware wrote a W3C traceparent for the client span onto the
+ # outgoing request headers, so the called service joins this trace.
+ expect(@injected_headers["traceparent"])
+ .to eq("00-#{span.hex_trace_id}-#{span.hex_span_id}-01")
+ end
end
- it "instruments a http response" do
- data = { :host => "www.google.com" }
- Excon.defaults[:instrumentor].instrument("excon.response", data) {} # rubocop:disable Lint/EmptyBlock
+ describe "a http response" do
+ def perform
+ data = { :host => "www.google.com" }
+ Excon.defaults[:instrumentor].instrument("excon.response", data) {} # rubocop:disable Lint/EmptyBlock
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "response.excon",
+ "title" => "www.google.com",
+ "body" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(http_request_transaction)
+ perform
+ Appsignal::Transaction.complete_current!
- expect(transaction).to include_event(
- "name" => "response.excon",
- "title" => "www.google.com",
- "body" => ""
- )
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("www.google.com")
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.category"]).to eq("response.excon")
+ expect(span.attributes).not_to have_key("appsignal.body")
+ end
end
end
end
diff --git a/spec/lib/appsignal/hooks/http_spec.rb b/spec/lib/appsignal/hooks/http_spec.rb
index 3df5f9175..2f29940a8 100644
--- a/spec/lib/appsignal/hooks/http_spec.rb
+++ b/spec/lib/appsignal/hooks/http_spec.rb
@@ -28,6 +28,11 @@
.to include(Appsignal::Integrations::HttpIntegration::HashOptions)
end
end
+
+ it "injects trace context on outgoing requests" do
+ expect(HTTP::Client.included_modules)
+ .to include(Appsignal::Integrations::HttpIntegration::ContextInjection)
+ end
end
context "with instrument_http_rb set to false" do
diff --git a/spec/lib/appsignal/hooks/rake_spec.rb b/spec/lib/appsignal/hooks/rake_spec.rb
index 2c1360396..7a8b64614 100644
--- a/spec/lib/appsignal/hooks/rake_spec.rb
+++ b/spec/lib/appsignal/hooks/rake_spec.rb
@@ -5,11 +5,9 @@
let(:task) { Rake::Task.new("task:name", Rake::Application.new) }
let(:arguments) { Rake::TaskArguments.new(["foo"], ["bar"]) }
let(:options) { {} }
- before do
- start_agent(:options => options)
- allow(Kernel).to receive(:at_exit)
- end
- around { |example| keep_transactions { example.run } }
+ # The mode contexts run `start_agent`; thread the Rake options through them.
+ let(:start_agent_args) { { :options => options } }
+ before { allow(Kernel).to receive(:at_exit) }
after do
if helper.instance_variable_defined?(:@register_at_exit_hook)
helper.remove_instance_variable(:@register_at_exit_hook)
@@ -33,15 +31,15 @@ def perform
context "with :enable_rake_performance_instrumentation == false" do
let(:options) { { :enable_rake_performance_instrumentation => false } }
- it "creates no transaction" do
+ it_in_both_modes "creates no transaction" do
expect { perform }.to_not(change { created_transactions.count })
end
- it "calls the original task" do
+ it_in_both_modes "calls the original task" do
expect(perform).to eq([])
end
- it "does not register an at_exit hook" do
+ it_in_both_modes "does not register an at_exit hook" do
perform
expect_to_not_have_registered_at_exit_hook
end
@@ -50,24 +48,41 @@ def perform
context "with :enable_rake_performance_instrumentation == true" do
let(:options) { { :enable_rake_performance_instrumentation => true } }
- it "creates a transaction" do
- expect { perform }.to(change { created_transactions.count }.by(1))
-
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_namespace("rake")
- expect(transaction).to have_action("task:name")
- expect(transaction).to_not have_error
- expect(transaction).to include_params("foo" => "bar")
- expect(transaction).to include_event("name" => "task.rake")
- expect(transaction).to be_completed
+ describe "creates a transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_namespace("rake")
+ expect(transaction).to have_action("task:name")
+ expect(transaction).to_not have_error
+ expect(transaction).to include_params("foo" => "bar")
+ expect(transaction).to include_event("name" => "task.rake")
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ # NOTE: params (include_params) is a collector-mode gap --
+ # set_sample_data is not yet implemented in the OpenTelemetry backend.
+ expect(root_span.attributes["appsignal.namespace"]).to eq("rake")
+ expect(root_span.name).to eq("task:name")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("task:name")
+ expect(exception_events).to be_empty
+ expect(event_spans.map(&:name)).to include("task.rake")
+ expect(last_transaction).to be_completed
+ end
end
- it "calls the original task" do
+ it_in_both_modes "calls the original task" do
expect(perform).to eq([])
end
- it "registers an at_exit hook" do
+ it_in_both_modes "registers an at_exit hook" do
perform
expect_to_have_registered_at_exit_hook
end
@@ -86,19 +101,42 @@ def perform
context "with normal error" do
let(:error) { ExampleException.new("error message") }
- it "creates a background job transaction" do
- perform
+ describe "creates a background job transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_namespace("rake")
+ expect(transaction).to have_action("task:name")
+ expect(transaction).to have_error("ExampleException", "error message")
+ expect(transaction).to include_params("foo" => "bar")
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_namespace("rake")
- expect(transaction).to have_action("task:name")
- expect(transaction).to have_error("ExampleException", "error message")
- expect(transaction).to include_params("foo" => "bar")
- expect(transaction).to be_completed
+ # NOTE: params (include_params) is a collector-mode gap --
+ # set_sample_data is not yet implemented in the OpenTelemetry backend.
+ expect(root_span.attributes["appsignal.namespace"]).to eq("rake")
+ expect(root_span.name).to eq("task:name")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("task:name")
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(last_transaction).to be_completed
+ end
end
- it "registers an at_exit hook" do
+ it_in_both_modes "registers an at_exit hook" do
perform
expect_to_have_registered_at_exit_hook
end
@@ -106,7 +144,10 @@ def perform
context "when first argument is not a `Rake::TaskArguments`" do
let(:arguments) { nil }
- it "does not add the params to the transaction" do
+ # Agent-only: asserting on params is a collector-mode gap
+ # (set_sample_data is not yet implemented in the OpenTelemetry backend).
+ it "does not add the params to the transaction", :agent_mode do
+ start_agent(**start_agent_args)
perform
expect(last_transaction).to_not include_params
@@ -117,22 +158,40 @@ def perform
context "when error is a SystemExit" do
let(:error) { SystemExit.new(1) }
- it "does not report the error" do
- perform
+ describe "does not report the error" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
- transaction = last_transaction
- expect(transaction).to_not have_error
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ end
end
end
context "when error is a SignalException" do
let(:error) { SignalException.new(1) }
- it "does not report the error" do
- perform
+ describe "does not report the error" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
- transaction = last_transaction
- expect(transaction).to_not have_error
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ end
end
end
end
@@ -142,12 +201,16 @@ def perform
describe "Appsignal::Integrations::RakeIntegrationHelper" do
let(:helper) { Appsignal::Integrations::RakeIntegrationHelper }
describe ".register_at_exit_hook" do
- before do
- start_agent
- allow(Appsignal).to receive(:stop)
+ before { allow(Appsignal).to receive(:stop) }
+ # Reset the memoized registration flag so each example (including the
+ # agent/collector pair) starts fresh.
+ after do
+ if helper.instance_variable_defined?(:@register_at_exit_hook)
+ helper.remove_instance_variable(:@register_at_exit_hook)
+ end
end
- it "registers the at_exit hook only once" do
+ it_in_both_modes "registers the at_exit hook only once" do
allow(Kernel).to receive(:at_exit)
helper.register_at_exit_hook
helper.register_at_exit_hook
@@ -157,12 +220,9 @@ def perform
describe ".at_exit_hook" do
let(:helper) { Appsignal::Integrations::RakeIntegrationHelper }
- before do
- start_agent
- allow(Appsignal).to receive(:stop)
- end
+ before { allow(Appsignal).to receive(:stop) }
- it "calls Appsignal.stop" do
+ it_in_both_modes "calls Appsignal.stop" do
helper.at_exit_hook
expect(Appsignal).to have_received(:stop).with("rake")
end
diff --git a/spec/lib/appsignal/hooks/redis_client_spec.rb b/spec/lib/appsignal/hooks/redis_client_spec.rb
index 888af0737..a9fa819cf 100644
--- a/spec/lib/appsignal/hooks/redis_client_spec.rb
+++ b/spec/lib/appsignal/hooks/redis_client_spec.rb
@@ -1,13 +1,11 @@
describe Appsignal::Hooks::RedisClientHook do
let(:options) { {} }
- before do
- start_agent(:options => options)
- end
if DependencyHelper.redis_client_present?
context "with redis-client" do
context "with instrumentation enabled" do
describe "#dependencies_present?" do
+ before { start_agent(:options => options) }
subject { described_class.new.dependencies_present? }
context "with gem version new than 0.14.0" do
@@ -35,6 +33,7 @@
context "install" do
before do
+ start_agent(:options => options)
Appsignal::Hooks.load_hooks
end
@@ -48,6 +47,8 @@
end
context "requirements" do
+ before { start_agent(:options => options) }
+
it "driver should have the write method" do
# Since we stub the driver class below, to make sure that we don't
# create a real connection, the test won't fail if the method definition
@@ -58,8 +59,8 @@
end
context "instrumentation" do
+ let(:client_config) { RedisClient::Config.new(:id => "stub_id") }
before do
- start_agent
# Stub RedisClient::RubyConnection class so that it doesn't perform an actual
# Redis query. This class will be included (prepended) with the
# AppSignal Redis integration.
@@ -77,35 +78,82 @@ def write(_commands)
# track if it was installed already or not.
Appsignal::Hooks::RedisClientHook.new.install
end
- let(:transaction) { http_request_transaction }
- let!(:client_config) { RedisClient::Config.new(:id => "stub_id") }
- before { set_current_transaction(transaction) }
- around { |example| keep_transactions { example.run } }
-
- it "instrument a redis call" do
- connection = RedisClient::RubyConnection.new client_config
- expect(connection.write([:get, "key"])).to eql("stub_write")
-
- expect(transaction).to include_event(
- "name" => "query.redis",
- "body" => "get ?",
- "title" => "stub_id"
- )
+
+ describe "a redis call" do
+ def perform
+ RedisClient::RubyConnection.new(client_config).write([:get, "key"])
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+
+ expect(transaction).to include_event(
+ "name" => "query.redis",
+ "body" => "get ?",
+ "title" => "stub_id"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("stub_id")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.body"]).to eq("get ?")
+ expect(span.attributes["appsignal.category"]).to eq("query.redis")
+ expect(span.attributes).not_to have_key("db.query.text")
+ end
end
- it "instrument a redis script call" do
- connection = ::RedisClient::RubyConnection.new client_config
- script = "return redis.call('set',KEYS[1],ARGV[1])"
- keys = ["foo"]
- argv = ["bar"]
- expect(connection.write([:eval, script, keys.size, keys, argv]))
- .to eql("stub_write")
-
- expect(transaction).to include_event(
- "name" => "query.redis",
- "body" => "#{script} ? ?",
- "title" => "stub_id"
- )
+ describe "a redis script call" do
+ let(:script) { "return redis.call('set',KEYS[1],ARGV[1])" }
+
+ def perform
+ keys = ["foo"]
+ argv = ["bar"]
+ RedisClient::RubyConnection.new(client_config)
+ .write([:eval, script, keys.size, keys, argv])
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+
+ expect(transaction).to include_event(
+ "name" => "query.redis",
+ "body" => "#{script} ? ?",
+ "title" => "stub_id"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("stub_id")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.body"]).to eq("#{script} ? ?")
+ expect(span.attributes["appsignal.category"]).to eq("query.redis")
+ expect(span.attributes).not_to have_key("db.query.text")
+ end
end
end
end
@@ -118,6 +166,7 @@ def write(_commands)
context "install" do
before do
+ start_agent(:options => options)
Appsignal::Hooks.load_hooks
end
@@ -131,6 +180,8 @@ def write(_commands)
end
context "requirements" do
+ before { start_agent(:options => options) }
+
it "driver should have the write method" do
# Since we stub the driver class below, to make sure that we don't
# create a real connection, the test won't fail if the method definition
@@ -141,8 +192,8 @@ def write(_commands)
end
context "instrumentation" do
+ let(:client_config) { RedisClient::Config.new(:id => "stub_id") }
before do
- start_agent
# Stub RedisClient::HiredisConnection class so that it doesn't perform an actual
# Redis query. This class will be included (prepended) with the
# AppSignal Redis integration.
@@ -160,35 +211,80 @@ def write(_commands)
# track if it was installed already or not.
Appsignal::Hooks::RedisClientHook.new.install
end
- let(:transaction) { http_request_transaction }
- let!(:client_config) { RedisClient::Config.new(:id => "stub_id") }
- before { set_current_transaction(transaction) }
- around { |example| keep_transactions { example.run } }
- it "instrument a redis call" do
- connection = RedisClient::HiredisConnection.new client_config
- expect(connection.write([:get, "key"])).to eql("stub_write")
+ describe "a redis call" do
+ def perform
+ RedisClient::HiredisConnection.new(client_config).write([:get, "key"])
+ end
- expect(transaction).to include_event(
- "name" => "query.redis",
- "body" => "get ?",
- "title" => "stub_id"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+
+ expect(transaction).to include_event(
+ "name" => "query.redis",
+ "body" => "get ?",
+ "title" => "stub_id"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("stub_id")
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.body"]).to eq("get ?")
+ expect(span.attributes["appsignal.category"]).to eq("query.redis")
+ expect(span.attributes).not_to have_key("db.query.text")
+ end
end
- it "instrument a redis script call" do
- connection = ::RedisClient::HiredisConnection.new client_config
- script = "return redis.call('set',KEYS[1],ARGV[1])"
- keys = ["foo"]
- argv = ["bar"]
- expect(connection.write([:eval, script, keys.size, keys,
- argv])).to eql("stub_write")
+ describe "a redis script call" do
+ let(:script) { "return redis.call('set',KEYS[1],ARGV[1])" }
- expect(transaction).to include_event(
- "name" => "query.redis",
- "body" => "#{script} ? ?",
- "title" => "stub_id"
- )
+ def perform
+ keys = ["foo"]
+ argv = ["bar"]
+ RedisClient::HiredisConnection.new(client_config)
+ .write([:eval, script, keys.size, keys, argv])
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+
+ expect(transaction).to include_event(
+ "name" => "query.redis",
+ "body" => "#{script} ? ?",
+ "title" => "stub_id"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("stub_id")
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.body"]).to eq("#{script} ? ?")
+ expect(span.attributes["appsignal.category"]).to eq("query.redis")
+ expect(span.attributes).not_to have_key("db.query.text")
+ end
end
end
end
@@ -200,6 +296,7 @@ def write(_commands)
let(:options) { { :instrument_redis => false } }
describe "#dependencies_present?" do
+ before { start_agent(:options => options) }
subject { described_class.new.dependencies_present? }
it { is_expected.to be_falsy }
@@ -209,6 +306,7 @@ def write(_commands)
else
context "without redis-client" do
describe "#dependencies_present?" do
+ before { start_agent(:options => options) }
subject { described_class.new.dependencies_present? }
it { is_expected.to be_falsy }
diff --git a/spec/lib/appsignal/hooks/redis_spec.rb b/spec/lib/appsignal/hooks/redis_spec.rb
index a4edab721..28db616a4 100644
--- a/spec/lib/appsignal/hooks/redis_spec.rb
+++ b/spec/lib/appsignal/hooks/redis_spec.rb
@@ -1,6 +1,5 @@
describe Appsignal::Hooks::RedisHook do
let(:options) { {} }
- before { start_agent(:options => options) }
if DependencyHelper.redis_present?
context "with redis" do
@@ -8,6 +7,7 @@
context "with redis-client" do
context "with instrumentation enabled" do
describe "#dependencies_present?" do
+ before { start_agent(:options => options) }
subject { described_class.new.dependencies_present? }
it { is_expected.to be_falsey }
@@ -17,6 +17,7 @@
else
context "with instrumentation enabled" do
describe "#dependencies_present?" do
+ before { start_agent(:options => options) }
subject { described_class.new.dependencies_present? }
it { is_expected.to be_truthy }
@@ -27,6 +28,7 @@
context "install" do
before do
+ start_agent(:options => options)
Appsignal::Hooks.load_hooks
end
@@ -40,6 +42,8 @@
end
context "requirements" do
+ before { start_agent(:options => options) }
+
it "driver should have the write method" do
# Since we stub the client class below, to make sure that we don't
# create a real connection, the test won't fail if the method definition
@@ -51,7 +55,6 @@
context "instrumentation" do
before do
- start_agent
# Stub Redis::Client class so that it doesn't perform an actual
# Redis query. This class will be included (prepended) with the
# AppSignal Redis integration.
@@ -69,33 +72,81 @@ def write(_commands)
# track if it was installed already or not.
Appsignal::Hooks::RedisHook.new.install
end
- let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
- around { |example| keep_transactions { example.run } }
-
- it "instrument a redis call" do
- client = Redis::Client.new
- expect(client.write([:get, "key"])).to eql("stub_write")
-
- expect(transaction).to include_event(
- "name" => "query.redis",
- "body" => "get ?",
- "title" => "stub_id"
- )
+
+ describe "a redis call" do
+ def perform
+ Redis::Client.new.write([:get, "key"])
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+
+ expect(transaction).to include_event(
+ "name" => "query.redis",
+ "body" => "get ?",
+ "title" => "stub_id"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("stub_id")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.body"]).to eq("get ?")
+ expect(span.attributes["appsignal.category"]).to eq("query.redis")
+ expect(span.attributes).not_to have_key("db.query.text")
+ end
end
- it "instrument a redis script call" do
- client = Redis::Client.new
- script = "return redis.call('set',KEYS[1],ARGV[1])"
- keys = ["foo"]
- argv = ["bar"]
- expect(client.write([:eval, script, keys.size, keys, argv])).to eql("stub_write")
-
- expect(transaction).to include_event(
- "name" => "query.redis",
- "body" => "#{script} ? ?",
- "title" => "stub_id"
- )
+ describe "a redis script call" do
+ let(:script) { "return redis.call('set',KEYS[1],ARGV[1])" }
+
+ def perform
+ keys = ["foo"]
+ argv = ["bar"]
+ Redis::Client.new.write([:eval, script, keys.size, keys, argv])
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+
+ expect(transaction).to include_event(
+ "name" => "query.redis",
+ "body" => "#{script} ? ?",
+ "title" => "stub_id"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ expect(perform).to eql("stub_write")
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("stub_id")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.body"]).to eq("#{script} ? ?")
+ expect(span.attributes["appsignal.category"]).to eq("query.redis")
+ expect(span.attributes).not_to have_key("db.query.text")
+ end
end
end
end
@@ -105,6 +156,7 @@ def write(_commands)
let(:options) { { :instrument_redis => false } }
describe "#dependencies_present?" do
+ before { start_agent(:options => options) }
subject { described_class.new.dependencies_present? }
it { is_expected.to be_falsy }
@@ -115,6 +167,7 @@ def write(_commands)
else
context "without redis" do
describe "#dependencies_present?" do
+ before { start_agent(:options => options) }
subject { described_class.new.dependencies_present? }
it { is_expected.to be_falsy }
diff --git a/spec/lib/appsignal/hooks/sequel_spec.rb b/spec/lib/appsignal/hooks/sequel_spec.rb
index 9e758fbca..d82805af7 100644
--- a/spec/lib/appsignal/hooks/sequel_spec.rb
+++ b/spec/lib/appsignal/hooks/sequel_spec.rb
@@ -8,30 +8,48 @@
end
end
- before { start_agent }
-
describe "#dependencies_present?" do
+ before { start_agent }
subject { described_class.new.dependencies_present? }
it { is_expected.to be_truthy }
end
context "with a transaction" do
- let(:transaction) { http_request_transaction }
- before do
- set_current_transaction(transaction)
- db.logger = Logger.new($stdout) # To test #log_duration call
+ def perform
+ db["SELECT 1"].all.to_a
end
- it "should instrument queries" do
- expect(transaction).to receive(:start_event).at_least(:once)
- expect(transaction).to receive(:finish_event)
- .at_least(:once)
- .with("sql.sequel", nil, kind_of(String), 1)
-
- expect(db).to receive(:log_duration).at_least(:once)
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "sql.sequel",
+ "title" => "",
+ "body" => "SELECT 1",
+ "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT
+ )
+ end
- db["SELECT 1"].all.to_a
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ span = event_spans.find do |s|
+ s.name == "sql.sequel" && s.attributes["db.query.text"] == "SELECT 1"
+ end
+ expect(span).not_to be_nil
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["db.system.name"]).to eq("other_sql")
+ expect(span.attributes).not_to have_key("appsignal.body")
+ expect(span.attributes["appsignal.category"]).to eq("sql.sequel")
end
end
else
diff --git a/spec/lib/appsignal/integrations/active_support_event_reporter_spec.rb b/spec/lib/appsignal/integrations/active_support_event_reporter_spec.rb
index dfa32b8f3..03f51c590 100644
--- a/spec/lib/appsignal/integrations/active_support_event_reporter_spec.rb
+++ b/spec/lib/appsignal/integrations/active_support_event_reporter_spec.rb
@@ -2,24 +2,48 @@
describe Appsignal::Integrations::ActiveSupportEventReporter::Subscriber do
let(:subscriber) { described_class.new }
- let(:logger) { instance_double(Appsignal::Logger) }
-
- before do
- start_agent
- allow(Appsignal::Logger).to receive(:new).with("rails_events").and_return(logger)
+ let(:event) do
+ {
+ :name => "user.created",
+ :payload => { :id => 123, :email => "user@example.com" }
+ }
end
describe "#emit" do
- it "logs the event name and payload" do
- event = {
- :name => "user.created",
- :payload => { :id => 123, :email => "user@example.com" }
- }
+ def perform
+ subscriber.emit(event)
+ end
- expect(logger).to receive(:info).with("user.created",
- { :id => 123, :email => "user@example.com" })
+ it "in agent mode", :agent_mode do
+ start_agent
- subscriber.emit(event)
+ logger = instance_double(Appsignal::Logger)
+ allow(Appsignal::Logger).to receive(:new).with("rails_events").and_return(logger)
+
+ expect(logger).to receive(:info).with(
+ "user.created",
+ { :id => 123, :email => "user@example.com" }
+ )
+
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect(log_records.size).to eq(1)
+ record = log_records.first
+ expect(record).not_to be_nil
+ expect(record.body).to eq("user.created")
+ expect(record.severity_number).to eq(9)
+ expect(record.severity_text).to eq("INFO")
+ expect(record.attributes).to include(
+ "id" => 123,
+ "email" => "user@example.com",
+ "appsignal.group" => "rails_events"
+ )
end
end
end
diff --git a/spec/lib/appsignal/integrations/code_ownership_spec.rb b/spec/lib/appsignal/integrations/code_ownership_spec.rb
index ec834c9c6..c55747760 100644
--- a/spec/lib/appsignal/integrations/code_ownership_spec.rb
+++ b/spec/lib/appsignal/integrations/code_ownership_spec.rb
@@ -3,8 +3,6 @@
describe Appsignal::Integrations::CodeOwnershipIntegration do
before do
- start_agent
-
Appsignal::Hooks::CodeOwnershipHook.new.install
end
@@ -19,7 +17,10 @@
FileUtils.rm_rf(File.join(tmp_dir, "config"))
end
+ # These examples exercise the error-handling path and assert on
+ # internal_logger output, which is not OTel-routed. No collector coverage.
it "handles missing config file" do
+ start_agent
create_app_files
transaction = create_transaction
@@ -39,6 +40,7 @@
end
it "handles missing team config files" do
+ start_agent
create_app_files
create_config_file
transaction = create_transaction
@@ -75,10 +77,10 @@
FileUtils.rm_rf(File.join(tmp_dir, "config"))
end
- it "sets an owner tag of the transaction based on file-annotation" do
- transaction = create_transaction
+ describe "sets an owner tag of the transaction based on file-annotation" do
+ let(:transaction) { create_transaction }
- begin
+ def perform
load File.join(tmp_dir, "app", "file_annotation_based.rb")
rescue => error
transaction.add_error(error)
@@ -86,13 +88,31 @@
transaction.complete
end
- expect(transaction).to include_tags("owner" => "FileTeam")
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ transaction._sample
+
+ expect(transaction).to include_tags("owner" => "FileTeam")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ # The owner lookup is driven by the recorded error; assert the
+ # exception event that produced it is present.
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("RuntimeError")
+ expect(root_span.attributes["appsignal.tag.owner"]).to eq("FileTeam")
+ end
end
- it "sets an owner tag of the transaction based on directory ownership" do
- transaction = create_transaction
+ describe "sets an owner tag of the transaction based on directory ownership" do
+ let(:transaction) { create_transaction }
- begin
+ def perform
load File.join(tmp_dir, "app", "dir", "directory_based.rb")
rescue => error
transaction.add_error(error)
@@ -100,13 +120,31 @@
transaction.complete
end
- expect(transaction).to include_tags("owner" => "DirectoryTeam")
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ transaction._sample
+
+ expect(transaction).to include_tags("owner" => "DirectoryTeam")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ # The owner lookup is driven by the recorded error; assert the
+ # exception event that produced it is present.
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("RuntimeError")
+ expect(root_span.attributes["appsignal.tag.owner"]).to eq("DirectoryTeam")
+ end
end
- it "sets owner tag of the transaction based on `owned_globs` in team.yml file" do
- transaction = create_transaction
+ describe "sets owner tag of the transaction based on `owned_globs` in team.yml file" do
+ let(:transaction) { create_transaction }
- begin
+ def perform
load File.join(tmp_dir, "app", "glob", "glob_based.rb")
rescue => error
transaction.add_error(error)
@@ -114,10 +152,31 @@
transaction.complete
end
- expect(transaction).to include_tags("owner" => "GlobTeam")
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ transaction._sample
+
+ expect(transaction).to include_tags("owner" => "GlobTeam")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ # The owner lookup is driven by the recorded error; assert the
+ # exception event that produced it is present.
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("RuntimeError")
+ expect(root_span.attributes["appsignal.tag.owner"]).to eq("GlobTeam")
+ end
end
+ # These examples assert on both tag absence and internal_logger output
+ # (no log emitted). No collector coverage for logging behavior.
it "handles files without owners" do
+ start_agent
transaction = create_transaction
logs = capture_logs do
@@ -133,6 +192,7 @@
end
it "handles transactions without errors" do
+ start_agent
transaction = create_transaction
logs = capture_logs do
diff --git a/spec/lib/appsignal/integrations/data_mapper_spec.rb b/spec/lib/appsignal/integrations/data_mapper_spec.rb
index 0b0fd58e6..57240a140 100644
--- a/spec/lib/appsignal/integrations/data_mapper_spec.rb
+++ b/spec/lib/appsignal/integrations/data_mapper_spec.rb
@@ -2,7 +2,6 @@
describe Appsignal::Hooks::DataMapperLogListener do
describe "#log" do
- let(:transaction) { http_request_transaction }
let(:message) do
double(
:query => "SELECT * from users",
@@ -15,16 +14,13 @@ def log(message)
end
end)
stub_const("DataObjects", Module.new)
- start_agent
- set_current_transaction(transaction)
end
- around { |example| keep_transactions { example.run } }
def log_message
connection_class.new.log(message)
end
- context "when the scheme is SQL-like" do
+ describe "a SQL-like scheme" do
let(:connection_class) { DataObjects::Sqlite3::Connection }
before do
stub_const("DataObjects::Sqlite3::Connection", Class.new do
@@ -33,9 +29,16 @@ def log_message
end)
end
- it "records the log entry in an event" do
+ def perform
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
log_message
+ transaction
+ end
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = perform
expect(transaction).to include_event(
"name" => "query.data_mapper",
"title" => "DataMapper Query",
@@ -44,9 +47,28 @@ def log_message
"duration" => 100.0
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("DataMapper Query")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ attrs = span.attributes
+ expect(attrs["db.query.text"]).to eq("SELECT * from users")
+ expect(attrs["db.system.name"]).to eq("other_sql")
+ expect(attrs["appsignal.category"]).to eq("query.data_mapper")
+ expect(attrs).not_to have_key("appsignal.body")
+ observed = span.end_timestamp - span.start_timestamp
+ expect(observed).to be_within(50_000_000).of(100_000_000)
+ end
end
- context "when the scheme is not SQL-like" do
+ describe "a non-SQL scheme" do
let(:connection_class) { DataObjects::MongoDB::Connection }
before do
stub_const("DataObjects::MongoDB::Connection", Class.new do
@@ -55,9 +77,16 @@ def log_message
end)
end
- it "records the log entry in an event without body" do
+ def perform
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
log_message
+ transaction
+ end
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = perform
expect(transaction).to include_event(
"name" => "query.data_mapper",
"title" => "DataMapper Query",
@@ -66,6 +95,25 @@ def log_message
"duration" => 100.0
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("DataMapper Query")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ attrs = span.attributes
+ expect(attrs["appsignal.category"]).to eq("query.data_mapper")
+ expect(attrs).not_to have_key("appsignal.body")
+ expect(attrs).not_to have_key("db.query.text")
+ expect(attrs).not_to have_key("db.system.name")
+ observed = span.end_timestamp - span.start_timestamp
+ expect(observed).to be_within(50_000_000).of(100_000_000)
+ end
end
end
end
diff --git a/spec/lib/appsignal/integrations/delayed_job_plugin_spec.rb b/spec/lib/appsignal/integrations/delayed_job_plugin_spec.rb
index 25b4eb3aa..28e39df1e 100644
--- a/spec/lib/appsignal/integrations/delayed_job_plugin_spec.rb
+++ b/spec/lib/appsignal/integrations/delayed_job_plugin_spec.rb
@@ -12,7 +12,6 @@ def self.plugins
end
end)
require "appsignal/integrations/delayed_job_plugin"
- start_agent(:options => options)
end
# We haven't found a way to test the hooks, we'll have to do that manually
@@ -23,6 +22,7 @@ def self.plugins
let(:created_at) { time - 3600 }
let(:run_at) { time - 3600 }
let(:payload_object) { double(:args => args) }
+ let(:start_agent_args) { { :options => options } }
let(:job_data) do
{
:id => 123,
@@ -41,28 +41,50 @@ def self.plugins
def perform
Timecop.freeze(time) do
- keep_transactions do
- plugin.invoke_with_instrumentation(job, invoked_block)
- end
+ plugin.invoke_with_instrumentation(job, invoked_block)
end
end
context "with a normal call" do
- it "wraps it in a transaction" do
- perform
-
- transaction = last_transaction
- expect(transaction).to have_namespace("background_job")
- expect(transaction).to have_action("TestClass#perform")
- expect(transaction).to_not have_error
- expect(transaction).to include_event(:name => "perform_job.delayed_job")
- expect(transaction).to include_tags(
- "priority" => 1,
- "attempts" => 1,
- "queue" => "default",
- "id" => "123"
- )
- expect(transaction).to include_params(["argument"])
+ describe "wraps it in a transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_namespace("background_job")
+ expect(transaction).to have_action("TestClass#perform")
+ expect(transaction).to_not have_error
+ expect(transaction).to include_event(:name => "perform_job.delayed_job")
+ expect(transaction).to include_tags(
+ "priority" => 1,
+ "attempts" => 1,
+ "queue" => "default",
+ "id" => "123"
+ )
+ expect(transaction).to include_params(["argument"])
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.action_name"]).to eq("TestClass#perform")
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(exception_events).to be_empty
+ span = event_spans.find { |s| s.name == "perform_job.delayed_job" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes).not_to have_key("appsignal.body")
+ expect(span.attributes["appsignal.category"]).to eq("perform_job.delayed_job")
+ expect(root_span.attributes["appsignal.tag.priority"]).to eq(1)
+ expect(root_span.attributes["appsignal.tag.attempts"]).to eq(1)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("default")
+ expect(root_span.attributes["appsignal.tag.id"]).to eq("123")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(["argument"])
+ end
end
context "with more complex params" do
@@ -73,19 +95,41 @@ def perform
}
end
- it "adds the more complex arguments" do
- perform
+ describe "adds the more complex arguments" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_params("foo" => "Foo", "bar" => "Bar")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to include_params("foo" => "Foo", "bar" => "Bar")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq("foo" => "Foo", "bar" => "Bar")
+ end
end
context "with parameter filtering" do
let(:options) { { :filter_parameters => ["foo"] } }
- it "filters selected arguments" do
- perform
+ describe "filters selected arguments" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_params("foo" => "[FILTERED]", "bar" => "Bar")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to include_params("foo" => "[FILTERED]", "bar" => "Bar")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq("foo" => "[FILTERED]", "bar" => "Bar")
+ end
end
end
end
@@ -93,7 +137,8 @@ def perform
context "with run_at in the future" do
let(:run_at) { Time.parse("2017-01-01 10:01:00UTC") }
- it "reports queue_start with run_at time" do
+ it "reports queue_start with run_at time", :agent_mode do
+ start_agent(**start_agent_args)
perform
expect(last_transaction).to have_queue_start(run_at.to_i * 1000)
@@ -105,39 +150,87 @@ def perform
{ :name => "CustomClassMethod.perform", :payload_object => payload_object }
end
- it "wraps it in a transaction using the class method job name" do
- perform
- expect(last_transaction).to have_action("CustomClassMethod.perform")
+ describe "wraps it in a transaction using the class method job name" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("CustomClassMethod.perform")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("CustomClassMethod.perform")
+ end
end
end
context "with custom name call" do
- before { perform }
-
context "with appsignal_name defined" do
context "with payload_object being an object" do
context "with value" do
let(:payload_object) { double(:appsignal_name => "CustomClass#perform") }
- it "wraps it in a transaction using the custom name" do
- expect(last_transaction).to have_action("CustomClass#perform")
+ describe "wraps it in a transaction using the custom name" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("CustomClass#perform")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("CustomClass#perform")
+ end
end
end
context "with non-String value" do
let(:payload_object) { double(:appsignal_name => Object.new) }
- it "wraps it in a transaction using the original job name" do
- expect(last_transaction).to have_action("TestClass#perform")
+ describe "wraps it in a transaction using the original job name" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("TestClass#perform")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("TestClass#perform")
+ end
end
end
context "with class method name as job" do
let(:payload_object) { double(:appsignal_name => "CustomClassMethod.perform") }
- it "wraps it in a transaction using the custom name" do
- perform
- expect(last_transaction).to have_action("CustomClassMethod.perform")
+ describe "wraps it in a transaction using the custom name" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("CustomClassMethod.perform")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("CustomClassMethod.perform")
+ end
end
end
end
@@ -146,25 +239,63 @@ def perform
context "with value" do
let(:payload_object) { double(:appsignal_name => "CustomClassHash#perform") }
- it "wraps it in a transaction using the custom name" do
- expect(last_transaction).to have_action("CustomClassHash#perform")
+ describe "wraps it in a transaction using the custom name" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("CustomClassHash#perform")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("CustomClassHash#perform")
+ end
end
end
context "with non-String value" do
let(:payload_object) { double(:appsignal_name => Object.new) }
- it "wraps it in a transaction using the original job name" do
- expect(last_transaction).to have_action("TestClass#perform")
+ describe "wraps it in a transaction using the original job name" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("TestClass#perform")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("TestClass#perform")
+ end
end
end
context "with class method name as job" do
let(:payload_object) { { :appsignal_name => "CustomClassMethod.perform" } }
- it "wraps it in a transaction using the custom name" do
- perform
- expect(last_transaction).to have_action("CustomClassMethod.perform")
+ describe "wraps it in a transaction using the custom name" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("CustomClassMethod.perform")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("CustomClassMethod.perform")
+ end
end
end
end
@@ -185,8 +316,21 @@ def self.appsignal_name
# this means ClassActingAsHash returns `Object.new` instead
# of `self.appsignal_name`. Since this isn't a valid `String`
# we return the default job name as action name.
- it "wraps it in a transaction using the original job name" do
- expect(last_transaction).to have_action("TestClass#perform")
+ describe "wraps it in a transaction using the original job name" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("TestClass#perform")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("TestClass#perform")
+ end
end
end
end
@@ -197,9 +341,20 @@ def self.appsignal_name
{ :name => "Banana", :payload_object => payload_object }
end
- it "appends #perform to the class name" do
- perform
- expect(last_transaction).to have_action("Banana#perform")
+ describe "appends #perform to the class name" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("Banana#perform")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"]).to eq("Banana#perform")
+ end
end
end
@@ -230,21 +385,45 @@ def self.appsignal_name
end
let(:args) { ["activejob_argument"] }
- it "wraps it in a transaction with the correct params" do
- perform
+ describe "wraps it in a transaction with the correct params" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
- transaction = last_transaction
- expect(transaction).to have_namespace("background_job")
- expect(transaction).to have_action("TestClass#perform")
- expect(transaction).to_not have_error
- expect(transaction).to include_event("name" => "perform_job.delayed_job")
- expect(transaction).to include_tags(
- "priority" => 1,
- "attempts" => 1,
- "queue" => "default",
- "id" => "123"
- )
- expect(transaction).to include_params(["activejob_argument"])
+ transaction = last_transaction
+ expect(transaction).to have_namespace("background_job")
+ expect(transaction).to have_action("TestClass#perform")
+ expect(transaction).to_not have_error
+ expect(transaction).to include_event("name" => "perform_job.delayed_job")
+ expect(transaction).to include_tags(
+ "priority" => 1,
+ "attempts" => 1,
+ "queue" => "default",
+ "id" => "123"
+ )
+ expect(transaction).to include_params(["activejob_argument"])
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.action_name"]).to eq("TestClass#perform")
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(exception_events).to be_empty
+ span = event_spans.find { |s| s.name == "perform_job.delayed_job" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes).not_to have_key("appsignal.body")
+ expect(span.attributes["appsignal.category"]).to eq("perform_job.delayed_job")
+ expect(root_span.attributes["appsignal.tag.priority"]).to eq(1)
+ expect(root_span.attributes["appsignal.tag.attempts"]).to eq(1)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("default")
+ expect(root_span.attributes["appsignal.tag.id"]).to eq("123")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(["activejob_argument"])
+ end
end
context "with more complex params" do
@@ -255,35 +434,62 @@ def self.appsignal_name
}
end
- it "adds the more complex arguments" do
- perform
- transaction = last_transaction
- expect(transaction).to have_action("TestClass#perform")
- expect(transaction).to include_params(
- "foo" => "Foo",
- "bar" => "Bar"
- )
- end
-
- context "with parameter filtering" do
- let(:options) { { :filter_parameters => ["foo"] } }
-
- it "filters selected arguments" do
+ describe "adds the more complex arguments" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
perform
+
transaction = last_transaction
expect(transaction).to have_action("TestClass#perform")
expect(transaction).to include_params(
- "foo" => "[FILTERED]",
+ "foo" => "Foo",
"bar" => "Bar"
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"]).to eq("TestClass#perform")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq("foo" => "Foo", "bar" => "Bar")
+ end
+ end
+
+ context "with parameter filtering" do
+ let(:options) { { :filter_parameters => ["foo"] } }
+
+ describe "filters selected arguments" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_action("TestClass#perform")
+ expect(transaction).to include_params(
+ "foo" => "[FILTERED]",
+ "bar" => "Bar"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"]).to eq("TestClass#perform")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq("foo" => "[FILTERED]", "bar" => "Bar")
+ end
+ end
end
end
context "with run_at in the future" do
let(:run_at) { Time.parse("2017-01-01 10:01:00UTC") }
- it "reports queue_start with run_at time" do
+ it "reports queue_start with run_at time", :agent_mode do
+ start_agent(**start_agent_args)
perform
expect(last_transaction).to have_queue_start(run_at.to_i * 1000)
@@ -299,15 +505,36 @@ def self.appsignal_name
expect(invoked_block).to receive(:call).and_raise(error)
end
- it "adds the error to the transaction" do
- expect do
- perform
- end.to raise_error(error)
+ describe "adds the error to the transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect do
+ perform
+ end.to raise_error(error)
+
+ transaction = last_transaction
+ expect(transaction).to have_namespace("background_job")
+ expect(transaction).to have_action("TestClass#perform")
+ expect(transaction).to have_error("ExampleException", "uh oh")
+ end
- transaction = last_transaction
- expect(transaction).to have_namespace("background_job")
- expect(transaction).to have_action("TestClass#perform")
- expect(transaction).to have_error("ExampleException", "uh oh")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect do
+ perform
+ end.to raise_error(error)
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("TestClass#perform")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
end
@@ -315,6 +542,8 @@ def self.appsignal_name
describe ".extract_value" do
let(:plugin) { Appsignal::Integrations::DelayedJobPlugin }
+ before { start_agent }
+
context "for a hash" do
let(:hash) { { :key => "value", :bool_false => false } }
diff --git a/spec/lib/appsignal/integrations/excon_spec.rb b/spec/lib/appsignal/integrations/excon_spec.rb
new file mode 100644
index 000000000..3a50cf245
--- /dev/null
+++ b/spec/lib/appsignal/integrations/excon_spec.rb
@@ -0,0 +1,60 @@
+if DependencyHelper.excon_present?
+ require "excon"
+ require "appsignal/integrations/excon"
+ require "appsignal/integrations/excon/appsignal_middleware"
+
+ # Integration test against the real Excon gem (the hooks/excon_spec.rb suite
+ # stubs Excon). Verifies, end to end, that the inject-only middleware writes
+ # trace context onto a live outgoing request while the instrumentor's client
+ # event span is current -- the ordering the stubbed suite can't prove.
+ describe "Excon integration" do
+ before { Appsignal::Hooks::ExconHook.new.install }
+
+ describe "a GET request" do
+ def perform
+ stub_request(:get, "http://www.example.com/")
+ Excon.get("http://www.example.com/")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "request.excon",
+ "title" => "GET http://www.example.com"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ span = event_spans.find { |s| s.attributes["appsignal.category"] == "request.excon" }
+ expect(span).not_to be_nil
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+
+ # The injected traceparent must reflect the Excon CLIENT event span, not
+ # the root span -- proving the middleware runs inside the instrumentor's
+ # event span on a real request.
+ expect(injected_traceparent("http://www.example.com/"))
+ .to eq("00-#{span.hex_trace_id}-#{span.hex_span_id}-01")
+ end
+ end
+
+ # Reads the `traceparent` header off the recorded outgoing request to `url`.
+ def injected_traceparent(url)
+ traceparent = nil
+ expect(
+ a_request(:get, url).with { |request| traceparent = request.headers["Traceparent"] }
+ ).to have_been_made
+ traceparent
+ end
+ end
+end
diff --git a/spec/lib/appsignal/integrations/faraday_spec.rb b/spec/lib/appsignal/integrations/faraday_spec.rb
index 46d3658dd..45affbb9c 100644
--- a/spec/lib/appsignal/integrations/faraday_spec.rb
+++ b/spec/lib/appsignal/integrations/faraday_spec.rb
@@ -1,25 +1,25 @@
if DependencyHelper.faraday_present?
require "faraday"
require "appsignal/integrations/faraday"
- require "faraday/excon" if DependencyHelper.excon_present?
# Integration test against the real Faraday gem. The hook auto-installs
# AppSignal's middleware onto every connection, so the `request.faraday` event
- # is recorded without the user adding anything themselves -- and without a
- # dependency on ActiveSupport (these gemfiles no longer load it).
+ # is recorded and outgoing requests carry trace context, without the user
+ # adding anything -- and without a dependency on ActiveSupport (these gemfiles
+ # no longer load it).
describe "Faraday integration" do
before { Appsignal::Hooks::FaradayHook.new.install }
# The common case: the default adapter is Net::HTTP, which AppSignal also
# instruments. Faraday suppresses it, so the request is recorded once -- as
- # the `request.faraday` event.
+ # the `request.faraday` event, which also writes the `traceparent`.
describe "a request over the default Net::HTTP adapter" do
def perform
stub_request(:get, "http://www.example.com/")
Faraday.new("http://www.example.com").get("/")
end
- it "records the request once, as the Faraday event" do
+ it "in agent mode", :agent_mode do
start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
@@ -34,36 +34,68 @@ def perform
# Net::HTTP is suppressed under Faraday, so it isn't recorded again.
expect(transaction).to_not include_event("name" => "request.net_http")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ faraday_span = event_span("request.faraday")
+ expect(faraday_span).not_to be_nil
+ expect(faraday_span.kind).to eq(:client)
+ expect(faraday_span.parent_span_id).to eq(root_span.span_id)
+
+ # Net::HTTP is suppressed, so there's no nested net_http span.
+ expect(event_span("request.net_http")).to be_nil
+
+ # Faraday writes the wire traceparent (Net::HTTP doesn't run its inject).
+ expect(injected_traceparent("http://www.example.com/"))
+ .to eq("00-#{faraday_span.hex_trace_id}-#{faraday_span.hex_span_id}-01")
+ end
end
- # Excon is also a Faraday adapter, and AppSignal instruments it through
- # Excon's instrumentor. Faraday suppresses it too, so the request is recorded
- # once -- as the `request.faraday` event.
- describe "a request over the Excon adapter", :if => DependencyHelper.excon_present? do
- before { Appsignal::Hooks::ExconHook.new.install }
+ # With a non-Net::HTTP adapter (here Faraday's test adapter), our inject
+ # middleware is the only thing writing context, so the request carries the
+ # `request.faraday` client span's traceparent -- proving the middleware runs
+ # and injects inside that event's span. This is the path that gives Faraday
+ # propagation for adapters AppSignal doesn't instrument directly.
+ it "injects the Faraday client context on a non-Net::HTTP adapter", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
- def perform
- stub_request(:get, "http://www.example.com/")
- connection = Faraday.new("http://www.example.com") do |faraday|
- faraday.adapter :excon
+ captured_env = nil
+ connection = Faraday.new("http://www.example.com") do |faraday|
+ faraday.adapter :test do |stub|
+ stub.get("/") do |env|
+ captured_env = env
+ [200, {}, ""]
+ end
end
- connection.get("/")
end
+ connection.get("/")
+ Appsignal::Transaction.complete_current!
- it "records the request once, as the Faraday event" do
- start_agent
- transaction = http_request_transaction
- set_current_transaction(transaction)
- perform
+ faraday_span = event_span("request.faraday")
+ expect(faraday_span).not_to be_nil
+ expect(captured_env.request_headers["traceparent"])
+ .to eq("00-#{faraday_span.hex_trace_id}-#{faraday_span.hex_span_id}-01")
+ end
- expect(transaction).to include_event(
- "name" => "request.faraday",
- "title" => "GET http://www.example.com",
- "body" => ""
- )
- # Excon is suppressed under Faraday, so it isn't recorded again.
- expect(transaction).to_not include_event("name" => "request.excon")
- end
+ # Finds the recorded event span for an `appsignal.category` (AS::N name).
+ def event_span(category)
+ event_spans.find { |span| span.attributes["appsignal.category"] == category }
+ end
+
+ # Reads the `traceparent` header off the recorded outgoing request to `url`.
+ def injected_traceparent(url)
+ traceparent = nil
+ expect(
+ a_request(:get, url).with { |request| traceparent = request.headers["Traceparent"] }
+ ).to have_been_made
+ traceparent
end
end
end
diff --git a/spec/lib/appsignal/integrations/http_spec.rb b/spec/lib/appsignal/integrations/http_spec.rb
index 1b01e4797..b48478ecd 100644
--- a/spec/lib/appsignal/integrations/http_spec.rb
+++ b/spec/lib/appsignal/integrations/http_spec.rb
@@ -6,78 +6,177 @@
describe Appsignal::Integrations::HttpIntegration do
let(:transaction) { http_request_transaction }
- around do |example|
- keep_transactions { example.run }
- end
- before do
- start_agent
- set_current_transaction(transaction)
- end
-
- it "instruments a HTTP request" do
- stub_request(:get, "http://www.google.com")
-
- HTTP.get("http://www.google.com")
+ describe "instrumenting a HTTP request" do
+ def perform
+ stub_request(:get, "http://www.google.com")
- expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "name" => "request.http_rb",
- "title" => "GET http://www.google.com"
- )
- end
+ HTTP.get("http://www.google.com")
+ end
- it "instruments a HTTPS request" do
- stub_request(:get, "https://www.google.com")
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
- HTTP.get("https://www.google.com")
+ expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "name" => "request.http_rb",
+ "title" => "GET http://www.google.com"
+ )
+ end
- expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "name" => "request.http_rb",
- "title" => "GET https://www.google.com"
- )
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET http://www.google.com")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ expect(span.attributes).not_to have_key("appsignal.body")
+
+ # The outgoing request carries a W3C traceparent for the client span, so
+ # the called service joins this trace.
+ expect(injected_traceparent("http://www.google.com/"))
+ .to eq("00-#{span.hex_trace_id}-#{span.hex_span_id}-01")
+ end
end
- context "with request parameters" do
- it "does not include the query parameters in the title" do
- stub_request(:get, "https://www.google.com?q=Appsignal")
+ describe "instrumenting a HTTPS request" do
+ def perform
+ stub_request(:get, "https://www.google.com")
- HTTP.get("https://www.google.com", :params => { :q => "Appsignal" })
+ HTTP.get("https://www.google.com")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+ expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
expect(transaction).to include_event(
"body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "name" => "request.http_rb",
"title" => "GET https://www.google.com"
)
end
- it "does not include the request body in the title" do
- stub_request(:post, "https://www.google.com")
- .with(:body => { :q => "Appsignal" }.to_json)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET https://www.google.com")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ expect(span.attributes).not_to have_key("appsignal.body")
+
+ expect(injected_traceparent("https://www.google.com/"))
+ .to eq("00-#{span.hex_trace_id}-#{span.hex_span_id}-01")
+ end
+ end
- HTTP.post("https://www.google.com", :json => { :q => "Appsignal" })
+ context "with request parameters" do
+ describe "not including the query parameters in the title" do
+ def perform
+ stub_request(:get, "https://www.google.com?q=Appsignal")
- expect(transaction).to include_event(
- "body" => "",
- "title" => "POST https://www.google.com"
- )
+ HTTP.get("https://www.google.com", :params => { :q => "Appsignal" })
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "body" => "",
+ "title" => "GET https://www.google.com"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET https://www.google.com")
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ expect(span.attributes).not_to have_key("appsignal.body")
+ end
+ end
+
+ describe "not including the request body in the title" do
+ def perform
+ stub_request(:post, "https://www.google.com")
+ .with(:body => { :q => "Appsignal" }.to_json)
+
+ HTTP.post("https://www.google.com", :json => { :q => "Appsignal" })
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "body" => "",
+ "title" => "POST https://www.google.com"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("POST https://www.google.com")
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ expect(span.attributes).not_to have_key("appsignal.body")
+ end
end
end
describe "following redirects" do
# `HTTP.follow` chains through `HTTP::Session#request` in http6, which is
# instrumented separately from `HTTP::Client#request`. The event is
- # recorded at the request boundary, so a redirected request is a single
- # `request.http_rb` event spanning every hop.
- it "records a single event spanning every hop" do
+ # recorded at the request boundary, so a redirected request stays a single
+ # `request.http_rb` event (span) spanning every hop; trace context still
+ # rides on each hop, injected at `perform`.
+ def perform
stub_request(:get, "http://www.google.com")
.to_return(:status => 301, :headers => { "Location" => "http://www.example.com" })
stub_request(:get, "http://www.example.com").to_return(:status => 200)
HTTP.follow.get("http://www.google.com")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
events = transaction.to_h["events"]
.select { |event| event["name"] == "request.http_rb" }
@@ -85,69 +184,191 @@
["GET http://www.google.com"]
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET http://www.google.com")
+ expect(span.kind).to eq(:client)
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ end
end
context "with various URI objects" do
- it "parses an object responding to #to_s" do
- request_uri = Struct.new(:uri) do
- def to_s
- uri.to_s
+ describe "parsing an object responding to #to_s" do
+ def perform
+ request_uri = Struct.new(:uri) do
+ def to_s
+ uri.to_s
+ end
end
+
+ stub_request(:get, "http://www.google.com")
+
+ HTTP.get(request_uri.new("http://www.google.com"))
end
- stub_request(:get, "http://www.google.com")
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
- HTTP.get(request_uri.new("http://www.google.com"))
+ expect(transaction).to include_event(
+ "name" => "request.http_rb",
+ "title" => "GET http://www.google.com"
+ )
+ end
- expect(transaction).to include_event(
- "name" => "request.http_rb",
- "title" => "GET http://www.google.com"
- )
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET http://www.google.com")
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ end
end
- it "parses an URI object" do
- stub_request(:get, "http://www.google.com")
+ describe "parsing an URI object" do
+ def perform
+ stub_request(:get, "http://www.google.com")
- HTTP.get(URI("http://www.google.com"))
+ HTTP.get(URI("http://www.google.com"))
+ end
- expect(transaction).to include_event(
- "name" => "request.http_rb",
- "title" => "GET http://www.google.com"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "request.http_rb",
+ "title" => "GET http://www.google.com"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET http://www.google.com")
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ end
end
- it "parses an HTTP::URI object" do
- stub_request(:get, "http://www.google.com")
+ describe "parsing an HTTP::URI object" do
+ def perform
+ stub_request(:get, "http://www.google.com")
- HTTP.get(HTTP::URI.parse("http://www.google.com"))
+ HTTP.get(HTTP::URI.parse("http://www.google.com"))
+ end
- expect(transaction).to include_event(
- "name" => "request.http_rb",
- "title" => "GET http://www.google.com"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "request.http_rb",
+ "title" => "GET http://www.google.com"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET http://www.google.com")
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ end
end
- it "parses a string" do
- stub_request(:get, "http://www.google.com")
+ describe "parsing a string" do
+ def perform
+ stub_request(:get, "http://www.google.com")
- HTTP.get("http://www.google.com")
+ HTTP.get("http://www.google.com")
+ end
- expect(transaction).to include_event(
- "name" => "request.http_rb",
- "title" => "GET http://www.google.com"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "request.http_rb",
+ "title" => "GET http://www.google.com"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET http://www.google.com")
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ end
end
- it "parses a string with non-ascii characters" do
- stub_request(:get, "http://www.example.com/áéíóúãÔù")
+ describe "parsing a string with non-ascii characters" do
+ def perform
+ stub_request(:get, "http://www.example.com/áéíóúãÔù")
- HTTP.get("http://www.example.com/áéíóúãÔù")
+ HTTP.get("http://www.example.com/áéíóúãÔù")
+ end
- expect(transaction).to include_event(
- "name" => "request.http_rb",
- "title" => "GET http://www.example.com"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "request.http_rb",
+ "title" => "GET http://www.example.com"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET http://www.example.com")
+ expect(span.attributes["appsignal.category"]).to eq("request.http_rb")
+ end
end
end
+
+ # Reads the `traceparent` header off the recorded outgoing request to `url`.
+ def injected_traceparent(url)
+ traceparent = nil
+ expect(
+ a_request(:get, url).with { |request| traceparent = request.headers["Traceparent"] }
+ ).to have_been_made
+ traceparent
+ end
end
end
diff --git a/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb b/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb
index 0e5934ab6..9c5bc9aa0 100644
--- a/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb
+++ b/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb
@@ -1,5 +1,4 @@
require "appsignal/integrations/mongo_ruby_driver"
-
describe Appsignal::Hooks::MongoMonitorSubscriber do
if DependencyHelper.mongo_present?
let(:subscriber) { Appsignal::Hooks::MongoMonitorSubscriber.new }
@@ -38,9 +37,10 @@ def command_failed_event(
end
# `started` sanitizes the command and stores it on the transaction, keyed by
- # request id, for the matching `succeeded`/`failed` to pick up.
- it "stores the sanitized command on the transaction" do
- start_agent
+ # request id, for the matching `succeeded`/`failed` to pick up. The store
+ # lives on the base transaction (not the backend), so this is identical in
+ # both modes.
+ it_in_both_modes "stores the sanitized command on the transaction" do
transaction = http_request_transaction
set_current_transaction(transaction)
@@ -58,7 +58,7 @@ def perform
subscriber.succeeded(succeeded_event)
end
- it "records the query as an event and emits a duration metric" do
+ it "in agent mode", :agent_mode do
start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
@@ -77,6 +77,27 @@ def perform
"body" => "{\"foo\":\"?\"}"
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ span = event_spans.find { |s| s.attributes["appsignal.category"] == "query.mongodb" }
+ expect(span).not_to be_nil
+ expect(span.name).to eq("find | test | SUCCEEDED")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.category"]).to eq("query.mongodb")
+ expect(span.attributes["appsignal.body"]).to eq("{\"foo\":\"?\"}")
+
+ snapshot = metric_snapshot("mongodb_query_duration")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.data_points.first.sum).to be_within(0.0001).of(0.9919)
+ expect(snapshot.data_points.first.attributes).to eq("database" => "test")
+ end
end
describe "instrumenting a failed query" do
@@ -88,7 +109,7 @@ def perform
subscriber.failed(failed_event)
end
- it "records the query as an event" do
+ it "in agent mode", :agent_mode do
start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
@@ -101,10 +122,27 @@ def perform
"body" => "{\"foo\":\"?\"}"
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ span = event_spans.find { |s| s.attributes["appsignal.category"] == "query.mongodb" }
+ expect(span).not_to be_nil
+ expect(span.name).to eq("find | test | FAILED")
+ expect(span.kind).to eq(:client)
+ expect(span.attributes["appsignal.category"]).to eq("query.mongodb")
+ expect(span.attributes["appsignal.body"]).to eq("{\"foo\":\"?\"}")
+ end
end
- # The subscriber guards on a current, unpaused transaction before touching
- # the extension, so nothing is recorded otherwise.
+ # The subscriber guards (`return unless current?` / `return if paused?`) run
+ # before any backend is touched, so "no instrumentation is recorded" is an
+ # invariant in both modes. Agent mode asserts no extension calls; collector
+ # mode asserts nothing is exported.
describe "without an active transaction" do
def perform
started = command_started_event
@@ -112,7 +150,7 @@ def perform
subscriber.succeeded(command_succeeded_event(started))
end
- it "does not record anything" do
+ it "in agent mode", :agent_mode do
start_agent
expect(Appsignal::Extension).to_not receive(:start_event)
expect(Appsignal::Extension).to_not receive(:finish_event)
@@ -120,6 +158,15 @@ def perform
perform
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect(span_exporter.finished_spans).to be_empty
+ expect(metric_snapshot("mongodb_query_duration")).to be_nil
+ end
end
describe "when the transaction is paused" do
@@ -129,7 +176,7 @@ def perform
subscriber.succeeded(command_succeeded_event(started))
end
- it "does not record anything" do
+ it "in agent mode", :agent_mode do
start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
@@ -141,6 +188,19 @@ def perform
perform
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ transaction.pause!
+
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans).to be_empty
+ expect(metric_snapshot("mongodb_query_duration")).to be_nil
+ end
end
end
end
diff --git a/spec/lib/appsignal/integrations/net_http_spec.rb b/spec/lib/appsignal/integrations/net_http_spec.rb
index 24138e2aa..76238c2f0 100644
--- a/spec/lib/appsignal/integrations/net_http_spec.rb
+++ b/spec/lib/appsignal/integrations/net_http_spec.rb
@@ -1,33 +1,97 @@
require "appsignal/integrations/net_http"
describe Appsignal::Integrations::NetHttpIntegration do
- let(:transaction) { http_request_transaction }
- before { start_agent }
- before { set_current_transaction transaction }
- around { |example| keep_transactions { example.run } }
+ describe "a http request" do
+ def perform
+ stub_request(:any, "http://www.google.com/")
- it "instruments a http request" do
- stub_request(:any, "http://www.google.com/")
+ Net::HTTP.get_response(URI.parse("http://www.google.com"))
+ end
- Net::HTTP.get_response(URI.parse("http://www.google.com"))
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
- expect(transaction).to include_event(
- "name" => "request.net_http",
- "title" => "GET http://www.google.com"
- )
+ expect(transaction).to include_event(
+ "name" => "request.net_http",
+ "title" => "GET http://www.google.com",
+ "body" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET http://www.google.com")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.category"]).to eq("request.net_http")
+ expect(span.attributes).not_to have_key("appsignal.body")
+
+ # The outgoing request carries a W3C traceparent for the client span, so
+ # the called service joins this trace.
+ expect(injected_traceparent("http://www.google.com/"))
+ .to eq("00-#{span.hex_trace_id}-#{span.hex_span_id}-01")
+ end
end
- it "instruments a https request" do
- stub_request(:any, "https://www.google.com/")
+ describe "a https request" do
+ def perform
+ stub_request(:any, "https://www.google.com/")
+
+ uri = URI.parse("https://www.google.com")
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.use_ssl = true
+ http.get(uri.request_uri)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
- uri = URI.parse("https://www.google.com")
- http = Net::HTTP.new(uri.host, uri.port)
- http.use_ssl = true
- http.get(uri.request_uri)
+ expect(transaction).to include_event(
+ "name" => "request.net_http",
+ "title" => "GET https://www.google.com",
+ "body" => ""
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("GET https://www.google.com")
+ expect(span.kind).to eq(:client)
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.category"]).to eq("request.net_http")
+ expect(span.attributes).not_to have_key("appsignal.body")
+
+ expect(injected_traceparent("https://www.google.com/"))
+ .to eq("00-#{span.hex_trace_id}-#{span.hex_span_id}-01")
+ end
+ end
- expect(transaction).to include_event(
- "name" => "request.net_http",
- "title" => "GET https://www.google.com"
- )
+ # Reads the `traceparent` header off the recorded outgoing request to `url`.
+ def injected_traceparent(url)
+ traceparent = nil
+ expect(
+ a_request(:get, url).with { |request| traceparent = request.headers["Traceparent"] }
+ ).to have_been_made
+ traceparent
end
end
diff --git a/spec/lib/appsignal/integrations/object_spec.rb b/spec/lib/appsignal/integrations/object_spec.rb
index a8a27d122..d3c710daa 100644
--- a/spec/lib/appsignal/integrations/object_spec.rb
+++ b/spec/lib/appsignal/integrations/object_spec.rb
@@ -1,8 +1,6 @@
require "appsignal/integrations/object"
describe Object do
- around { |example| keep_transactions { example.run } }
-
describe "#instrument_method" do
context "with instance method" do
let(:klass) do
@@ -14,6 +12,7 @@ def foo(param1, options = {}, keyword_param: 1)
end
end
let(:instance) { klass.new }
+ let(:transaction) { http_request_transaction }
def call_with_arguments
instance.foo(
@@ -24,12 +23,6 @@ def call_with_arguments
end
context "when active" do
- let(:transaction) { http_request_transaction }
- before do
- start_agent
- set_current_transaction(transaction)
- end
-
context "with different kind of arguments" do
let(:klass) do
Class.new do
@@ -62,7 +55,10 @@ def splat(*args, **kwargs)
end
end
- it "instruments the method and calls it" do
+ # Asserts only on return values, which are identical in both modes.
+ it_in_both_modes "instruments the method and calls it" do
+ set_current_transaction(transaction)
+
expect(instance.positional_arguments("abc", "def")).to eq(["abc", "def"])
expect(instance.positional_arguments_splat("abc", "def")).to eq(["abc", "def"])
expect(instance.keyword_arguments(:a => "a", :b => "b")).to eq(["a", "b"])
@@ -77,15 +73,32 @@ def splat(*args, **kwargs)
end
end
- context "with anonymous class" do
- it "instruments the method and calls it" do
+ describe "with anonymous class" do
+ def perform
expect(call_with_arguments).to eq(["abc", { :foo => "bar" }, 2])
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
expect(transaction).to include_event("name" => "foo.AnonymousClass.other")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ expect(event_spans.first.parent_span_id).to eq(root_span.span_id)
+ expect(event_spans.first.name).to eq("foo.AnonymousClass.other")
+ end
end
- context "with named class" do
+ describe "with named class" do
before do
stub_const("NamedClass", Class.new do
def foo
@@ -96,14 +109,31 @@ def foo
end
let(:klass) { NamedClass }
- it "instruments the method and calls it" do
+ def perform
expect(instance.foo).to eq(1)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
expect(transaction).to include_event("name" => "foo.NamedClass.other")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ expect(event_spans.first.parent_span_id).to eq(root_span.span_id)
+ expect(event_spans.first.name).to eq("foo.NamedClass.other")
+ end
end
- context "with nested named class" do
+ describe "with nested named class" do
before do
stub_const("MyModule::NestedModule::NamedClass", Class.new do
def bar
@@ -114,16 +144,33 @@ def bar
end
let(:klass) { MyModule::NestedModule::NamedClass }
- it "instruments the method and calls it" do
+ def perform
expect(instance.bar).to eq(2)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
expect(transaction).to include_event(
"name" => "bar.NamedClass.NestedModule.MyModule.other"
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ expect(event_spans.first.parent_span_id).to eq(root_span.span_id)
+ expect(event_spans.first.name).to eq("bar.NamedClass.NestedModule.MyModule.other")
+ end
end
- context "with custom name" do
+ describe "with custom name" do
let(:klass) do
Class.new do
def foo
@@ -133,12 +180,27 @@ def foo
end
end
- it "instruments with custom name" do
+ def perform
expect(instance.foo).to eq(1)
+ end
- expect(transaction).to include_event(
- "name" => "my_method.group"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+
+ expect(transaction).to include_event("name" => "my_method.group")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ expect(event_spans.first.parent_span_id).to eq(root_span.span_id)
+ expect(event_spans.first.name).to eq("my_method.group")
end
end
@@ -152,7 +214,10 @@ def foo
end
end
- it "yields the block" do
+ # Asserts only on the yielded return value, identical in both modes.
+ it_in_both_modes "yields the block" do
+ set_current_transaction(transaction)
+
expect(instance.foo { 42 }).to eq(42)
end
end
@@ -177,6 +242,8 @@ def self.bar(param1, options = {}, keyword_param: 1)
appsignal_instrument_class_method :bar
end
end
+ let(:transaction) { http_request_transaction }
+
def call_with_arguments
klass.bar(
"abc",
@@ -186,12 +253,6 @@ def call_with_arguments
end
context "when active" do
- let(:transaction) { http_request_transaction }
- before do
- start_agent
- set_current_transaction(transaction)
- end
-
context "with different kind of arguments" do
let(:klass) do
Class.new do
@@ -224,7 +285,10 @@ def self.splat(*args, **kwargs)
end
end
- it "instruments the method and calls it" do
+ # Asserts only on return values, which are identical in both modes.
+ it_in_both_modes "instruments the method and calls it" do
+ set_current_transaction(transaction)
+
expect(klass.positional_arguments("abc", "def")).to eq(["abc", "def"])
expect(klass.positional_arguments_splat("abc", "def")).to eq(["abc", "def"])
expect(klass.keyword_arguments(:a => "a", :b => "b")).to eq(["a", "b"])
@@ -239,17 +303,34 @@ def self.splat(*args, **kwargs)
end
end
- context "with anonymous class" do
- it "instruments the method and calls it" do
+ describe "with anonymous class" do
+ def perform
expect(Appsignal.active?).to be_truthy
expect(call_with_arguments).to eq(["abc", { :foo => "bar" }, 2])
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
transaction._sample
expect(transaction).to include_event("name" => "bar.class_method.AnonymousClass.other")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ expect(event_spans.first.parent_span_id).to eq(root_span.span_id)
+ expect(event_spans.first.name).to eq("bar.class_method.AnonymousClass.other")
+ end
end
- context "with named class" do
+ describe "with named class" do
before do
stub_const("NamedClass", Class.new do
def self.bar
@@ -260,13 +341,30 @@ def self.bar
end
let(:klass) { NamedClass }
- it "instruments the method and calls it" do
+ def perform
expect(Appsignal.active?).to be_truthy
expect(klass.bar).to eq(2)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
expect(transaction).to include_event("name" => "bar.class_method.NamedClass.other")
end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ expect(event_spans.first.parent_span_id).to eq(root_span.span_id)
+ expect(event_spans.first.name).to eq("bar.class_method.NamedClass.other")
+ end
+
context "with nested named class" do
before do
stub_const("MyModule::NestedModule::NamedClass", Class.new do
@@ -278,17 +376,31 @@ def self.bar
end
let(:klass) { MyModule::NestedModule::NamedClass }
- it "instruments the method and calls it" do
- expect(Appsignal.active?).to be_truthy
- expect(klass.bar).to eq(2)
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+
expect(transaction).to include_event(
"name" => "bar.class_method.NamedClass.NestedModule.MyModule.other"
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ expect(event_spans.first.parent_span_id).to eq(root_span.span_id)
+ expect(event_spans.first.name)
+ .to eq("bar.class_method.NamedClass.NestedModule.MyModule.other")
+ end
end
end
- context "with custom name" do
+ describe "with custom name" do
let(:klass) do
Class.new do
def self.bar
@@ -298,12 +410,29 @@ def self.bar
end
end
- it "instruments with custom name" do
+ def perform
expect(Appsignal.active?).to be_truthy
expect(klass.bar).to eq(2)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
expect(transaction).to include_event("name" => "my_method.group")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ expect(event_spans.first.parent_span_id).to eq(root_span.span_id)
+ expect(event_spans.first.name).to eq("my_method.group")
+ end
end
context "with a method given a block" do
@@ -316,7 +445,10 @@ def self.bar
end
end
- it "yields the block" do
+ # Asserts only on the yielded return value, identical in both modes.
+ it_in_both_modes "yields the block" do
+ set_current_transaction(transaction)
+
expect(klass.bar { 42 }).to eq(42)
end
end
diff --git a/spec/lib/appsignal/integrations/ownership_spec.rb b/spec/lib/appsignal/integrations/ownership_spec.rb
index 2a8bd8af6..de36a08ea 100644
--- a/spec/lib/appsignal/integrations/ownership_spec.rb
+++ b/spec/lib/appsignal/integrations/ownership_spec.rb
@@ -2,30 +2,40 @@
require "appsignal/integrations/ownership"
describe Appsignal::Integrations::OwnershipIntegration do
+ let(:start_agent_args) { { :options => config } }
let(:config) { { :ownership_set_namespace => false } }
before do
Ownership.around_change = nil
-
- start_agent(:options => config)
-
Appsignal::Hooks::OwnershipHook.new.install
end
context "when the transaction is created within an owner block" do
- it "adds the owner to the transaction tags" do
- transaction = nil
- owner("owner") do
- transaction = Appsignal::Transaction.create("namespace")
+ describe "adds the owner to the transaction tags" do
+ def perform
+ owner("owner") do
+ @transaction = Appsignal::Transaction.create("namespace")
+ end
end
- keep_transactions { transaction.complete }
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ keep_transactions { @transaction.complete }
- expect(transaction).to include_tags("owner" => "owner")
+ expect(@transaction).to include_tags("owner" => "owner")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ @transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.owner"]).to eq("owner")
+ end
end
- it "does not set the namespace of the transaction to the owner" do
- transaction = nil
+ it_in_both_modes "does not set the namespace of the transaction to the owner" do
owner("owner") do
transaction = Appsignal::Transaction.create("namespace")
expect(transaction.namespace).to eq("namespace")
@@ -35,7 +45,7 @@
context "when `ownership_set_namespace` config option is enabled" do
let(:config) { { :ownership_set_namespace => true } }
- it "sets the namespace of the transaction to the owner" do
+ it_in_both_modes "sets the namespace of the transaction to the owner" do
owner("owner") do
transaction = Appsignal::Transaction.create("namespace")
expect(transaction.namespace).to eq("owner")
@@ -45,33 +55,55 @@
end
context "when the owner is changed after a transaction has been created" do
- it "adds the new owner to the transaction tags" do
- transaction = Appsignal::Transaction.create("namespace")
+ describe "adds the new owner to the transaction tags" do
+ def perform
+ @transaction = Appsignal::Transaction.create("namespace")
+ owner("owner") { nil }
+ end
- owner("owner") do
- keep_transactions { transaction.complete }
- expect(transaction).to include_tags("owner" => "owner")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ keep_transactions { @transaction.complete }
+
+ expect(@transaction).to include_tags("owner" => "owner")
end
- end
- it "keeps the owner tag set by the last ownership change" do
- transaction = Appsignal::Transaction.create("namespace")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ @transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.owner"]).to eq("owner")
+ end
+ end
- owner("first") do
- nil
+ describe "keeps the owner tag set by the last ownership change" do
+ def perform
+ @transaction = Appsignal::Transaction.create("namespace")
+ owner("first") { nil }
+ owner("second") { nil }
end
- owner("second") do
- nil
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ keep_transactions { @transaction.complete }
+
+ expect(@transaction).to include_tags("owner" => "second")
end
- keep_transactions { transaction.complete }
- expect(transaction).to include_tags("owner" => "second")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ @transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.owner"]).to eq("second")
+ end
end
- it "does not set the namespace of the current transaction to the owner" do
+ it_in_both_modes "does not set the namespace of the current transaction to the owner" do
transaction = Appsignal::Transaction.create("namespace")
-
owner("owner") do
expect(transaction.namespace).to eq("namespace")
end
@@ -80,7 +112,7 @@
context "when `ownership_set_namespace` config option is enabled" do
let(:config) { { :ownership_set_namespace => true } }
- it "sets the namespace of the current transaction to the owner" do
+ it_in_both_modes "sets the namespace of the current transaction to the owner" do
transaction = Appsignal::Transaction.create("namespace")
expect(transaction.namespace).to eq("namespace")
@@ -89,69 +121,98 @@
end
end
- it "keeps the namespace given by the last ownership change" do
+ it_in_both_modes "keeps the namespace given by the last ownership change" do
owner("owner") do
transaction = Appsignal::Transaction.create("namespace")
- owner("first") do
- nil
- end
-
- owner("second") do
- nil
- end
+ owner("first") { nil }
+ owner("second") { nil }
expect(transaction.namespace).to eq("second")
end
end
end
- it "allows the `around_change` hook to be set" do
- override = proc do |_owner, block|
- # The `around_change` hook must call `block.call` to actually run
- # the code within the `owner` block, as documented in `ownership`'s
- # README:
- # https://github.com/ankane/ownership/blob/b277ef821654d0e73d2e6c8df4f636932b7a90fa/README.md#custom-integrations
- block.call
- end
+ describe "allows the `around_change` hook to be set" do
+ def perform
+ override = proc do |_owner, block|
+ # The `around_change` hook must call `block.call` to actually run
+ # the code within the `owner` block, as documented in `ownership`'s
+ # README:
+ # https://github.com/ankane/ownership/blob/b277ef821654d0e73d2e6c8df4f636932b7a90fa/README.md#custom-integrations
+ block.call
+ end
- expect(override).to receive(:call).with("owner", kind_of(Proc)).and_call_original
+ expect(override).to receive(:call).with("owner", kind_of(Proc)).and_call_original
- Ownership.around_change = override
+ Ownership.around_change = override
- transaction = Appsignal::Transaction.create("namespace")
+ @transaction = Appsignal::Transaction.create("namespace")
+
+ block = proc {}
+ expect(block).to receive(:call)
+
+ owner("owner", &block)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ keep_transactions { @transaction.complete }
- block = proc {}
- expect(block).to receive(:call)
+ expect(@transaction).to include_tags("owner" => "owner")
+ end
- owner("owner", &block)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ @transaction.complete
- keep_transactions { transaction.complete }
- expect(transaction).to include_tags("owner" => "owner")
+ expect(root_span.attributes["appsignal.tag.owner"]).to eq("owner")
+ end
end
end
context "when an error is reported in a transaction" do
- it "sets the owner tag of the transaction to the owner where the error was raised" do
- transaction = Appsignal::Transaction.create("namespace")
+ describe "sets the owner tag of the transaction to the owner where the error was raised" do
+ def perform
+ @transaction = Appsignal::Transaction.create("namespace")
- begin
- owner("error") do
- raise "error"
- end
- rescue StandardError => error
- # This owner should be overriden on the tag by the error owner.
- owner("rescue") do
- nil
+ begin
+ owner("error") do
+ raise "error"
+ end
+ rescue StandardError => error
+ # This owner should be overriden on the tag by the error owner.
+ owner("rescue") { nil }
+
+ @transaction.add_error(error)
end
+ end
- transaction.add_error(error)
- keep_transactions { transaction.complete }
- expect(transaction).to include_tags("owner" => "error")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ keep_transactions { @transaction.complete }
+
+ expect(@transaction).to include_tags("owner" => "error")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ @transaction.complete
+
+ # The owner tag is driven by the recorded error; assert the exception
+ # event that produced it is present.
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.message"]).to eq("error")
+ expect(root_span.attributes["appsignal.tag.owner"]).to eq("error")
end
end
- it "does not set the namespace of the transaction to the owner where the error was raised" do
+ it_in_both_modes "does not set the namespace to the owner where the error was raised" do
transaction = Appsignal::Transaction.create("namespace")
begin
@@ -159,9 +220,7 @@
raise "error"
end
rescue StandardError => error
- owner("rescue") do
- nil
- end
+ owner("rescue") { nil }
transaction.add_error(error)
transaction.complete
@@ -172,7 +231,7 @@
context "when `ownership_set_namespace` config option is enabled" do
let(:config) { { :ownership_set_namespace => true } }
- it "sets the namespace of the transaction to the owner where the error was raised" do
+ it_in_both_modes "sets the namespace to the owner where the error was raised" do
transaction = Appsignal::Transaction.create("namespace")
begin
@@ -181,9 +240,7 @@
end
rescue StandardError => error
# This owner should be overriden on the namespace by the error owner.
- owner("rescue") do
- nil
- end
+ owner("rescue") { nil }
expect(transaction.namespace).to eq("rescue")
transaction.add_error(error)
@@ -195,83 +252,111 @@
end
context "when several errors are reported in a transaction" do
- it "sets the owner tag of the transaction to the owner where its error was raised" do
- transaction = Appsignal::Transaction.create("namespace")
+ describe "sets the owner tag of the transaction to the owner where its error was raised" do
+ def perform
+ @transaction = Appsignal::Transaction.create("namespace")
- begin
- owner("first") do
- raise "first error"
- end
- rescue StandardError => first_error
- # This owner should be overriden on the tag by the error owner.
- owner("first_rescue") do
- nil
+ begin
+ owner("first") do
+ raise "first error"
+ end
+ rescue StandardError => first_error
+ # This owner should be overriden on the tag by the error owner.
+ owner("first_rescue") { nil }
+ @transaction.add_error(first_error)
end
- transaction.add_error(first_error)
+ begin
+ owner("second") do
+ raise "second error"
+ end
+ rescue StandardError => second_error
+ # This owner should be overriden on the tag by the error owner.
+ owner("second_rescue") { nil }
+ @transaction.add_error(second_error)
+ end
end
- begin
- owner("second") do
- raise "second error"
- end
- rescue StandardError => second_error
- # This owner should be overriden on the tag by the error owner.
- owner("second_rescue") do
- nil
- end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ keep_transactions { @transaction.complete }
- transaction.add_error(second_error)
+ expect(created_transactions.length).to eq(2)
+ expect(created_transactions.find { |t| t == @transaction })
+ .to include_tags("owner" => "first")
+ expect(created_transactions.find { |t| t != @transaction })
+ .to include_tags("owner" => "second")
end
- keep_transactions { transaction.complete }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ @transaction.complete
- expect(created_transactions.length).to eq(2)
- expect(created_transactions.find do |t|
- t == transaction
- end).to include_tags("owner" => "first")
- expect(created_transactions.find do |t|
- t != transaction
- end).to include_tags("owner" => "second")
+ # In collector mode, multiple errors are recorded as exception events
+ # on the single root span — no duplicate transactions. The owner tag
+ # is set by the `before_complete` hook with the first error's owner.
+ root_spans = span_exporter.finished_spans.select do |s|
+ [:server, :consumer].include?(s.kind)
+ end
+ expect(root_spans.size).to eq(1)
+ events = root_spans.first.events.select { |e| e.name == "exception" }
+ expect(events.map { |e| e.attributes["exception.message"] })
+ .to contain_exactly("first error", "second error")
+ expect(root_span.attributes["appsignal.tag.owner"]).to eq("first")
+ end
end
context "when `ownership_set_namespace` config option is enabled" do
let(:config) { { :ownership_set_namespace => true } }
- it "sets the namespace of each transaction to the owner where its error was raised" do
- transaction = Appsignal::Transaction.create("namespace")
-
- begin
- owner("first") do
- raise "first error"
- end
- rescue StandardError => first_error
- # This owner should be overriden on the namespace by the error owner.
- owner("first_rescue") do
- nil
+ describe "sets the namespace of each transaction to the owner where its error was raised" do
+ def perform
+ @transaction = Appsignal::Transaction.create("namespace")
+
+ begin
+ owner("first") do
+ raise "first error"
+ end
+ rescue StandardError => first_error
+ # This owner should be overriden on the namespace by the error owner.
+ owner("first_rescue") { nil }
+ @transaction.add_error(first_error)
end
- transaction.add_error(first_error)
+ begin
+ owner("second") do
+ raise "second error"
+ end
+ rescue StandardError => second_error
+ # This owner should be overriden on the namespace by the error owner.
+ owner("second_rescue") { nil }
+ @transaction.add_error(second_error)
+ end
end
- begin
- owner("second") do
- raise "second error"
- end
- rescue StandardError => second_error
- # This owner should be overriden on the namespace by the error owner.
- owner("second_rescue") do
- nil
- end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ keep_transactions { @transaction.complete }
- transaction.add_error(second_error)
+ expect(created_transactions.length).to eq(2)
+ expect(created_transactions.find { |t| t == @transaction }.namespace)
+ .to eq("first")
+ expect(created_transactions.find { |t| t != @transaction }.namespace)
+ .to eq("second")
end
- transaction.complete
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ @transaction.complete
- expect(created_transactions.length).to eq(2)
- expect(created_transactions.find { |t| t == transaction }.namespace).to eq("first")
- expect(created_transactions.find { |t| t != transaction }.namespace).to eq("second")
+ # In collector mode there is one trace: the namespace is set by the
+ # `before_complete` hook with the first error's owner.
+ expect(@transaction.namespace).to eq("first")
+ end
end
end
end
diff --git a/spec/lib/appsignal/integrations/puma_spec.rb b/spec/lib/appsignal/integrations/puma_spec.rb
index 39dea34a0..e739fef12 100644
--- a/spec/lib/appsignal/integrations/puma_spec.rb
+++ b/spec/lib/appsignal/integrations/puma_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "appsignal/integrations/puma"
describe Appsignal::Integrations::PumaServer do
@@ -5,8 +7,10 @@
before do
stub_const("Puma", PumaMock)
stub_const("Puma::Server", puma_server)
- start_agent
+ Appsignal::Hooks::PumaHook.new.install
end
+
+ let(:puma_server) { default_puma_server_mock }
let(:queue_start_time) { fixed_time * 1_000 }
let(:env) do
Rack::MockRequest.env_for(
@@ -20,7 +24,6 @@
let(:server) { Puma::Server.new }
let(:error) { ExampleException.new("error message") }
around { |example| keep_transactions { example.run } }
- before { Appsignal::Hooks::PumaHook.new.install }
def lowlevel_error(error, env, status = nil)
result =
@@ -34,92 +37,205 @@ def lowlevel_error(error, env, status = nil)
result
end
- describe "error reporting" do
- let(:puma_server) { default_puma_server_mock }
+ describe "reporting an error on the active transaction" do
+ def perform
+ lowlevel_error(error, env)
+ end
- context "with active transaction" do
- before { create_transaction }
+ it "in agent mode", :agent_mode do
+ start_agent
+ create_transaction
+ expect do
+ perform
+ end.to_not(change { created_transactions.count })
- it "reports the error to the transaction" do
- expect do
- lowlevel_error(error, env)
- end.to_not(change { created_transactions.count })
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ expect(last_transaction).to include_tags("reported_by" => "puma_lowlevel_error")
+ end
- expect(last_transaction).to have_error("ExampleException", "error message")
- expect(last_transaction).to include_tags("reported_by" => "puma_lowlevel_error")
- end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ create_transaction
+ expect do
+ perform
+ end.to_not(change { created_transactions.count })
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.attributes["appsignal.tag.reported_by"]).to eq("puma_lowlevel_error")
end
+ end
- # This shouldn't happen if the EventHandler is set up correctly, but if
- # it's not it will create a new transaction.
- context "without active transaction" do
- it "creates a new transaction with the error" do
- expect do
- lowlevel_error(error, env)
- end.to change { created_transactions.count }.by(1)
+ # This shouldn't happen if the EventHandler is set up correctly, but if
+ # it's not it will create a new transaction.
+ describe "creating a new transaction with the error when no active transaction" do
+ def perform
+ lowlevel_error(error, env)
+ end
- expect(last_transaction).to have_error("ExampleException", "error message")
- expect(last_transaction).to include_tags("reported_by" => "puma_lowlevel_error")
- end
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect do
+ perform
+ end.to change { created_transactions.count }.by(1)
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ expect(last_transaction).to include_tags("reported_by" => "puma_lowlevel_error")
end
- it "doesn't report internal Puma errors" do
+ it "in collector mode", :collector_mode do
+ start_collector_agent
expect do
- lowlevel_error(Puma::MiniSSL::SSLError.new("error message"), env)
- lowlevel_error(Puma::HttpParserError.new("error message"), env)
- lowlevel_error(Puma::HttpParserError501.new("error message"), env)
- end.to_not(change { created_transactions.count })
+ perform
+ end.to change { created_transactions.count }.by(1)
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.attributes["appsignal.tag.reported_by"]).to eq("puma_lowlevel_error")
end
+ end
- describe "request metadata" do
- it "sets request metadata" do
- lowlevel_error(error, env)
+ it_in_both_modes "doesn't report internal Puma errors" do
+ expect do
+ lowlevel_error(Puma::MiniSSL::SSLError.new("error message"), env)
+ lowlevel_error(Puma::HttpParserError.new("error message"), env)
+ lowlevel_error(Puma::HttpParserError501.new("error message"), env)
+ end.to_not(change { created_transactions.count })
+ end
- expect(last_transaction).to include_metadata(
- "request_method" => "GET",
- "method" => "GET",
- "request_path" => "/some/path",
- "path" => "/some/path"
- )
- expect(last_transaction).to include_environment(
- "REQUEST_METHOD" => "GET",
- "PATH_INFO" => "/some/path"
- # and more, but we don't need to test Rack mock defaults
- )
- end
+ describe "request metadata" do
+ def perform
+ lowlevel_error(error, env)
+ end
- it "sets request parameters" do
- lowlevel_error(error, env)
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
- expect(last_transaction).to include_params(
- "page" => "2",
- "query" => "lorem"
- )
- end
+ expect(last_transaction).to include_metadata(
+ "request_method" => "GET",
+ "method" => "GET",
+ "request_path" => "/some/path",
+ "path" => "/some/path"
+ )
+ expect(last_transaction).to include_environment(
+ "REQUEST_METHOD" => "GET",
+ "PATH_INFO" => "/some/path"
+ # and more, but we don't need to test Rack mock defaults
+ )
+ end
- it "sets session data" do
- lowlevel_error(error, env)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to include_session_data("session" => "data", "user_id" => 123)
- end
+ # Metadata is emitted as appsignal.tag.* attributes in collector mode
+ expect(root_span.attributes["appsignal.tag.request_method"]).to eq("GET")
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("GET")
+ expect(root_span.attributes["appsignal.tag.request_path"]).to eq("/some/path")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/some/path")
+ end
+ end
- it "sets the queue start" do
- lowlevel_error(error, env)
+ describe "request parameters" do
+ def perform
+ lowlevel_error(error, env)
+ end
- expect(last_transaction).to have_queue_start(queue_start_time)
- end
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to include_params(
+ "page" => "2",
+ "query" => "lorem"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ # The transaction uses the HTTP_REQUEST (web) namespace, so params are
+ # stored under appsignal.request.payload.
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("page" => "2", "query" => "lorem")
+ end
+ end
+
+ describe "session data" do
+ def perform
+ lowlevel_error(error, env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to include_session_data("session" => "data", "user_id" => 123)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("session" => "data", "user_id" => 123)
+ end
+ end
+
+ describe "queue start" do
+ def perform
+ lowlevel_error(error, env)
+ end
+
+ # Queue start has no OpenTelemetry consumer; it is agent-only.
+ it "sets the queue start", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_queue_start(queue_start_time)
+ end
+
+ it "completes without error in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to_not raise_error
+ expect(root_span).not_to be_nil
end
end
- context "with Puma::Server#lowlevel_error accepting 3 arguments" do
- let(:puma_server) { default_puma_server_mock }
+ describe "with Puma::Server#lowlevel_error accepting 3 arguments" do
+ def perform(status = nil)
+ lowlevel_error(error, env, status)
+ end
- it "calls the super class with 3 arguments" do
- result = lowlevel_error(error, env, 501)
+ it "in agent mode", :agent_mode do
+ start_agent
+ result = perform(501)
expect(result).to eq([501, {}, ""])
expect(last_transaction).to include_tags("response_status" => 501)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ result = perform(501)
+ expect(result).to eq([501, {}, ""])
+
+ expect(root_span.attributes["appsignal.tag.response_status"]).to eq(501)
+ end
end
context "with Puma::Server#lowlevel_error accepting 2 arguments" do
@@ -131,11 +247,26 @@ def lowlevel_error(_error, _env)
end
end
- it "calls the super class with 3 arguments" do
- result = lowlevel_error(error, env)
- expect(result).to eq([500, {}, ""])
+ describe "calls the super class with 2 arguments and sets the response status" do
+ def perform
+ lowlevel_error(error, env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ result = perform
+ expect(result).to eq([500, {}, ""])
- expect(last_transaction).to include_tags("response_status" => 500)
+ expect(last_transaction).to include_tags("response_status" => 500)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ result = perform
+ expect(result).to eq([500, {}, ""])
+
+ expect(root_span.attributes["appsignal.tag.response_status"]).to eq(500)
+ end
end
end
end
diff --git a/spec/lib/appsignal/integrations/que_spec.rb b/spec/lib/appsignal/integrations/que_spec.rb
index 1fe937dfd..327358fb8 100644
--- a/spec/lib/appsignal/integrations/que_spec.rb
+++ b/spec/lib/appsignal/integrations/que_spec.rb
@@ -25,111 +25,205 @@ def run(post_id, user_id)
let(:instance) { job.new(job_attrs) }
before do
allow(Que).to receive(:execute)
-
- start_agent
end
- around { |example| keep_transactions { example.run } }
def perform_que_job(job)
job._run
end
context "without exception" do
- it "creates a transaction for a job" do
- expect do
- perform_que_job(instance)
- end.to change { created_transactions.length }.by(1)
-
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
- expect(transaction).to have_action("MyQueJob#run")
- expect(transaction).to_not have_error
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "perform_job.que",
- "title" => ""
- )
- expect(transaction).to include_params(
- "arguments" => %w[post_id_123 user_id_123]
- )
- if DependencyHelper.que2_present?
+ def perform
+ perform_que_job(instance)
+ end
+
+ describe "creates a transaction for a job" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
+ expect(transaction).to have_action("MyQueJob#run")
+ expect(transaction).to_not have_error
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "perform_job.que",
+ "title" => ""
+ )
expect(transaction).to include_params(
- "keyword_arguments" => {}
+ "arguments" => %w[post_id_123 user_id_123]
)
- else
- expect(transaction).to_not include_params(
- "keyword_arguments" => anything
+ if DependencyHelper.que2_present?
+ expect(transaction).to include_params(
+ "keyword_arguments" => {}
+ )
+ else
+ expect(transaction).to_not include_params(
+ "keyword_arguments" => anything
+ )
+ end
+ expect(transaction).to include_tags(
+ "attempts" => 0,
+ "id" => 123,
+ "priority" => 100,
+ "queue" => "dfl",
+ "run_at" => fixed_time.to_s
)
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MyQueJob#run")
+ expect(exception_events).to be_empty
+ span = event_spans.find { |s| s.name == "perform_job.que" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes).not_to have_key("appsignal.body")
+ expect(span.attributes["appsignal.category"]).to eq("perform_job.que")
+ expected_params = { "arguments" => %w[post_id_123 user_id_123] }
+ expected_params["keyword_arguments"] = {} if DependencyHelper.que2_present?
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(expected_params)
+ expect(root_span.attributes["appsignal.tag.attempts"]).to eq(0)
+ expect(root_span.attributes["appsignal.tag.id"]).to eq(123)
+ expect(root_span.attributes["appsignal.tag.priority"]).to eq(100)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("dfl")
+ expect(root_span.attributes["appsignal.tag.run_at"]).to eq(fixed_time.to_s)
+ expect(last_transaction).to be_completed
end
- expect(transaction).to include_tags(
- "attempts" => 0,
- "id" => 123,
- "priority" => 100,
- "queue" => "dfl",
- "run_at" => fixed_time.to_s
- )
- expect(transaction).to be_completed
end
end
context "with exception" do
let(:error) { ExampleException.new("oh no!") }
- it "reports exceptions and re-raise them" do
+ before do
allow(instance).to receive(:run).and_raise(error)
+ end
+ def perform
expect do
- expect do
- perform_que_job(instance)
- end.to raise_error(ExampleException)
- end.to change { created_transactions.length }.by(1)
-
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_action("MyQueJob#run")
- expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
- expect(transaction).to have_error(error.class.name, error.message)
- expect(transaction).to include_params(
- "arguments" => %w[post_id_123 user_id_123]
- )
- expect(transaction).to include_tags(
- "attempts" => 0,
- "id" => 123,
- "priority" => 100,
- "queue" => "dfl",
- "run_at" => fixed_time.to_s
- )
- expect(transaction).to be_completed
+ perform_que_job(instance)
+ end.to raise_error(ExampleException)
+ end
+
+ describe "reports exceptions and re-raises them" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_action("MyQueJob#run")
+ expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
+ expect(transaction).to have_error(error.class.name, error.message)
+ expect(transaction).to include_params(
+ "arguments" => %w[post_id_123 user_id_123]
+ )
+ expect(transaction).to include_tags(
+ "attempts" => 0,
+ "id" => 123,
+ "priority" => 100,
+ "queue" => "dfl",
+ "run_at" => fixed_time.to_s
+ )
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MyQueJob#run")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("background")
+ event = exception_events.find { |e| e.attributes["exception.type"] == error.class.name }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.message"]).to eq(error.message)
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expected_params = { "arguments" => %w[post_id_123 user_id_123] }
+ expected_params["keyword_arguments"] = {} if DependencyHelper.que2_present?
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(expected_params)
+ expect(root_span.attributes["appsignal.tag.attempts"]).to eq(0)
+ expect(root_span.attributes["appsignal.tag.id"]).to eq(123)
+ expect(root_span.attributes["appsignal.tag.priority"]).to eq(100)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("dfl")
+ expect(root_span.attributes["appsignal.tag.run_at"]).to eq(fixed_time.to_s)
+ expect(last_transaction).to be_completed
+ end
end
end
context "with error" do
let(:error) { ExampleStandardError.new("oh no!") }
- it "reports errors and not re-raise them" do
+ before do
allow(instance).to receive(:run).and_raise(error)
+ end
+
+ def perform
+ perform_que_job(instance)
+ end
+
+ describe "reports errors and does not re-raise them" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_action("MyQueJob#run")
+ expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
+ expect(transaction).to have_error(error.class.name, error.message)
+ expect(transaction).to include_params(
+ "arguments" => %w[post_id_123 user_id_123]
+ )
+ expect(transaction).to include_tags(
+ "attempts" => 0,
+ "id" => 123,
+ "priority" => 100,
+ "queue" => "dfl",
+ "run_at" => fixed_time.to_s
+ )
+ expect(transaction).to be_completed
+ end
- expect { perform_que_job(instance) }.to change { created_transactions.length }.by(1)
-
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_action("MyQueJob#run")
- expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
- expect(transaction).to have_error(error.class.name, error.message)
- expect(transaction).to include_params(
- "arguments" => %w[post_id_123 user_id_123]
- )
- expect(transaction).to include_tags(
- "attempts" => 0,
- "id" => 123,
- "priority" => 100,
- "queue" => "dfl",
- "run_at" => fixed_time.to_s
- )
- expect(transaction).to be_completed
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MyQueJob#run")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("background")
+ event = exception_events.find { |e| e.attributes["exception.type"] == error.class.name }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.message"]).to eq(error.message)
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expected_params = { "arguments" => %w[post_id_123 user_id_123] }
+ expected_params["keyword_arguments"] = {} if DependencyHelper.que2_present?
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(expected_params)
+ expect(root_span.attributes["appsignal.tag.attempts"]).to eq(0)
+ expect(root_span.attributes["appsignal.tag.id"]).to eq(123)
+ expect(root_span.attributes["appsignal.tag.priority"]).to eq(100)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("dfl")
+ expect(root_span.attributes["appsignal.tag.run_at"]).to eq(fixed_time.to_s)
+ expect(last_transaction).to be_completed
+ end
end
end
@@ -154,13 +248,31 @@ def run(post_id, user_id: nil)
end
end
- it "reports keyword arguments as parameters" do
+ def perform
perform_que_job(instance)
+ end
- expect(last_transaction).to include_params(
- "arguments" => %w[post_id_123],
- "keyword_arguments" => { "user_id" => "user_id_123" }
- )
+ describe "reports keyword arguments as parameters" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to include_params(
+ "arguments" => %w[post_id_123],
+ "keyword_arguments" => { "user_id" => "user_id_123" }
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(
+ "arguments" => %w[post_id_123],
+ "keyword_arguments" => { "user_id" => "user_id_123" }
+ )
+ end
end
end
end
@@ -174,17 +286,70 @@ def run(*_args)
end
end
- it "uses the custom action" do
+ def perform
+ perform_que_job(instance)
+ end
+
+ describe "uses the custom action" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_action("MyCustomJob#perform")
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("MyCustomJob#perform")
+ expect(last_transaction).to be_completed
+ end
+ end
+ end
+
+ context "with incoming trace context" do
+ let(:trace_id_hex) { "0af7651916cd43dd8448eb211c80319c" }
+ let(:span_id_hex) { "b7ad6b7169203331" }
+ let(:traceparent) { "00-#{trace_id_hex}-#{span_id_hex}-01" }
+ # OpenTelemetry's Que instrumentation carries the trace context as
+ # "key:value" tag strings under the job's `data` attribute.
+ let(:job_attrs) do
+ super().merge(:data => { :tags => ["traceparent:#{traceparent}"] })
+ end
+
+ def perform
perform_que_job(instance)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect { perform }.to change { created_transactions.length }.by(1)
+ expect(last_transaction).to be_completed
+ end
- transaction = last_transaction
- expect(transaction).to have_action("MyCustomJob#perform")
- expect(transaction).to be_completed
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ # The job runs as its own trace, linked back to the enqueuer.
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.hex_trace_id).to_not eq(trace_id_hex)
+ expect(root_span.links.size).to eq(1)
+ link_context = root_span.links.first.span_context
+ expect(link_context.hex_trace_id).to eq(trace_id_hex)
+ expect(link_context.hex_span_id).to eq(span_id_hex)
end
end
end
end
+ # Enqueue-side propagation reads context from the job's tags. The carrier
+ # (tags serialized into the job's `data`) is identical on Que 1 and Que 2, so
+ # this is covered on both versions.
describe Appsignal::Integrations::QueClientPlugin do
let(:job) do
Class.new(::Que::Job) do
@@ -201,10 +366,7 @@ def self.name
captured[:values] = values if command == :insert_job
[{}]
end
-
- start_agent
end
- around { |example| keep_transactions { example.run } }
# `data` is the last value Que passes to its `:insert_job` query on both Que
# 1 and Que 2 (Que 2 inserts `kwargs` before it, shifting its index); the
@@ -219,7 +381,8 @@ def enqueue(tags: ["user:42"])
end
context "with an active transaction" do
- it "records an enqueue event and leaves the job's tags untouched" do
+ it "in agent mode", :agent_mode do
+ start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
@@ -229,34 +392,94 @@ def enqueue(tags: ["user:42"])
event = transaction.to_h["events"].find { |e| e["name"] == "enqueue.que" }
expect(event).to_not be_nil
expect(event["title"]).to eq("enqueue MyQueJob job")
+ # No wire context in agent mode; only the user's own tag persists.
expect(enqueued_tags).to eq(["user:42"])
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ enqueue
+ Appsignal::Transaction.complete_current!
+
+ # The enqueue is a producer event span under the active transaction,
+ # named after the job being enqueued.
+ producer = event_spans.find { |s| s.name == "enqueue MyQueJob job" }
+ expect(producer.attributes["appsignal.category"]).to eq("enqueue.que")
+ expect(producer.kind).to eq(:producer)
+ expect(producer.parent_span_id).to eq(root_span.span_id)
+
+ # The job carries the producer span's context as a traceparent tag,
+ # alongside the user's own tag.
+ expect(enqueued_tags).to include("user:42")
+ expect(enqueued_tags)
+ .to include("traceparent:00-#{producer.hex_trace_id}-#{producer.hex_span_id}-01")
+ end
+
+ it "skips propagation rather than break the enqueue when tags are full",
+ :collector_mode do
+ start_collector_agent
+ set_current_transaction(http_request_transaction)
+
+ # Already at Que's 5-tag limit; adding trace context would exceed it, so
+ # propagation is skipped and the enqueue still succeeds unchanged.
+ full = %w[t1 t2 t3 t4 t5]
+ expect { enqueue(:tags => full) }.to_not raise_error
+ Appsignal::Transaction.complete_current!
+
+ expect(enqueued_tags).to eq(full)
+ end
end
context "without an active transaction" do
- it "is a transparent pass-through" do
- expect { enqueue }.to_not raise_error
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ enqueue
+ # No transaction to attach to: nothing recorded, nothing injected.
+ expect(span_exporter.finished_spans.map(&:name)).to_not include("enqueue.que")
expect(enqueued_tags).to eq(["user:42"])
end
end
context "when job enqueue events are suppressed" do
# As happens under Active Job, which records the enqueue itself.
- it "passes through without recording the enqueue" do
+ def enqueue_suppressed(transaction)
+ transaction.suppress_job_enqueue_events { enqueue }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
- transaction.suppress_job_enqueue_events { enqueue }
+ enqueue_suppressed(transaction)
# The outer integration records the enqueue, so this one doesn't.
event_names = transaction.to_h["events"].map { |event| event["name"] }
expect(event_names).to_not include("enqueue.que")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ enqueue_suppressed(transaction)
+ Appsignal::Transaction.complete_current!
+
+ # No producer span for the suppressed enqueue...
+ expect(span_exporter.finished_spans.map(&:name)).to_not include("enqueue.que")
+ # ...but the trace context is still injected so the job links back.
+ expect(enqueued_tags).to include(a_string_starting_with("traceparent:"))
+ end
end
- # `bulk_enqueue` is Que 2 only. The whole batch records a single
- # `bulk_enqueue.que` event; the inner enqueues are pass-throughs.
+ # `bulk_enqueue` is Que 2 only. The whole batch shares one `job_options`, so
+ # it records a single producer event and the inner enqueues are pass-throughs.
describe "#bulk_enqueue", :if => DependencyHelper.que2_present? do
before do
# Que's bulk path constantizes the job class by name, so it needs a real
@@ -276,35 +499,90 @@ def bulk_enqueue(tags: ["user:42"])
end
end
- it "records one bulk_enqueue event for the whole batch" do
- transaction = http_request_transaction
- set_current_transaction(transaction)
+ context "with an active transaction" do
+ it "records one producer event for the batch in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
- bulk_enqueue
+ bulk_enqueue
- # One event for the whole batch, titled after the job -- the inner
- # enqueues don't add their own.
- bulk_events =
- transaction.to_h["events"].select { |e| e["name"] == "bulk_enqueue.que" }
- expect(bulk_events.size).to eq(1)
- expect(bulk_events.first["title"]).to eq("bulk enqueue MyQueJob jobs")
- event_names = transaction.to_h["events"].map { |event| event["name"] }
- expect(event_names).to_not include("enqueue.que")
- expect(enqueued_tags).to eq(["user:42"])
+ # One event for the whole batch, titled after the job -- the inner
+ # enqueues don't add their own.
+ bulk_events =
+ transaction.to_h["events"].select { |e| e["name"] == "bulk_enqueue.que" }
+ expect(bulk_events.size).to eq(1)
+ expect(bulk_events.first["title"]).to eq("bulk enqueue MyQueJob jobs")
+ event_names = transaction.to_h["events"].map { |event| event["name"] }
+ expect(event_names).to_not include("enqueue.que")
+ expect(enqueued_tags).to eq(["user:42"])
+ end
+
+ it "injects the batch's context once in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ bulk_enqueue
+ Appsignal::Transaction.complete_current!
+
+ producers = event_spans.select { |s| s.name == "bulk enqueue MyQueJob jobs" }
+ expect(producers.size).to eq(1)
+ producer = producers.first
+ expect(producer.attributes["appsignal.category"]).to eq("bulk_enqueue.que")
+ expect(producer.kind).to eq(:producer)
+ expect(producer.parent_span_id).to eq(root_span.span_id)
+
+ # Every job in the batch carries the one producer span's context.
+ expect(enqueued_tags).to include("user:42")
+ expect(enqueued_tags)
+ .to include("traceparent:00-#{producer.hex_trace_id}-#{producer.hex_span_id}-01")
+ end
+
+ it "skips propagation rather than break the enqueue when tags are full",
+ :collector_mode do
+ start_collector_agent
+ set_current_transaction(http_request_transaction)
+
+ full = %w[t1 t2 t3 t4 t5]
+ expect { bulk_enqueue(:tags => full) }.to_not raise_error
+ Appsignal::Transaction.complete_current!
+
+ expect(enqueued_tags).to eq(full)
+ end
end
context "when job enqueue events are suppressed" do
# As happens under Active Job, which records the enqueue itself.
- it "passes through without recording the enqueue" do
+ def bulk_enqueue_suppressed(transaction)
+ transaction.suppress_job_enqueue_events { bulk_enqueue }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
- transaction.suppress_job_enqueue_events { bulk_enqueue }
+ bulk_enqueue_suppressed(transaction)
# The outer integration records the enqueue, so this one doesn't.
event_names = transaction.to_h["events"].map { |event| event["name"] }
expect(event_names).to_not include("bulk_enqueue.que")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ bulk_enqueue_suppressed(transaction)
+ Appsignal::Transaction.complete_current!
+
+ # No producer span for the suppressed batch...
+ expect(span_exporter.finished_spans.map(&:name)).to_not include("bulk_enqueue.que")
+ # ...but the trace context is still injected so the jobs link back.
+ expect(enqueued_tags).to include(a_string_starting_with("traceparent:"))
+ end
end
end
end
diff --git a/spec/lib/appsignal/integrations/railtie_spec.rb b/spec/lib/appsignal/integrations/railtie_spec.rb
index dbff4aeea..652d60acb 100644
--- a/spec/lib/appsignal/integrations/railtie_spec.rb
+++ b/spec/lib/appsignal/integrations/railtie_spec.rb
@@ -254,25 +254,62 @@ def subscribe(subscriber)
if Rails.respond_to?(:error)
describe "Rails error reporter" do
- before { start_agent }
- around { |example| keep_transactions { example.run } }
-
- it "reports the error when the error is not handled (reraises the error)" do
- with_rails_error_reporter do
- expect do
- Rails.error.record { raise ExampleStandardError, "error message" }
- end.to raise_error(ExampleStandardError, "error message")
+ describe "reports the error when the error is not handled (reraises the error)" do
+ def perform
+ with_rails_error_reporter do
+ expect do
+ Rails.error.record { raise ExampleStandardError, "error message" }
+ end.to raise_error(ExampleStandardError, "error message")
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_error("ExampleStandardError", "error message")
end
- expect(last_transaction).to have_error("ExampleStandardError", "error message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
- it "reports the error when the error is handled (not reraised)" do
- with_rails_error_reporter do
- Rails.error.handle { raise ExampleStandardError, "error message" }
+ describe "reports the error when the error is handled (not reraised)" do
+ def perform
+ with_rails_error_reporter do
+ Rails.error.handle { raise ExampleStandardError, "error message" }
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_error("ExampleStandardError", "error message")
end
- expect(last_transaction).to have_error("ExampleStandardError", "error message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
context "Sidekiq internal errors" do
@@ -281,37 +318,92 @@ def subscribe(subscriber)
require "sidekiq/job_retry"
end
- it "ignores Sidekiq::JobRetry::Handled errors" do
- with_rails_error_reporter do
- Rails.error.handle { raise Sidekiq::JobRetry::Handled, "error message" }
+ describe "ignores Sidekiq::JobRetry::Handled errors" do
+ def perform
+ with_rails_error_reporter do
+ Rails.error.handle { raise Sidekiq::JobRetry::Handled, "error message" }
+ end
end
- expect(last_transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ end
end
- it "ignores Sidekiq::JobRetry::Skip errors" do
- with_rails_error_reporter do
- Rails.error.handle { raise Sidekiq::JobRetry::Skip, "error message" }
+ describe "ignores Sidekiq::JobRetry::Skip errors" do
+ def perform
+ with_rails_error_reporter do
+ Rails.error.handle { raise Sidekiq::JobRetry::Skip, "error message" }
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to_not have_error
end
- expect(last_transaction).to_not have_error
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ end
end
- it "doesn't crash when no Sidekiq error classes are found" do
- hide_const("Sidekiq::JobRetry")
- with_rails_error_reporter do
- Rails.error.handle { raise ExampleStandardError, "error message" }
+ describe "doesn't crash when no Sidekiq error classes are found" do
+ def perform
+ hide_const("Sidekiq::JobRetry")
+ with_rails_error_reporter do
+ Rails.error.handle { raise ExampleStandardError, "error message" }
+ end
end
- expect(last_transaction).to have_error("ExampleStandardError", "error message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_error("ExampleStandardError", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
context "when no transaction is active" do
- it "reports the error on a new transaction" do
- with_rails_error_reporter do
- expect do
+ describe "reports the error on a new transaction" do
+ def perform
+ with_rails_error_reporter do
Rails.error.handle { raise ExampleStandardError, "error message" }
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect do
+ perform
end.to change { created_transactions.count }.by(1)
transaction = last_transaction
@@ -319,158 +411,349 @@ def subscribe(subscriber)
expect(transaction).to_not have_action
expect(transaction).to have_error("ExampleStandardError", "error message")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect do
+ perform
+ end.to change { created_transactions.count }.by(1)
+
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
context "when a transaction is active" do
- it "reports the error on the transaction when a transaction is active" do
- current_transaction = http_request_transaction
- current_transaction.set_namespace "custom"
- current_transaction.set_action "CustomAction"
- current_transaction.add_tags(:duplicated_tag => "duplicated value")
-
- with_rails_error_reporter do
- with_current_transaction current_transaction do
- Rails.error.handle { raise ExampleStandardError, "error message" }
- expect do
- current_transaction.complete
- end.to_not(change { created_transactions.count })
-
- transaction = current_transaction
- expect(transaction).to have_namespace("custom")
- expect(transaction).to have_action("CustomAction")
- expect(transaction).to have_error("ExampleStandardError", "error message")
- expect(transaction).to include_tags(
- "reported_by" => "rails_error_reporter",
- "duplicated_tag" => "duplicated value",
- "severity" => "warning"
- )
+ describe "reports the error on the transaction when a transaction is active" do
+ def perform(current_transaction)
+ with_rails_error_reporter do
+ with_current_transaction current_transaction do
+ Rails.error.handle { raise ExampleStandardError, "error message" }
+ end
end
end
- end
- context "when the current transaction has an error" do
- it "reports the error on a new transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent
current_transaction = http_request_transaction
current_transaction.set_namespace "custom"
current_transaction.set_action "CustomAction"
current_transaction.add_tags(:duplicated_tag => "duplicated value")
- current_transaction.add_error(ExampleStandardError.new("error message"))
+ expect do
+ perform(current_transaction)
+ end.to_not(change { created_transactions.count })
+ current_transaction.complete
+
+ expect(current_transaction).to have_namespace("custom")
+ expect(current_transaction).to have_action("CustomAction")
+ expect(current_transaction).to have_error("ExampleStandardError", "error message")
+ expect(current_transaction).to include_tags(
+ "reported_by" => "rails_error_reporter",
+ "duplicated_tag" => "duplicated value",
+ "severity" => "warning"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ current_transaction = http_request_transaction
+ current_transaction.set_namespace "custom"
+ current_transaction.set_action "CustomAction"
+ current_transaction.add_tags(:duplicated_tag => "duplicated value")
+
+ expect do
+ perform(current_transaction)
+ end.to_not(change { created_transactions.count })
+ current_transaction.complete
+
+ expect(root_span.attributes["appsignal.namespace"]).to eq("custom")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("CustomAction")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.reported_by"])
+ .to eq("rails_error_reporter")
+ expect(root_span.attributes["appsignal.tag.duplicated_tag"])
+ .to eq("duplicated value")
+ expect(root_span.attributes["appsignal.tag.severity"]).to eq("warning")
+ end
+ end
+
+ context "when the current transaction has an error" do
+ describe "reports the error (new transaction in agent, span in collector)" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ current_transaction = http_request_transaction
+ current_transaction.set_namespace "custom"
+ current_transaction.set_action "CustomAction"
+ current_transaction.add_tags(:duplicated_tag => "duplicated value")
+ current_transaction.add_error(ExampleStandardError.new("error message"))
+
+ with_rails_error_reporter do
+ with_current_transaction current_transaction do
+ Rails.error.handle { raise ExampleStandardError, "other message" }
+ expect do
+ current_transaction.complete
+ end.to change { created_transactions.count }.by(1)
+
+ expect(current_transaction)
+ .to_not include_tags("reported_by" => "rails_error_reporter")
+
+ transaction = last_transaction
+ expect(transaction).to have_namespace("custom")
+ expect(transaction).to have_action("CustomAction")
+ expect(transaction).to have_error("ExampleStandardError", "other message")
+ expect(transaction).to include_tags(
+ "reported_by" => "rails_error_reporter",
+ "duplicated_tag" => "duplicated value",
+ "severity" => "warning"
+ )
+ end
+ end
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ current_transaction = http_request_transaction
+ current_transaction.set_namespace "custom"
+ current_transaction.set_action "CustomAction"
+ current_transaction.add_tags(:duplicated_tag => "duplicated value")
+ current_transaction.add_error(ExampleStandardError.new("error message"))
+
+ with_rails_error_reporter do
+ with_current_transaction current_transaction do
+ Rails.error.handle { raise ExampleStandardError, "other message" }
+ # In collector mode both errors collapse onto one span — no
+ # duplicate transaction is created.
+ expect do
+ current_transaction.complete
+ end.to_not(change { created_transactions.count })
+
+ expect(root_span.attributes["appsignal.namespace"]).to eq("custom")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("CustomAction")
+ # Both errors collapse onto one root span as two exception events.
+ root_spans = span_exporter.finished_spans.select do |s|
+ [:server, :consumer].include?(s.kind)
+ end
+ expect(root_spans.size).to eq(1)
+ events = root_spans.first.events.select { |e| e.name == "exception" }
+ expect(events.map { |e| e.attributes["exception.message"] })
+ .to contain_exactly("error message", "other message")
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.reported_by"])
+ .to eq("rails_error_reporter")
+ expect(root_span.attributes["appsignal.tag.duplicated_tag"])
+ .to eq("duplicated value")
+ expect(root_span.attributes["appsignal.tag.severity"]).to eq("warning")
+ end
+ end
+ end
+ end
+
+ describe "reports the error on a new transaction with the given context (agent) / merges context onto the span (collector)" do # rubocop:disable Layout/LineLength
+ it "in agent mode", :agent_mode do
+ start_agent
+ current_transaction = http_request_transaction
+ current_transaction.set_namespace "custom"
+ current_transaction.set_action "CustomAction"
+ current_transaction.add_tags(:duplicated_tag => "duplicated value")
+ current_transaction.add_custom_data(:original => "custom value")
+ current_transaction.add_error(ExampleStandardError.new("error message"))
+
+ with_rails_error_reporter do
+ with_current_transaction current_transaction do
+ given_context = {
+ :appsignal => {
+ :namespace => "context",
+ :action => "ContextAction",
+ :custom_data => { :context => "context data" }
+
+ }
+ }
+ Rails.error.handle(:context => given_context) do
+ raise ExampleStandardError, "other message"
+ end
+ expect do
+ current_transaction.complete
+ end.to change { created_transactions.count }.by(1)
+
+ transaction = last_transaction
+ expect(transaction).to have_namespace("context")
+ expect(transaction).to have_action("ContextAction")
+ expect(transaction).to have_error("ExampleStandardError", "other message")
+ expect(transaction).to include_tags(
+ "reported_by" => "rails_error_reporter",
+ "duplicated_tag" => "duplicated value",
+ "severity" => "warning"
+ )
+ expect(transaction).to include_custom_data(
+ "original" => "custom value",
+ "context" => "context data"
+ )
+ end
+ end
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ current_transaction = http_request_transaction
+ current_transaction.set_namespace "custom"
+ current_transaction.set_action "CustomAction"
+ current_transaction.add_tags(:duplicated_tag => "duplicated value")
+ current_transaction.add_custom_data(:original => "custom value")
+ current_transaction.add_error(ExampleStandardError.new("error message"))
+
+ with_rails_error_reporter do
+ with_current_transaction current_transaction do
+ given_context = {
+ :appsignal => {
+ :namespace => "context",
+ :action => "ContextAction",
+ :custom_data => { :context => "context data" }
+ }
+ }
+ Rails.error.handle(:context => given_context) do
+ raise ExampleStandardError, "other message"
+ end
+ # In collector mode both errors collapse onto one span — no
+ # duplicate transaction is created. The reporter's block
+ # overrides namespace/action on the existing span.
+ expect do
+ current_transaction.complete
+ end.to_not(change { created_transactions.count })
+
+ expect(root_span.attributes["appsignal.namespace"]).to eq("context")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("ContextAction")
+ # Both errors collapse onto one root span as two exception events.
+ root_spans = span_exporter.finished_spans.select do |s|
+ [:server, :consumer].include?(s.kind)
+ end
+ expect(root_spans.size).to eq(1)
+ events = root_spans.first.events.select { |e| e.name == "exception" }
+ expect(events.map { |e| e.attributes["exception.message"] })
+ .to contain_exactly("error message", "other message")
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.reported_by"])
+ .to eq("rails_error_reporter")
+ expect(root_span.attributes["appsignal.tag.duplicated_tag"])
+ .to eq("duplicated value")
+ expect(root_span.attributes["appsignal.tag.severity"]).to eq("warning")
+ custom_data = JSON.parse(root_span.attributes["appsignal.custom_data"])
+ expect(custom_data).to include(
+ "original" => "custom value",
+ "context" => "context data"
+ )
+ end
+ end
+ end
+ end
+ end
+
+ describe "overwrites duplicate tags with tags from context" do
+ def perform(current_transaction)
with_rails_error_reporter do
with_current_transaction current_transaction do
- Rails.error.handle { raise ExampleStandardError, "other message" }
- expect do
- current_transaction.complete
- end.to change { created_transactions.count }.by(1)
-
- expect(current_transaction)
- .to_not include_tags("reported_by" => "rails_error_reporter")
-
- transaction = last_transaction
- expect(transaction).to have_namespace("custom")
- expect(transaction).to have_action("CustomAction")
- expect(transaction).to have_error("ExampleStandardError", "other message")
- expect(transaction).to include_tags(
- "reported_by" => "rails_error_reporter",
- "duplicated_tag" => "duplicated value",
- "severity" => "warning"
- )
+ given_context = { :tag1 => "value1", :tag2 => "value2" }
+ Rails.error.handle(:context => given_context) { raise ExampleStandardError }
+ current_transaction.complete
end
end
end
- it "reports the error on a new transaction with the given context" do
+ it "in agent mode", :agent_mode do
+ start_agent
current_transaction = http_request_transaction
- current_transaction.set_namespace "custom"
- current_transaction.set_action "CustomAction"
- current_transaction.add_tags(:duplicated_tag => "duplicated value")
- current_transaction.add_custom_data(:original => "custom value")
- current_transaction.add_error(ExampleStandardError.new("error message"))
+ current_transaction.add_tags(:tag1 => "duplicated value")
+
+ perform(current_transaction)
+ expect(current_transaction).to include_tags(
+ "reported_by" => "rails_error_reporter",
+ "tag1" => "value1",
+ "tag2" => "value2",
+ "severity" => "warning"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ current_transaction = http_request_transaction
+ current_transaction.add_tags(:tag1 => "duplicated value")
+
+ perform(current_transaction)
+
+ expect(root_span.attributes["appsignal.tag.reported_by"])
+ .to eq("rails_error_reporter")
+ expect(root_span.attributes["appsignal.tag.tag1"]).to eq("value1")
+ expect(root_span.attributes["appsignal.tag.tag2"]).to eq("value2")
+ expect(root_span.attributes["appsignal.tag.severity"]).to eq("warning")
+ end
+ end
+
+ describe "sets namespace, action and custom data with values from context" do
+ def perform(current_transaction)
with_rails_error_reporter do
with_current_transaction current_transaction do
given_context = {
:appsignal => {
:namespace => "context",
:action => "ContextAction",
- :custom_data => { :context => "context data" }
-
+ :custom_data => { :data => "context data" }
}
}
- Rails.error.handle(:context => given_context) do
- raise ExampleStandardError, "other message"
- end
- expect do
- current_transaction.complete
- end.to change { created_transactions.count }.by(1)
-
- transaction = last_transaction
- expect(transaction).to have_namespace("context")
- expect(transaction).to have_action("ContextAction")
- expect(transaction).to have_error("ExampleStandardError", "other message")
- expect(transaction).to include_tags(
- "reported_by" => "rails_error_reporter",
- "duplicated_tag" => "duplicated value",
- "severity" => "warning"
- )
- expect(transaction).to include_custom_data(
- "original" => "custom value",
- "context" => "context data"
- )
+ Rails.error.handle(:context => given_context) { raise ExampleStandardError }
+ current_transaction.complete
end
end
end
- end
- it "overwrites duplicate tags with tags from context" do
- current_transaction = http_request_transaction
- current_transaction.add_tags(:tag1 => "duplicated value")
+ it "in agent mode", :agent_mode do
+ start_agent
+ current_transaction = http_request_transaction
+ current_transaction.set_namespace "custom"
+ current_transaction.set_action "CustomAction"
- with_rails_error_reporter do
- with_current_transaction current_transaction do
- given_context = { :tag1 => "value1", :tag2 => "value2" }
- Rails.error.handle(:context => given_context) { raise ExampleStandardError }
- current_transaction.complete
-
- expect(current_transaction).to include_tags(
- "reported_by" => "rails_error_reporter",
- "tag1" => "value1",
- "tag2" => "value2",
- "severity" => "warning"
- )
- end
+ perform(current_transaction)
+
+ expect(current_transaction).to have_namespace("context")
+ expect(current_transaction).to have_action("ContextAction")
+ expect(current_transaction).to include_custom_data("data" => "context data")
end
- end
- it "sets namespace, action and custom data with values from context" do
- current_transaction = http_request_transaction
- current_transaction.set_namespace "custom"
- current_transaction.set_action "CustomAction"
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ current_transaction = http_request_transaction
+ current_transaction.set_namespace "custom"
+ current_transaction.set_action "CustomAction"
- with_rails_error_reporter do
- with_current_transaction current_transaction do
- given_context = {
- :appsignal => {
- :namespace => "context",
- :action => "ContextAction",
- :custom_data => { :data => "context data" }
- }
- }
- Rails.error.handle(:context => given_context) { raise ExampleStandardError }
- current_transaction.complete
+ perform(current_transaction)
- expect(current_transaction).to have_namespace("context")
- expect(current_transaction).to have_action("ContextAction")
- expect(current_transaction).to include_custom_data("data" => "context data")
- end
+ expect(root_span.attributes["appsignal.namespace"]).to eq("context")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("ContextAction")
+ expect(JSON.parse(root_span.attributes["appsignal.custom_data"]))
+ .to include("data" => "context data")
end
end
end
if DependencyHelper.rails7_1_present?
- it "sets the namespace to 'runner' if the source is the Rails runner" do
- expect do
+ describe "sets the namespace to 'runner' if the source is the Rails runner" do
+ def perform
with_rails_error_reporter do
expect do
Rails.error.record(:source => "application.runner.railties") do
@@ -478,35 +761,82 @@ def subscribe(subscriber)
end
end.to raise_error(ExampleStandardError, "error message")
end
- end.to change { created_transactions.count }.by(1)
+ end
- transaction = last_transaction
- expect(transaction).to have_namespace("runner")
- expect(transaction).to_not have_action
- expect(transaction).to have_error("ExampleStandardError", "error message")
- expect(transaction).to include_tags(
- "reported_by" => "rails_error_reporter",
- "source" => "application.runner.railties"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect do
+ perform
+ end.to change { created_transactions.count }.by(1)
+
+ transaction = last_transaction
+ expect(transaction).to have_namespace("runner")
+ expect(transaction).to_not have_action
+ expect(transaction).to have_error("ExampleStandardError", "error message")
+ expect(transaction).to include_tags(
+ "reported_by" => "rails_error_reporter",
+ "source" => "application.runner.railties"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect do
+ perform
+ end.to change { created_transactions.count }.by(1)
+
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("runner")
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.reported_by"])
+ .to eq("rails_error_reporter")
+ expect(root_span.attributes["appsignal.tag.source"])
+ .to eq("application.runner.railties")
+ end
end
end
- it "sets the error context as tags" do
- given_context = {
- :appsignal => { :something => "not used" }, # Not set as tag
- :tag1 => "value1",
- :tag2 => "value2"
- }
- with_rails_error_reporter do
- Rails.error.handle(:context => given_context) { raise ExampleStandardError }
+ describe "sets the error context as tags" do
+ def perform
+ given_context = {
+ :appsignal => { :something => "not used" }, # Not set as tag
+ :tag1 => "value1",
+ :tag2 => "value2"
+ }
+ with_rails_error_reporter do
+ Rails.error.handle(:context => given_context) { raise ExampleStandardError }
+ end
end
- expect(last_transaction).to include_tags(
- "reported_by" => "rails_error_reporter",
- "tag1" => "value1",
- "tag2" => "value2",
- "severity" => "warning"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to include_tags(
+ "reported_by" => "rails_error_reporter",
+ "tag1" => "value1",
+ "tag2" => "value2",
+ "severity" => "warning"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.tag.reported_by"])
+ .to eq("rails_error_reporter")
+ expect(root_span.attributes["appsignal.tag.tag1"]).to eq("value1")
+ expect(root_span.attributes["appsignal.tag.tag2"]).to eq("value2")
+ expect(root_span.attributes["appsignal.tag.severity"]).to eq("warning")
+ end
end
end
end
diff --git a/spec/lib/appsignal/integrations/resque_spec.rb b/spec/lib/appsignal/integrations/resque_spec.rb
index 16208e081..cf9c574f7 100644
--- a/spec/lib/appsignal/integrations/resque_spec.rb
+++ b/spec/lib/appsignal/integrations/resque_spec.rb
@@ -2,18 +2,12 @@
if DependencyHelper.resque_present?
describe Appsignal::Integrations::ResqueIntegration do
- def perform_rescue_job(klass, options = {})
- payload = { "class" => klass.to_s }.merge(options)
- job = ::Resque::Job.new(queue, payload)
- keep_transactions { job.perform }
- end
-
let(:queue) { "default" }
let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
let(:options) { {} }
- before do
- start_agent(:options => options)
+ let(:start_agent_args) { { :options => options } }
+ before do
stub_const("ResqueTestJob", Class.new do
def self.perform(*_args)
end
@@ -24,32 +18,102 @@ def self.perform
raise "resque job error"
end
end)
+ end
- expect(Appsignal).to receive(:stop) # Resque calls stop after every job
+ def perform_rescue_job(klass, job_options = {})
+ payload = { "class" => klass.to_s }.merge(job_options)
+ job = ::Resque::Job.new(queue, payload)
+ keep_transactions { job.perform }
end
- around do |example|
- keep_transactions { example.run }
+
+ describe "tracks a transaction on perform" do
+ def perform
+ perform_rescue_job(ResqueTestJob)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect(Appsignal).to receive(:stop)
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_namespace(namespace)
+ expect(transaction).to have_action("ResqueTestJob#perform")
+ expect(transaction).to_not have_error
+ expect(transaction).to_not include_metadata
+ expect(transaction).to_not include_breadcrumbs
+ expect(transaction).to include_tags("queue" => queue)
+ expect(transaction).to include_event("name" => "perform.resque")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal).to receive(:stop)
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("ResqueTestJob#perform")
+ expect(exception_events).to be_empty
+ expect(root_span.attributes).to_not have_key("appsignal.tag.metadata_key")
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ span = event_spans.find { |s| s.name == "perform.resque" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ end
end
- it "tracks a transaction on perform" do
- perform_rescue_job(ResqueTestJob)
-
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_namespace(namespace)
- expect(transaction).to have_action("ResqueTestJob#perform")
- expect(transaction).to_not have_error
- expect(transaction).to_not include_metadata
- expect(transaction).to_not include_breadcrumbs
- expect(transaction).to include_tags("queue" => queue)
- expect(transaction).to include_event("name" => "perform.resque")
+ describe "with incoming trace context" do
+ let(:trace_id_hex) { "0af7651916cd43dd8448eb211c80319c" }
+ let(:span_id_hex) { "b7ad6b7169203331" }
+ let(:traceparent) { "00-#{trace_id_hex}-#{span_id_hex}-01" }
+
+ def perform
+ perform_rescue_job(ResqueTestJob, "traceparent" => traceparent)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect(Appsignal).to receive(:stop)
+ perform
+
+ # The trace header doesn't leak into the transaction as metadata or tags.
+ transaction = last_transaction
+ expect(transaction).to_not include_metadata
+ expect(transaction).to include_tags("queue" => queue)
+ expect(transaction).to_not include_tags("traceparent" => traceparent)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal).to receive(:stop)
+ perform
+
+ # The job runs as its own trace, linked back to the span that enqueued it.
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.hex_trace_id).to_not eq(trace_id_hex)
+ expect(root_span.links.size).to eq(1)
+ link_context = root_span.links.first.span_context
+ expect(link_context.hex_trace_id).to eq(trace_id_hex)
+ expect(link_context.hex_span_id).to eq(span_id_hex)
+
+ # The trace header doesn't leak into the trace as a tag.
+ expect(root_span.attributes).to_not have_key("appsignal.tag.traceparent")
+ end
end
- context "with error" do
- it "tracks the error on the transaction" do
+ describe "tracks the error on the transaction" do
+ def perform
expect do
perform_rescue_job(ResqueErrorTestJob)
end.to raise_error(RuntimeError, "resque job error")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect(Appsignal).to receive(:stop)
+ perform
transaction = last_transaction
expect(transaction).to have_id
@@ -61,12 +125,33 @@ def self.perform
expect(transaction).to include_tags("queue" => queue)
expect(transaction).to include_event("name" => "perform.resque")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal).to receive(:stop)
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("ResqueErrorTestJob#perform")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("RuntimeError")
+ expect(event.attributes["exception.message"]).to eq("resque job error")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ span = event_spans.find { |s| s.name == "perform.resque" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ end
end
- context "with arguments" do
+ describe "filters out configured arguments" do
let(:options) { { :filter_parameters => ["foo"] } }
- it "filters out configured arguments" do
+ def perform
perform_rescue_job(
ResqueTestJob,
"args" => [
@@ -78,6 +163,12 @@ def self.perform
}
]
)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect(Appsignal).to receive(:stop)
+ perform
transaction = last_transaction
expect(transaction).to have_id
@@ -99,9 +190,145 @@ def self.perform
]
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal).to receive(:stop)
+ perform
+
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("ResqueTestJob#perform")
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ expect(event_spans.map(&:name)).to include("perform.resque")
+ params = JSON.parse(root_span.attributes["appsignal.function.parameters"])
+ expect(params).to eq(
+ [
+ "foo",
+ {
+ "foo" => "[FILTERED]",
+ "bar" => "Bar",
+ "baz" => { "1" => "foo" }
+ }
+ ]
+ )
+ end
end
- context "with active job" do
+ describe Appsignal::Integrations::ResquePushIntegration do
+ # A stand-in for the `Resque` singleton with the integration prepended.
+ # Its `push` records the pushed item so we can inspect what was written.
+ let(:resque) do
+ Class.new do
+ attr_reader :pushed
+
+ def push(queue, item)
+ @pushed = [queue, item]
+ :pushed
+ end
+
+ prepend Appsignal::Integrations::ResquePushIntegration
+ end.new
+ end
+ let(:item) { { "class" => "ResqueTestJob", "args" => [] } }
+
+ def enqueue
+ resque.push("default", item)
+ end
+
+ context "with an active transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ expect(enqueue).to eq(:pushed)
+
+ # Records an enqueue event on the transaction, titled after the job;
+ # no wire context in agent mode.
+ event = transaction.to_h["events"].find { |e| e["name"] == "enqueue.resque" }
+ expect(event).to_not be_nil
+ expect(event["title"]).to eq("enqueue ResqueTestJob job")
+ expect(item).to_not have_key("traceparent")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ expect(enqueue).to eq(:pushed)
+ Appsignal::Transaction.complete_current!
+
+ # The enqueue is a producer event span under the active transaction,
+ # named after the job being enqueued.
+ producer = event_spans.find { |s| s.name == "enqueue ResqueTestJob job" }
+ expect(producer.attributes["appsignal.category"]).to eq("enqueue.resque")
+ expect(producer.kind).to eq(:producer)
+ expect(producer.parent_span_id).to eq(root_span.span_id)
+
+ # The job carries the producer span's trace context, so the job that
+ # performs can link back to it.
+ expect(item["traceparent"])
+ .to eq("00-#{producer.hex_trace_id}-#{producer.hex_span_id}-01")
+ end
+ end
+
+ context "without an active transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ # A transparent pass-through: the job hash is untouched.
+ expect(enqueue).to eq(:pushed)
+ expect(item).to_not have_key("traceparent")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ # No transaction to attach the event to, so nothing is emitted and the
+ # job hash is untouched.
+ expect(enqueue).to eq(:pushed)
+ expect(span_exporter.finished_spans.map(&:name)).to_not include("enqueue.resque")
+ expect(item).to_not have_key("traceparent")
+ end
+ end
+
+ context "when job enqueue events are suppressed" do
+ # As happens under Active Job, which records the enqueue itself.
+ def enqueue_suppressed(transaction)
+ transaction.suppress_job_enqueue_events { enqueue }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ expect(enqueue_suppressed(transaction)).to eq(:pushed)
+
+ # The outer integration records the enqueue, so this one doesn't.
+ event_names = transaction.to_h["events"].map { |event| event["name"] }
+ expect(event_names).to_not include("enqueue.resque")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ expect(enqueue_suppressed(transaction)).to eq(:pushed)
+ Appsignal::Transaction.complete_current!
+
+ # No producer span for the suppressed enqueue...
+ expect(span_exporter.finished_spans.map(&:name)).to_not include("enqueue.resque")
+ # ...but the trace context is still injected so the job links back.
+ expect(item).to have_key("traceparent")
+ end
+ end
+ end
+
+ describe "does not set arguments for ActiveJob" do
before do
stub_const("ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper", Class.new do
class << self
@@ -114,7 +341,7 @@ def perform(job_data)
end)
end
- it "does not set arguments but lets the ActiveJob integration handle it" do
+ def perform
perform_rescue_job(
ResqueTestJob,
"class" => "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper",
@@ -125,6 +352,12 @@ def perform(job_data)
}
]
)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect(Appsignal).to receive(:stop)
+ perform
transaction = last_transaction
expect(transaction).to have_id
@@ -137,67 +370,19 @@ def perform(job_data)
expect(transaction).to include_event("name" => "perform.resque")
expect(transaction).to_not include_params
end
- end
- end
-
- describe Appsignal::Integrations::ResquePushIntegration do
- # A stand-in for the `Resque` singleton with the integration prepended.
- # Its `push` records the pushed item so we can inspect what was written.
- let(:resque) do
- Class.new do
- attr_reader :pushed
-
- def push(queue, item)
- @pushed = [queue, item]
- :pushed
- end
-
- prepend Appsignal::Integrations::ResquePushIntegration
- end.new
- end
- let(:item) { { "class" => "ResqueTestJob", "args" => [] } }
-
- before { start_agent }
- around { |example| keep_transactions { example.run } }
-
- def enqueue
- resque.push("default", item)
- end
-
- context "with an active transaction" do
- it "records an enqueue event and leaves the job untouched" do
- transaction = http_request_transaction
- set_current_transaction(transaction)
-
- expect(enqueue).to eq(:pushed)
-
- # Records an enqueue event on the transaction, titled after the job.
- event = transaction.to_h["events"].find { |e| e["name"] == "enqueue.resque" }
- expect(event).to_not be_nil
- expect(event["title"]).to eq("enqueue ResqueTestJob job")
- expect(item).to eq("class" => "ResqueTestJob", "args" => [])
- end
- end
-
- context "without an active transaction" do
- it "is a transparent pass-through" do
- expect(enqueue).to eq(:pushed)
- expect(item).to eq("class" => "ResqueTestJob", "args" => [])
- end
- end
-
- context "when job enqueue events are suppressed" do
- # As happens under Active Job, which records the enqueue itself.
- it "passes through without recording the enqueue" do
- transaction = http_request_transaction
- set_current_transaction(transaction)
- result = transaction.suppress_job_enqueue_events { enqueue }
- expect(result).to eq(:pushed)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal).to receive(:stop)
+ perform
- # The outer integration records the enqueue, so this one doesn't.
- event_names = transaction.to_h["events"].map { |event| event["name"] }
- expect(event_names).to_not include("enqueue.resque")
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ResqueTestJobByActiveJob#perform")
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ expect(event_spans.map(&:name)).to include("perform.resque")
+ expect(root_span.attributes).to_not have_key("appsignal.function.parameters")
end
end
end
diff --git a/spec/lib/appsignal/integrations/shoryuken_client_spec.rb b/spec/lib/appsignal/integrations/shoryuken_client_spec.rb
index e425318c9..185aa865b 100644
--- a/spec/lib/appsignal/integrations/shoryuken_client_spec.rb
+++ b/spec/lib/appsignal/integrations/shoryuken_client_spec.rb
@@ -3,16 +3,12 @@
require "appsignal/integrations/shoryuken"
# Integration test against the real Shoryuken gem and a stubbed AWS SQS client
- # (the shoryuken_spec.rb suite drives the middleware with doubles). Verifies,
- # end to end, that the hook registers the client middleware on the real send
- # path and that an enqueue records an `enqueue.shoryuken` event -- the
- # registration the doubled suite can't prove.
+ # (the shoryuken_spec.rb suite drives the middleware with doubles). Verifies, end
+ # to end, that the hook registers the client middleware on the real send path and
+ # that it writes trace context onto a live outgoing message -- the registration
+ # the doubled suite can't prove.
describe "Shoryuken client integration" do
- before do
- start_agent
- Appsignal::Hooks::ShoryukenHook.new.install
- end
- around { |example| keep_transactions { example.run } }
+ before { Appsignal::Hooks::ShoryukenHook.new.install }
after do
::Shoryuken.client_middleware.remove(Appsignal::Integrations::ShoryukenClientMiddleware)
@@ -29,14 +25,50 @@
end
let(:queue) { Shoryuken::Queue.new(sqs_client, "test-queue") }
- it "records an enqueue event through the real send path" do
+ # Sends a real message through Shoryuken's send path and returns the params
+ # the SQS client was called with.
+ def send_message
+ sent = nil
+ allow(sqs_client).to receive(:send_message).and_wrap_original do |original, params|
+ sent = params
+ original.call(params)
+ end
+ queue.send_message(:message_body => "foo")
+ sent
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
- queue.send_message(:message_body => "foo")
+ sent = send_message
event_names = transaction.to_h["events"].map { |event| event["name"] }
expect(event_names).to include("enqueue.shoryuken")
+ expect(sent).to_not have_key(:message_attributes)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ sent = send_message
+ Appsignal::Transaction.complete_current!
+
+ # A raw `send_message` has no worker class, so the event names the queue.
+ producer = event_spans.find { |s| s.name == "enqueue on test-queue" }
+ expect(producer.attributes["appsignal.category"]).to eq("enqueue.shoryuken")
+ expect(producer.kind).to eq(:producer)
+
+ # The middleware the hook registered injected the producer span's trace
+ # context onto the real outgoing message, wire-equivalent to OpenTelemetry's
+ # aws-sdk instrumentation.
+ expect(sent[:message_attributes]["traceparent"]).to eq(
+ :string_value => "00-#{producer.hex_trace_id}-#{producer.hex_span_id}-01",
+ :data_type => "String"
+ )
end
end
end
diff --git a/spec/lib/appsignal/integrations/shoryuken_spec.rb b/spec/lib/appsignal/integrations/shoryuken_spec.rb
index bcf1ad9d1..2774a8906 100644
--- a/spec/lib/appsignal/integrations/shoryuken_spec.rb
+++ b/spec/lib/appsignal/integrations/shoryuken_spec.rb
@@ -7,11 +7,15 @@ class DemoShoryukenWorker
let(:time) { "2010-01-01 10:01:00UTC" }
let(:worker_instance) { DemoShoryukenWorker.new }
let(:queue) { "some-funky-queue-name" }
- let(:sqs_msg) { double(:message_id => "msg1", :attributes => {}) }
+ let(:sqs_msg) { double(:message_id => "msg1", :attributes => {}, :message_attributes => {}) }
let(:body) { {} }
let(:options) { {} }
- before { start_agent(:options => options) }
- around { |example| keep_transactions { example.run } }
+
+ # Pass the example's options through to the mode contexts' `start_agent`. In
+ # collector mode `start_collector_agent` merges these on top of the
+ # `collector_endpoint`, so options like `:filter_parameters` apply in both
+ # modes.
+ let(:start_agent_args) { { :options => options } }
def perform_shoryuken_job(&block)
block ||= lambda {}
@@ -27,46 +31,98 @@ def perform_shoryuken_job(&block)
end
context "with a performance call" do
- let(:sent_timestamp) { Time.parse("1976-11-18 0:00:00UTC").to_i * 1000 }
+ let(:sent_timestamp) { Time.parse("2024-11-18 0:00:00UTC").to_i * 1000 }
let(:sqs_msg) do
- double(:message_id => "msg1", :attributes => { "SentTimestamp" => sent_timestamp })
+ double(
+ :message_id => "msg1",
+ :attributes => { "SentTimestamp" => sent_timestamp },
+ :message_attributes => {}
+ )
end
context "with complex argument" do
let(:body) { { :foo => "Foo", :bar => "Bar" } }
- it "wraps the job in a transaction with the correct params" do
- expect { perform_shoryuken_job }.to change { created_transactions.length }.by(1)
+ describe "wraps the job in a transaction" do
+ def perform
+ perform_shoryuken_job
+ end
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
- expect(transaction).to have_action("DemoShoryukenWorker#perform")
- expect(transaction).to_not have_error
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "perform_job.shoryuken",
- "title" => ""
- )
- expect(transaction).to include_params("foo" => "Foo", "bar" => "Bar")
- expect(transaction).to include_tags(
- "message_id" => "msg1",
- "queue" => queue,
- "SentTimestamp" => sent_timestamp
- )
- expect(transaction).to have_queue_start(sent_timestamp)
- expect(transaction).to be_completed
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
+ expect(transaction).to have_action("DemoShoryukenWorker#perform")
+ expect(transaction).to_not have_error
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "perform_job.shoryuken",
+ "title" => ""
+ )
+ expect(transaction).to include_params("foo" => "Foo", "bar" => "Bar")
+ expect(transaction).to include_tags(
+ "message_id" => "msg1",
+ "queue" => queue,
+ "SentTimestamp" => sent_timestamp
+ )
+ expect(transaction).to have_queue_start(sent_timestamp)
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("background")
+ expect(root_span.name).to eq("DemoShoryukenWorker#perform")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("DemoShoryukenWorker#perform")
+ expect(exception_events).to be_empty
+ span = event_spans.find { |s| s.name == "perform_job.shoryuken" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes).not_to have_key("appsignal.body")
+ expect(span.attributes["appsignal.category"]).to eq("perform_job.shoryuken")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq("foo" => "Foo", "bar" => "Bar")
+ expect(root_span.attributes["appsignal.tag.message_id"]).to eq("msg1")
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq(queue)
+ expect(root_span.attributes["appsignal.tag.SentTimestamp"]).to eq(sent_timestamp)
+ queue_event = Array(root_span.events).find { |e| e.name == "appsignal.queue_start" }
+ expect(queue_event.attributes["appsignal.queue_start"]).to eq(sent_timestamp)
+ expect(last_transaction).to be_completed
+ end
end
context "with parameter filtering" do
let(:options) { { :filter_parameters => ["foo"] } }
- it "filters selected arguments" do
- perform_shoryuken_job
+ describe "filters selected arguments" do
+ def perform
+ perform_shoryuken_job
+ end
- expect(last_transaction).to include_params("foo" => "[FILTERED]", "bar" => "Bar")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_params("foo" => "[FILTERED]", "bar" => "Bar")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq("foo" => "[FILTERED]", "bar" => "Bar")
+ end
end
end
end
@@ -74,38 +130,139 @@ def perform_shoryuken_job(&block)
context "with a string as an argument" do
let(:body) { "foo bar" }
- it "handles string arguments" do
- perform_shoryuken_job
+ describe "handles string arguments" do
+ def perform
+ perform_shoryuken_job
+ end
- expect(last_transaction).to include_params("params" => body)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_params("params" => body)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq("params" => body)
+ end
end
end
context "with primitive type as argument" do
let(:body) { 1 }
- it "handles primitive types as arguments" do
+ describe "handles primitive types as arguments" do
+ def perform
+ perform_shoryuken_job
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_params("params" => body)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq("params" => body)
+ end
+ end
+ end
+ end
+
+ context "with incoming trace context" do
+ let(:trace_id_hex) { "0af7651916cd43dd8448eb211c80319c" }
+ let(:span_id_hex) { "b7ad6b7169203331" }
+ let(:traceparent) { "00-#{trace_id_hex}-#{span_id_hex}-01" }
+ let(:sqs_msg) do
+ double(
+ :message_id => "msg1",
+ :attributes => {},
+ :message_attributes => {
+ "traceparent" => { :string_value => traceparent, :data_type => "String" }
+ }
+ )
+ end
+
+ describe "links the transaction back to the enqueuer" do
+ def perform
perform_shoryuken_job
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
- expect(last_transaction).to include_params("params" => body)
+ # The trace header doesn't leak into the transaction as a tag.
+ expect(last_transaction).to_not include_tags("traceparent" => traceparent)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ # The job runs as its own trace, linked back to the span that enqueued it.
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.hex_trace_id).to_not eq(trace_id_hex)
+ expect(root_span.links.size).to eq(1)
+ link_context = root_span.links.first.span_context
+ expect(link_context.hex_trace_id).to eq(trace_id_hex)
+ expect(link_context.hex_span_id).to eq(span_id_hex)
+
+ # The trace header doesn't leak into the trace as a tag.
+ expect(root_span.attributes).to_not have_key("appsignal.tag.traceparent")
end
end
end
context "with exception" do
- it "sets the exception on the transaction" do
- expect do
+ describe "sets the exception on the transaction" do
+ def perform
+ perform_shoryuken_job { raise ExampleException, "error message" }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
expect do
- perform_shoryuken_job { raise ExampleException, "error message" }
- end.to raise_error(ExampleException)
- end.to change { created_transactions.length }.by(1)
+ expect { perform }.to raise_error(ExampleException)
+ end.to change { created_transactions.length }.by(1)
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_action("DemoShoryukenWorker#perform")
- expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
- expect(transaction).to have_error("ExampleException", "error message")
- expect(transaction).to be_completed
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_action("DemoShoryukenWorker#perform")
+ expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
+ expect(transaction).to have_error("ExampleException", "error message")
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect do
+ expect { perform }.to raise_error(ExampleException)
+ end.to change { created_transactions.length }.by(1)
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("DemoShoryukenWorker#perform")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("background")
+
+ error_event = exception_events
+ .find { |e| e.attributes["exception.type"] == "ExampleException" }
+ expect(error_event).not_to be_nil
+ expect(error_event.attributes["exception.message"]).to eq("error message")
+ expect(error_event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(error_event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(last_transaction).to be_completed
+ end
end
end
@@ -115,7 +272,7 @@ def perform_shoryuken_job(&block)
double(
:message_id => "msg2",
:attributes => {
- "SentTimestamp" => (Time.parse("1976-11-18 01:00:00UTC").to_i * 1000).to_s
+ "SentTimestamp" => (Time.parse("2024-11-18 01:00:00UTC").to_i * 1000).to_s
}
),
double(
@@ -130,44 +287,80 @@ def perform_shoryuken_job(&block)
{ :id => "123", :foo => "Foo", :bar => "Bar" }
]
end
- let(:sent_timestamp) { Time.parse("1976-11-18 01:00:00UTC").to_i * 1000 }
+ let(:sent_timestamp) { Time.parse("2024-11-18 01:00:00UTC").to_i * 1000 }
- it "creates a transaction for the batch" do
- expect do
+ describe "creates a transaction for the batch" do
+ def perform
perform_shoryuken_job {} # rubocop:disable Lint/EmptyBlock
- end.to change { created_transactions.length }.by(1)
-
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_action("DemoShoryukenWorker#perform")
- expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
- expect(transaction).to_not have_error
- expect(transaction).to include_event(
- "body" => "",
- "body_format" => Appsignal::EventFormatter::DEFAULT,
- "count" => 1,
- "name" => "perform_job.shoryuken",
- "title" => ""
- )
- expect(transaction).to include_params(
- "msg2" => "foo bar",
- "msg1" => { "id" => "123", "foo" => "Foo", "bar" => "Bar" }
- )
- expect(transaction).to include_tags(
- "batch" => true,
- "queue" => "some-funky-queue-name",
- "SentTimestamp" => sent_timestamp.to_s # Earliest/oldest timestamp from messages
- )
- # Queue time based on earliest/oldest timestamp from messages
- expect(transaction).to have_queue_start(sent_timestamp)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_action("DemoShoryukenWorker#perform")
+ expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
+ expect(transaction).to_not have_error
+ expect(transaction).to include_event(
+ "body" => "",
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
+ "count" => 1,
+ "name" => "perform_job.shoryuken",
+ "title" => ""
+ )
+ expect(transaction).to include_params(
+ "msg2" => "foo bar",
+ "msg1" => { "id" => "123", "foo" => "Foo", "bar" => "Bar" }
+ )
+ expect(transaction).to include_tags(
+ "batch" => true,
+ "queue" => "some-funky-queue-name",
+ "SentTimestamp" => sent_timestamp.to_s # Earliest/oldest timestamp from messages
+ )
+ # Queue time based on earliest/oldest timestamp from messages
+ expect(transaction).to have_queue_start(sent_timestamp)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.name).to eq("DemoShoryukenWorker#perform")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("DemoShoryukenWorker#perform")
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("background")
+ expect(exception_events).to be_empty
+ span = event_spans.find { |s| s.name == "perform_job.shoryuken" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes).not_to have_key("appsignal.body")
+ expect(span.attributes["appsignal.category"]).to eq("perform_job.shoryuken")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(
+ "msg2" => "foo bar",
+ "msg1" => { "id" => "123", "foo" => "Foo", "bar" => "Bar" }
+ )
+ expect(root_span.attributes["appsignal.tag.batch"]).to eq(true)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("some-funky-queue-name")
+ # Earliest/oldest timestamp from messages
+ expect(root_span.attributes["appsignal.tag.SentTimestamp"])
+ .to eq(sent_timestamp.to_s)
+ queue_event = Array(root_span.events).find { |e| e.name == "appsignal.queue_start" }
+ expect(queue_event.attributes["appsignal.queue_start"]).to eq(sent_timestamp)
+
+ # A batch carries messages from multiple traces, so it is not linked back.
+ expect(Array(root_span.links)).to be_empty
+ end
end
end
end
describe Appsignal::Integrations::ShoryukenClientMiddleware do
let(:options) { { :message_body => "foo" } }
- before { start_agent }
- around { |example| keep_transactions { example.run } }
def enqueue(&block)
block ||= lambda {}
@@ -175,9 +368,16 @@ def enqueue(&block)
end
context "with an active transaction" do
+ def perform
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+ enqueue
+ transaction
+ end
+
# Enqueuing through a Shoryuken worker carries the worker class in the
# `shoryuken_class` message attribute, so the event is titled after it.
- context "enqueued through a worker" do
+ describe "enqueued through a worker" do
let(:options) do
{
:message_body => "foo",
@@ -188,55 +388,118 @@ def enqueue(&block)
}
end
- it "records the enqueue under the transaction, titled after the worker" do
- transaction = http_request_transaction
- set_current_transaction(transaction)
-
- enqueue
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = perform
+ # Records an enqueue event on the transaction, titled after the worker;
+ # no wire context in agent mode.
event = transaction.to_h["events"].find { |e| e["name"] == "enqueue.shoryuken" }
expect(event).to_not be_nil
expect(event["title"]).to eq("enqueue MyShoryukenWorker job")
+ expect(options[:message_attributes]).to_not have_key("traceparent")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ Appsignal::Transaction.complete_current!
+
+ # The enqueue is a producer event span under the active transaction,
+ # named after the worker being enqueued.
+ producer = event_spans.find { |s| s.name == "enqueue MyShoryukenWorker job" }
+ expect(producer.attributes["appsignal.category"]).to eq("enqueue.shoryuken")
+ expect(producer.kind).to eq(:producer)
+ expect(producer.parent_span_id).to eq(root_span.span_id)
+
+ # The message carries the producer span's trace context as an SQS message
+ # attribute, wire-equivalent to OpenTelemetry's aws-sdk instrumentation.
+ expect(options[:message_attributes]["traceparent"]).to eq(
+ :string_value => "00-#{producer.hex_trace_id}-#{producer.hex_span_id}-01",
+ :data_type => "String"
+ )
end
end
# A raw `send_message` enqueue has no worker class, so the event falls back
# to naming the queue it was sent to.
- context "enqueued as a raw message" do
+ describe "enqueued as a raw message" do
let(:options) do
{ :message_body => "foo", :queue_url => "https://sqs.us-east-1.amazonaws.com/0/my-queue" }
end
- it "records the enqueue under the transaction, titled after the queue" do
- transaction = http_request_transaction
- set_current_transaction(transaction)
-
- enqueue
+ it "in agent mode", :agent_mode do
+ start_agent
+ transaction = perform
event = transaction.to_h["events"].find { |e| e["name"] == "enqueue.shoryuken" }
expect(event).to_not be_nil
expect(event["title"]).to eq("enqueue on my-queue")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ Appsignal::Transaction.complete_current!
+
+ producer = event_spans.find { |s| s.name == "enqueue on my-queue" }
+ expect(producer).to_not be_nil
+ expect(producer.attributes["appsignal.category"]).to eq("enqueue.shoryuken")
+ end
end
end
context "without an active transaction" do
- it "passes through without recording" do
- expect { |block| enqueue(&block) }.to yield_control
+ describe "passes through without recording or injecting" do
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ enqueue
+ expect(options).to_not have_key(:message_attributes)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ # No transaction to attach the event to, so nothing is emitted and the
+ # outgoing options are untouched.
+ enqueue
+ expect(span_exporter.finished_spans.map(&:name)).to_not include("enqueue.shoryuken")
+ expect(options).to_not have_key(:message_attributes)
+ end
end
end
context "when job enqueue events are suppressed" do
# As happens under Active Job, which records the enqueue itself.
- it "passes through without recording the enqueue" do
+ def enqueue_suppressed(transaction)
+ transaction.suppress_job_enqueue_events { enqueue }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
- transaction.suppress_job_enqueue_events { enqueue }
+ enqueue_suppressed(transaction)
# The outer integration records the enqueue, so this one doesn't.
event_names = transaction.to_h["events"].map { |event| event["name"] }
expect(event_names).to_not include("enqueue.shoryuken")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ enqueue_suppressed(transaction)
+ Appsignal::Transaction.complete_current!
+
+ # No producer span for the suppressed enqueue...
+ expect(span_exporter.finished_spans.map(&:name)).to_not include("enqueue.shoryuken")
+ # ...but the trace context is still injected so the job links back.
+ expect(options[:message_attributes]).to have_key("traceparent")
+ end
end
end
diff --git a/spec/lib/appsignal/integrations/sidekiq_spec.rb b/spec/lib/appsignal/integrations/sidekiq_spec.rb
index 5dc119a21..bdb249db0 100644
--- a/spec/lib/appsignal/integrations/sidekiq_spec.rb
+++ b/spec/lib/appsignal/integrations/sidekiq_spec.rb
@@ -3,10 +3,7 @@
describe Appsignal::Integrations::SidekiqDeathHandler do
let(:options) { {} }
- before do
- stub_const("Sidekiq::VERSION", "7.1.0")
- start_agent(:options => options)
- end
+ let(:start_agent_args) { { :options => options } }
around { |example| keep_transactions { example.run } }
let(:exception) do
@@ -16,53 +13,87 @@
end
let(:job_context) { {} }
let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
- def call_handler
+ def perform
+ set_current_transaction(transaction)
expect do
described_class.new.call(job_context, exception)
end.to_not(change { created_transactions.count })
end
- def expect_error_on_transaction
- expect(last_transaction).to have_error("ExampleStandardError", "uh oh")
- end
-
- def expect_no_error_on_transaction
- expect(last_transaction).to_not have_error
- end
-
context "when sidekiq_report_errors = none" do
let(:options) { { :sidekiq_report_errors => "none" } }
- before { call_handler }
- it "doesn't track the error on the transaction" do
- expect_no_error_on_transaction
+ describe "doesn't track the error on the transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(exception_events).to be_empty
+ end
end
end
context "when sidekiq_report_errors = all" do
let(:options) { { :sidekiq_report_errors => "all" } }
- before { call_handler }
- it "doesn't track the error on the transaction" do
- expect_no_error_on_transaction
+ describe "doesn't track the error on the transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(exception_events).to be_empty
+ end
end
end
context "when sidekiq_report_errors = discard" do
let(:options) { { :sidekiq_report_errors => "discard" } }
- before { call_handler }
- it "records each occurrence of the error on the transaction" do
- expect_error_on_transaction
+ describe "records the error on the transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_error("ExampleStandardError", "uh oh")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
end
describe Appsignal::Integrations::SidekiqErrorHandler do
let(:options) { {} }
- before { start_agent(:options => options) }
+ let(:start_agent_args) { { :options => options } }
around { |example| keep_transactions { example.run } }
let(:exception) do
@@ -79,43 +110,130 @@ def expect_no_error_on_transaction
}
end
- def expect_report_internal_error
+ def perform
expect do
described_class.new.call(exception, job_context)
end.to(change { created_transactions.count }.by(1))
-
- transaction = last_transaction
- expect(transaction).to have_action("SidekiqInternal")
- expect(transaction).to have_error("ExampleStandardError", "uh oh")
- expect(transaction).to include_params(
- "jobstr" => "{ bad json }"
- )
- expect(transaction).to include_metadata(
- "sidekiq_error" => "Sidekiq internal error!"
- )
end
context "when sidekiq_report_errors = none" do
let(:options) { { :sidekiq_report_errors => "none" } }
- it "tracks the error on a new transaction" do
- expect_report_internal_error
+ describe "tracks the error on a new transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_action("SidekiqInternal")
+ expect(transaction).to have_error("ExampleStandardError", "uh oh")
+ expect(transaction).to include_params(
+ "jobstr" => "{ bad json }"
+ )
+ expect(transaction).to include_metadata(
+ "sidekiq_error" => "Sidekiq internal error!"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("SidekiqInternal")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to include("jobstr" => "{ bad json }")
+ expect(root_span.attributes["appsignal.tag.sidekiq_error"])
+ .to eq("Sidekiq internal error!")
+ end
end
end
context "when sidekiq_report_errors = all" do
let(:options) { { :sidekiq_report_errors => "all" } }
- it "tracks the error on a new transaction" do
- expect_report_internal_error
+ describe "tracks the error on a new transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_action("SidekiqInternal")
+ expect(transaction).to have_error("ExampleStandardError", "uh oh")
+ expect(transaction).to include_params(
+ "jobstr" => "{ bad json }"
+ )
+ expect(transaction).to include_metadata(
+ "sidekiq_error" => "Sidekiq internal error!"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("SidekiqInternal")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to include("jobstr" => "{ bad json }")
+ expect(root_span.attributes["appsignal.tag.sidekiq_error"])
+ .to eq("Sidekiq internal error!")
+ end
end
end
context "when sidekiq_report_errors = discard" do
let(:options) { { :sidekiq_report_errors => "discard" } }
- it "tracks the error on a new transaction" do
- expect_report_internal_error
+ describe "tracks the error on a new transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_action("SidekiqInternal")
+ expect(transaction).to have_error("ExampleStandardError", "uh oh")
+ expect(transaction).to include_params(
+ "jobstr" => "{ bad json }"
+ )
+ expect(transaction).to include_metadata(
+ "sidekiq_error" => "Sidekiq internal error!"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("SidekiqInternal")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to include("jobstr" => "{ bad json }")
+ expect(root_span.attributes["appsignal.tag.sidekiq_error"])
+ .to eq("Sidekiq internal error!")
+ end
end
end
end
@@ -123,52 +241,84 @@ def expect_report_internal_error
context "when error is a job error" do
let(:sidekiq_context) { { :job => {} } }
let(:transaction) { http_request_transaction }
- before do
+
+ def perform
transaction.set_action("existing transaction action")
set_current_transaction(transaction)
- end
-
- def call_handler
expect do
described_class.new.call(exception, sidekiq_context)
end.to_not(change { created_transactions.count })
end
- def expect_error_on_transaction
- expect(last_transaction).to have_error("ExampleStandardError", "uh oh")
- end
-
- def expect_no_error_on_transaction
- expect(last_transaction).to_not have_error
- end
-
context "when sidekiq_report_errors = none" do
let(:options) { { :sidekiq_report_errors => "none" } }
- before { call_handler }
- it "doesn't track the error on the transaction" do
- expect_no_error_on_transaction
- expect(last_transaction).to be_completed
+ describe "doesn't track the error and completes the transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ expect(last_transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ expect(transaction).to be_completed
+ end
end
end
context "when sidekiq_report_errors = all" do
let(:options) { { :sidekiq_report_errors => "all" } }
- before { call_handler }
- it "records each occurrence of the error on the transaction" do
- expect_error_on_transaction
- expect(last_transaction).to be_completed
+ describe "records the error and completes the transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_error("ExampleStandardError", "uh oh")
+ expect(last_transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(transaction).to be_completed
+ end
end
end
context "when sidekiq_report_errors = discard" do
let(:options) { { :sidekiq_report_errors => "discard" } }
- before { call_handler }
- it "doesn't track the error on the transaction" do
- expect_no_error_on_transaction
- expect(last_transaction).to be_completed
+ describe "doesn't track the error and completes the transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ expect(last_transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ expect(transaction).to be_completed
+ end
end
end
end
@@ -177,49 +327,102 @@ def expect_no_error_on_transaction
describe Appsignal::Integrations::SidekiqClientMiddleware do
let(:plugin) { described_class.new }
let(:job) { { "class" => "TestClass", "args" => [] } }
- before { start_agent }
- around { |example| keep_transactions { example.run } }
def enqueue
plugin.call("TestClass", job, "default", nil) { :enqueued }
end
context "with an active transaction" do
- it "records the enqueue under the transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
expect(enqueue).to eq(:enqueued)
- # Records an enqueue event on the transaction, titled after the job.
+ # Records an enqueue event on the transaction, titled after the job;
+ # no wire context in agent mode.
event = transaction.to_h["events"].find { |e| e["name"] == "enqueue.sidekiq" }
expect(event).to_not be_nil
expect(event["title"]).to eq("enqueue TestClass job")
+ expect(job).to_not have_key("traceparent")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ expect(enqueue).to eq(:enqueued)
+ Appsignal::Transaction.complete_current!
+
+ # The enqueue is a producer event span under the active transaction,
+ # named after the job being enqueued.
+ producer = event_spans.find { |s| s.name == "enqueue TestClass job" }
+ expect(producer.attributes["appsignal.category"]).to eq("enqueue.sidekiq")
+ expect(producer.kind).to eq(:producer)
+ expect(producer.parent_span_id).to eq(root_span.span_id)
+
+ # The job carries the producer span's trace context, so the job that
+ # performs can link back to it.
+ expect(job["traceparent"])
+ .to eq("00-#{producer.hex_trace_id}-#{producer.hex_span_id}-01")
end
end
context "without an active transaction" do
- it "passes through without recording" do
- expect { |block| plugin.call("TestClass", job, "default", nil, &block) }
- .to yield_control
+ it "in agent mode", :agent_mode do
+ start_agent
+ # A transparent pass-through: nothing is recorded and the job hash is
+ # untouched.
+ expect(enqueue).to eq(:enqueued)
expect(created_transactions).to be_empty
+ expect(job).to_not have_key("traceparent")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ # No transaction to attach the event to, so nothing is emitted and the
+ # job hash is untouched.
+ expect(enqueue).to eq(:enqueued)
+ expect(span_exporter.finished_spans.map(&:name)).to_not include("enqueue.sidekiq")
+ expect(job).to_not have_key("traceparent")
end
end
context "when job enqueue events are suppressed" do
# As happens under Active Job, which records the enqueue itself.
- it "passes through without recording the enqueue" do
+ def enqueue_suppressed(transaction)
+ transaction.suppress_job_enqueue_events { enqueue }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
transaction = http_request_transaction
set_current_transaction(transaction)
- result = transaction.suppress_job_enqueue_events { enqueue }
- expect(result).to eq(:enqueued)
+ expect(enqueue_suppressed(transaction)).to eq(:enqueued)
# The outer integration records the enqueue, so this one doesn't.
event_names = transaction.to_h["events"].map { |event| event["name"] }
expect(event_names).to_not include("enqueue.sidekiq")
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ set_current_transaction(transaction)
+
+ expect(enqueue_suppressed(transaction)).to eq(:enqueued)
+ Appsignal::Transaction.complete_current!
+
+ # No producer span for the suppressed enqueue...
+ expect(span_exporter.finished_spans.map(&:name)).to_not include("enqueue.sidekiq")
+ # ...but the trace context is still injected so the job links back.
+ expect(job).to have_key("traceparent")
+ end
end
end
@@ -271,9 +474,7 @@ class DelayedTestClass; end
end
let(:plugin) { Appsignal::Integrations::SidekiqMiddleware.new }
let(:options) { {} }
- before do
- start_agent(:options => options)
- end
+ let(:start_agent_args) { { :options => options } }
around { |example| keep_transactions { example.run } }
def expect_no_yaml_parse_error(logs)
@@ -281,31 +482,124 @@ def expect_no_yaml_parse_error(logs)
end
describe "internal Sidekiq job values" do
- it "does not save internal Sidekiq values as metadata on transaction" do
- perform_sidekiq_job
+ describe "does not save internal Sidekiq values as metadata on transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform_sidekiq_job
+
+ transaction_hash = transaction.to_h
+ expect(transaction_hash["metadata"].keys)
+ .to_not include(*Appsignal::Integrations::SidekiqMiddleware::EXCLUDED_JOB_KEYS)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform_sidekiq_job
- transaction_hash = transaction.to_h
- expect(transaction_hash["metadata"].keys)
- .to_not include(*Appsignal::Integrations::SidekiqMiddleware::EXCLUDED_JOB_KEYS)
+ excluded = Appsignal::Integrations::SidekiqMiddleware::EXCLUDED_JOB_KEYS
+ excluded.each do |key|
+ expect(root_span.attributes).to_not have_key("appsignal.tag.#{key}")
+ end
+ end
+ end
+ end
+
+ describe "with incoming trace context" do
+ let(:trace_id_hex) { "0af7651916cd43dd8448eb211c80319c" }
+ let(:span_id_hex) { "b7ad6b7169203331" }
+ let(:traceparent) { "00-#{trace_id_hex}-#{span_id_hex}-01" }
+
+ # A job runs as its own trace, linked back to the span that enqueued it.
+ def expect_linked_back_to_remote
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.hex_trace_id).to_not eq(trace_id_hex)
+ expect(root_span.links.size).to eq(1)
+ link_context = root_span.links.first.span_context
+ expect(link_context.hex_trace_id).to eq(trace_id_hex)
+ expect(link_context.hex_span_id).to eq(span_id_hex)
+ end
+
+ context "with a top-level traceparent (Sidekiq style)" do
+ let(:item) { super().merge("traceparent" => traceparent) }
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform_sidekiq_job
+
+ # The trace header doesn't leak into the transaction as metadata.
+ expect(transaction.to_h["metadata"].keys).to_not include("traceparent")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform_sidekiq_job
+
+ expect_linked_back_to_remote
+ end
+ end
+
+ context "with a traceparent nested under __otel_headers (ActiveJob style)" do
+ # OpenTelemetry's ActiveJob instrumentation runs the headers through
+ # ActiveJob's argument serializer, so they arrive as an array of
+ # [key, value] pairs, not a hash.
+ let(:item) { super().merge("__otel_headers" => [["traceparent", traceparent]]) }
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform_sidekiq_job
+
+ expect(transaction.to_h["metadata"].keys).to_not include("__otel_headers")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform_sidekiq_job
+
+ expect_linked_back_to_remote
+ end
end
end
context "with parameter filtering" do
let(:options) { { :filter_parameters => ["foo"] } }
- it "filters selected arguments" do
- perform_sidekiq_job
+ describe "filters selected arguments" do
+ def perform
+ perform_sidekiq_job
+ end
- expect(transaction).to include_params(
- [
- "foo",
- {
- "foo" => "[FILTERED]",
- "bar" => "Bar",
- "baz" => { "1" => "foo" }
- }
- ]
- )
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to include_params(
+ [
+ "foo",
+ {
+ "foo" => "[FILTERED]",
+ "bar" => "Bar",
+ "baz" => { "1" => "foo" }
+ }
+ ]
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ params = JSON.parse(root_span.attributes["appsignal.function.parameters"])
+ expect(params).to eq(
+ [
+ "foo",
+ {
+ "foo" => "[FILTERED]",
+ "bar" => "Bar",
+ "baz" => { "1" => "foo" }
+ }
+ ]
+ )
+ end
end
end
@@ -315,10 +609,25 @@ def expect_no_yaml_parse_error(logs)
item["args"] << "super secret value" # Last argument will be replaced
end
- it "replaces the last argument (the secret bag) with an [encrypted data] string" do
- perform_sidekiq_job
+ describe "replaces the last argument (the secret bag) with an [encrypted data] string" do
+ def perform
+ perform_sidekiq_job
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to include_params(expected_args << "[encrypted data]")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(transaction).to include_params(expected_args << "[encrypted data]")
+ params = JSON.parse(root_span.attributes["appsignal.function.parameters"])
+ expect(params).to eq(expected_args << "[encrypted data]")
+ end
end
end
@@ -338,22 +647,57 @@ def expect_no_yaml_parse_error(logs)
}
end
- it "uses the delayed class and method name for the action" do
- perform_sidekiq_job
+ describe "uses the delayed class and method name for the action" do
+ def perform
+ perform_sidekiq_job
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to have_action("DelayedTestClass.foo_method")
+ expect(transaction).to include_params([{ "bar" => "baz" }])
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(transaction).to have_action("DelayedTestClass.foo_method")
- expect(transaction).to include_params([{ "bar" => "baz" }])
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("DelayedTestClass.foo_method")
+ params = JSON.parse(root_span.attributes["appsignal.function.parameters"])
+ expect(params).to eq([{ "bar" => "baz" }])
+ end
end
context "when job arguments is a malformed YAML object" do
before { item["args"] = [] }
- it "logs a warning and uses the default argument" do
- logs = capture_logs { perform_sidekiq_job }
+ describe "logs a warning and uses the default argument" do
+ def perform
+ capture_logs { perform_sidekiq_job }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ logs = perform
+
+ expect(transaction).to have_action("Sidekiq::Extensions::DelayedClass#perform")
+ expect(transaction).to include_params([])
+ expect(logs).to contains_log(:warn, "Unable to load YAML")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ logs = perform
- expect(transaction).to have_action("Sidekiq::Extensions::DelayedClass#perform")
- expect(transaction).to include_params([])
- expect(logs).to contains_log(:warn, "Unable to load YAML")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("Sidekiq::Extensions::DelayedClass#perform")
+ params = JSON.parse(root_span.attributes["appsignal.function.parameters"])
+ expect(params).to eq([])
+ expect(logs).to contains_log(:warn, "Unable to load YAML")
+ end
end
end
end
@@ -374,22 +718,57 @@ def expect_no_yaml_parse_error(logs)
}
end
- it "uses the delayed class and method name for the action" do
- perform_sidekiq_job
+ describe "uses the delayed class and method name for the action" do
+ def perform
+ perform_sidekiq_job
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to have_action("DelayedTestClass#foo_method")
+ expect(transaction).to include_params([{ "bar" => "baz" }])
+ end
- expect(transaction).to have_action("DelayedTestClass#foo_method")
- expect(transaction).to include_params([{ "bar" => "baz" }])
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("DelayedTestClass#foo_method")
+ params = JSON.parse(root_span.attributes["appsignal.function.parameters"])
+ expect(params).to eq([{ "bar" => "baz" }])
+ end
end
context "when job arguments is a malformed YAML object" do
before { item["args"] = [] }
- it "logs a warning and uses the default argument" do
- logs = capture_logs { perform_sidekiq_job }
+ describe "logs a warning and uses the default argument" do
+ def perform
+ capture_logs { perform_sidekiq_job }
+ end
- expect(transaction).to have_action("Sidekiq::Extensions::DelayedModel#perform")
- expect(transaction).to include_params([])
- expect(logs).to contains_log(:warn, "Unable to load YAML")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ logs = perform
+
+ expect(transaction).to have_action("Sidekiq::Extensions::DelayedModel#perform")
+ expect(transaction).to include_params([])
+ expect(logs).to contains_log(:warn, "Unable to load YAML")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ logs = perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("Sidekiq::Extensions::DelayedModel#perform")
+ params = JSON.parse(root_span.attributes["appsignal.function.parameters"])
+ expect(params).to eq([])
+ expect(logs).to contains_log(:warn, "Unable to load YAML")
+ end
end
end
end
@@ -397,37 +776,72 @@ def expect_no_yaml_parse_error(logs)
context "with an error" do
let(:error) { ExampleException }
- it "creates a transaction and adds the error" do
- # TODO: additional curly brackets required for issue
- # https://github.com/rspec/rspec-mocks/issues/1460
- expect(Appsignal).to receive(:increment_counter)
- .with("sidekiq_queue_job_count", 1, { :queue => "default", :status => :failed })
- expect(Appsignal).to receive(:increment_counter)
- .with("sidekiq_queue_job_count", 1, { :queue => "default", :status => :processed })
- expect(Appsignal).to receive(:increment_counter)
- .with("sidekiq_worker_job_count", 1,
- { :worker => "TestClass#perform", :queue => "default", :status => :failed })
- expect(Appsignal).to receive(:increment_counter)
- .with("sidekiq_worker_job_count", 1,
- { :worker => "TestClass#perform", :queue => "default", :status => :processed })
- expect do
- perform_sidekiq_job { raise error, "uh oh" }
- end.to raise_error(error)
-
- expect(transaction).to have_id
- expect(transaction).to have_namespace(namespace)
- expect(transaction).to have_action("TestClass#perform")
- expect(transaction).to have_error("ExampleException", "uh oh")
- expect(transaction).to include_metadata(
- "extra" => "data",
- "queue" => "default",
- "retry_count" => "0"
- )
- expect(transaction).to_not include_environment
- expect(transaction).to include_params(expected_args)
- expect(transaction).to include_tags("request_id" => jid)
- expect(transaction).to_not include_breadcrumbs
- expect_transaction_to_have_sidekiq_event(transaction)
+ describe "creates a transaction and adds the error" do
+ def perform
+ # TODO: additional curly brackets required for issue
+ # https://github.com/rspec/rspec-mocks/issues/1460
+ expect(Appsignal).to receive(:increment_counter)
+ .with("sidekiq_queue_job_count", 1, { :queue => "default", :status => :failed })
+ expect(Appsignal).to receive(:increment_counter)
+ .with("sidekiq_queue_job_count", 1, { :queue => "default", :status => :processed })
+ expect(Appsignal).to receive(:increment_counter)
+ .with("sidekiq_worker_job_count", 1,
+ { :worker => "TestClass#perform", :queue => "default", :status => :failed })
+ expect(Appsignal).to receive(:increment_counter)
+ .with("sidekiq_worker_job_count", 1,
+ { :worker => "TestClass#perform", :queue => "default", :status => :processed })
+ expect do
+ perform_sidekiq_job { raise error, "uh oh" }
+ end.to raise_error(error)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to have_id
+ expect(transaction).to have_namespace(namespace)
+ expect(transaction).to have_action("TestClass#perform")
+ expect(transaction).to have_error("ExampleException", "uh oh")
+ expect(transaction).to include_metadata(
+ "extra" => "data",
+ "queue" => "default",
+ "retry_count" => "0"
+ )
+ expect(transaction).to_not include_environment
+ expect(transaction).to include_params(expected_args)
+ expect(transaction).to include_tags("request_id" => jid)
+ expect(transaction).to_not include_breadcrumbs
+ expect_transaction_to_have_sidekiq_event(transaction)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("TestClass#perform")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ expect(root_span.attributes["appsignal.tag.extra"]).to eq("data")
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("default")
+ expect(root_span.attributes["appsignal.tag.retry_count"]).to eq("0")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(expected_args)
+ expect(root_span.attributes["appsignal.tag.request_id"]).to eq(jid)
+ expect(event_spans.size).to eq(1)
+ span = event_spans.find { |s| s.name == "perform_job.sidekiq" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes).not_to have_key("appsignal.body")
+ expect(span.attributes["appsignal.category"]).to eq("perform_job.sidekiq")
+ end
end
end
@@ -435,55 +849,110 @@ def expect_no_yaml_parse_error(logs)
context "with Rails error reporter" do
include RailsHelper
- it "reports the worker name as the action, copies the namespace and tags" do
- expect do
- with_rails_error_reporter do
- perform_sidekiq_job do
- Appsignal.tag_job("test_tag" => "value")
- Rails.error.handle do
- raise ExampleStandardError, "error message"
+ describe "reports the worker name as the action, copies the namespace and tags" do
+ def perform
+ expect do
+ with_rails_error_reporter do
+ perform_sidekiq_job do
+ Appsignal.tag_job("test_tag" => "value")
+ Rails.error.handle do
+ raise ExampleStandardError, "error message"
+ end
end
end
- end
- end.to change { created_transactions.count }.by(1)
+ end.to change { created_transactions.count }.by(1)
+ end
- tags = { "test_tag" => "value" }
- transaction = last_transaction
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
- expect(transaction).to have_namespace("background_job")
- expect(transaction).to have_action("TestClass#perform")
- expect(transaction).to have_error("ExampleStandardError", "error message")
- expect(transaction).to include_tags(tags)
+ tags = { "test_tag" => "value" }
+ transaction = last_transaction
+
+ expect(transaction).to have_namespace("background_job")
+ expect(transaction).to have_action("TestClass#perform")
+ expect(transaction).to have_error("ExampleStandardError", "error message")
+ expect(transaction).to include_tags(tags)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("TestClass#perform")
+ event = exception_events
+ .find { |e| e.attributes["exception.type"] == "ExampleStandardError" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.attributes["appsignal.tag.test_tag"]).to eq("value")
+ end
end
end
end
context "without an error" do
- it "creates a transaction with events" do
- # TODO: additional curly brackets required for issue
- # https://github.com/rspec/rspec-mocks/issues/1460
- expect(Appsignal).to receive(:increment_counter)
- .with("sidekiq_queue_job_count", 1, { :queue => "default", :status => :processed })
- expect(Appsignal).to receive(:increment_counter)
- .with("sidekiq_worker_job_count", 1,
- { :worker => "TestClass#perform", :queue => "default", :status => :processed })
- perform_sidekiq_job
-
- expect(transaction).to have_id
- expect(transaction).to have_namespace(namespace)
- expect(transaction).to have_action("TestClass#perform")
- expect(transaction).to_not have_error
- expect(transaction).to include_tags("request_id" => jid)
- expect(transaction).to_not include_environment
- expect(transaction).to_not include_breadcrumbs
- expect(transaction).to_not include_params(expected_args)
- expect(transaction).to include_metadata(
- "extra" => "data",
- "queue" => "default",
- "retry_count" => "0"
- )
- expect(transaction).to have_queue_start(Time.parse("2001-01-01 10:00:00UTC").to_i * 1000)
- expect_transaction_to_have_sidekiq_event(transaction)
+ describe "creates a transaction with events" do
+ def perform
+ # TODO: additional curly brackets required for issue
+ # https://github.com/rspec/rspec-mocks/issues/1460
+ expect(Appsignal).to receive(:increment_counter)
+ .with("sidekiq_queue_job_count", 1, { :queue => "default", :status => :processed })
+ expect(Appsignal).to receive(:increment_counter)
+ .with("sidekiq_worker_job_count", 1,
+ { :worker => "TestClass#perform", :queue => "default", :status => :processed })
+ perform_sidekiq_job
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to have_id
+ expect(transaction).to have_namespace(namespace)
+ expect(transaction).to have_action("TestClass#perform")
+ expect(transaction).to_not have_error
+ expect(transaction).to include_tags("request_id" => jid)
+ expect(transaction).to_not include_environment
+ expect(transaction).to_not include_breadcrumbs
+ expect(transaction).to_not include_params(expected_args)
+ expect(transaction).to include_metadata(
+ "extra" => "data",
+ "queue" => "default",
+ "retry_count" => "0"
+ )
+ expect(transaction).to have_queue_start(
+ Time.parse("2001-01-01 10:00:00UTC").to_i * 1000
+ )
+ expect_transaction_to_have_sidekiq_event(transaction)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("TestClass#perform")
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.request_id"]).to eq(jid)
+ expect(root_span.attributes["appsignal.tag.extra"]).to eq("data")
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("default")
+ expect(root_span.attributes["appsignal.tag.retry_count"]).to eq("0")
+ queue_event = Array(root_span.events).find { |e| e.name == "appsignal.queue_start" }
+ expect(queue_event.attributes["appsignal.queue_start"])
+ .to eq(Time.parse("2001-01-01 10:00:00UTC").to_i * 1000)
+ expect(event_spans.size).to eq(1)
+ span = event_spans.find { |s| s.name == "perform_job.sidekiq" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes).not_to have_key("appsignal.body")
+ expect(span.attributes["appsignal.category"]).to eq("perform_job.sidekiq")
+ end
end
end
@@ -578,8 +1047,8 @@ def expect_transaction_to_have_sidekiq_event(transaction)
end
end
around { |example| keep_transactions { example.run } }
+
before do
- start_agent
Appsignal.internal_logger = test_logger(log)
ActiveJob::Base.queue_adapter = :sidekiq
@@ -605,35 +1074,19 @@ def perform(*_args)
end
end
- it "reports the transaction from the ActiveJob integration" do
- perform_activejob_sidekiq_job(ActiveJobSidekiqTestJob, given_args)
-
- transaction = last_transaction
- expect(transaction).to have_namespace(namespace)
- expect(transaction).to have_action("ActiveJobSidekiqTestJob#perform")
- expect(transaction).to_not have_error
- expect(transaction).to include_metadata("queue" => "default")
- expect(transaction).to_not include_environment
- expect(transaction).to include_params([expected_args])
- expect(transaction).to include_tags(expected_tags.merge("queue" => "default"))
- expect(transaction).to have_queue_start(time.to_i * 1000)
-
- events = transaction.to_h["events"]
- .sort_by { |e| e["start"] }
- .map { |event| event["name"] }
- expect(events).to eq(expected_perform_events)
- end
+ describe "reports the transaction from the ActiveJob integration" do
+ def perform
+ perform_activejob_sidekiq_job(ActiveJobSidekiqTestJob, given_args)
+ end
- context "with error" do
- it "reports the error on the transaction from the ActiveRecord integration" do
- expect do
- perform_activejob_sidekiq_job(ActiveJobSidekiqErrorTestJob, given_args)
- end.to raise_error(RuntimeError, "uh oh")
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction = last_transaction
expect(transaction).to have_namespace(namespace)
- expect(transaction).to have_action("ActiveJobSidekiqErrorTestJob#perform")
- expect(transaction).to have_error("RuntimeError", "uh oh")
+ expect(transaction).to have_action("ActiveJobSidekiqTestJob#perform")
+ expect(transaction).to_not have_error
expect(transaction).to include_metadata("queue" => "default")
expect(transaction).to_not include_environment
expect(transaction).to include_params([expected_args])
@@ -645,6 +1098,83 @@ def perform(*_args)
.map { |event| event["name"] }
expect(events).to eq(expected_perform_events)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActiveJobSidekiqTestJob#perform")
+ expect(exception_events).to be_empty
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("default")
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq([expected_args])
+ expect(root_span.attributes["appsignal.tag.executions"]).to eq(1)
+ queue_event = Array(root_span.events).find { |e| e.name == "appsignal.queue_start" }
+ expect(queue_event.attributes["appsignal.queue_start"]).to eq(time.to_i * 1000)
+ # The job is enqueued without an active transaction here, so no
+ # enqueue event/producer span is recorded -- only the perform events.
+ expect(event_spans.map(&:name)).to match_array(expected_perform_events)
+ sidekiq_span = event_spans.find { |s| s.name == "perform_job.sidekiq" }
+ expect(sidekiq_span).not_to be_nil
+ expect(sidekiq_span.parent_span_id).to eq(root_span.span_id)
+ end
+ end
+
+ context "with error" do
+ describe "reports the error on the transaction from the ActiveRecord integration" do
+ def perform
+ expect do
+ perform_activejob_sidekiq_job(ActiveJobSidekiqErrorTestJob, given_args)
+ end.to raise_error(RuntimeError, "uh oh")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_namespace(namespace)
+ expect(transaction).to have_action("ActiveJobSidekiqErrorTestJob#perform")
+ expect(transaction).to have_error("RuntimeError", "uh oh")
+ expect(transaction).to include_metadata("queue" => "default")
+ expect(transaction).to_not include_environment
+ expect(transaction).to include_params([expected_args])
+ expect(transaction).to include_tags(expected_tags.merge("queue" => "default"))
+ expect(transaction).to have_queue_start(time.to_i * 1000)
+
+ events = transaction.to_h["events"]
+ .sort_by { |e| e["start"] }
+ .map { |event| event["name"] }
+ expect(events).to eq(expected_perform_events)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:consumer)
+ expect(root_span.attributes["appsignal.namespace"]).to eq("background")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActiveJobSidekiqErrorTestJob#perform")
+ expect(exception_events.size).to be >= 1
+ event = exception_events.find { |e| e.attributes["exception.type"] == "RuntimeError" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.message"]).to eq("uh oh")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.attributes["appsignal.tag.queue"]).to eq("default")
+ queue_event = Array(root_span.events).find { |e| e.name == "appsignal.queue_start" }
+ expect(queue_event.attributes["appsignal.queue_start"]).to eq(time.to_i * 1000)
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq([expected_args])
+ sidekiq_span = event_spans.find { |s| s.name == "perform_job.sidekiq" }
+ expect(sidekiq_span).not_to be_nil
+ expect(sidekiq_span.parent_span_id).to eq(root_span.span_id)
+ end
+ end
end
context "with ActionMailer" do
@@ -657,15 +1187,34 @@ def welcome(*args)
end
end
- it "reports ActionMailer data on the transaction" do
- perform_mailer(ActionMailerSidekiqTestJob, :welcome, given_args)
+ describe "reports ActionMailer data on the transaction" do
+ def perform
+ perform_mailer(ActionMailerSidekiqTestJob, :welcome, given_args)
+ end
- transaction = last_transaction
- expect(transaction).to have_action("ActionMailerSidekiqTestJob#welcome")
- expect(transaction).to include_params(
- ["ActionMailerSidekiqTestJob", "welcome",
- "deliver_now"] + expected_wrapped_args
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ transaction = last_transaction
+ expect(transaction).to have_action("ActionMailerSidekiqTestJob#welcome")
+ expect(transaction).to include_params(
+ ["ActionMailerSidekiqTestJob", "welcome",
+ "deliver_now"] + expected_wrapped_args
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("ActionMailerSidekiqTestJob#welcome")
+ expected_params =
+ ["ActionMailerSidekiqTestJob", "welcome", "deliver_now"] + expected_wrapped_args
+ expect(JSON.parse(root_span.attributes["appsignal.function.parameters"]))
+ .to eq(expected_params)
+ end
end
end
diff --git a/spec/lib/appsignal/integrations/webmachine_spec.rb b/spec/lib/appsignal/integrations/webmachine_spec.rb
index 471e563fe..2f1990dd9 100644
--- a/spec/lib/appsignal/integrations/webmachine_spec.rb
+++ b/spec/lib/appsignal/integrations/webmachine_spec.rb
@@ -21,6 +21,7 @@ def headers
{
"REQUEST_METHOD" => "GET",
"PATH_INFO" => "/some/path",
+ "HTTP_ACCEPT" => "application/json",
"ignored_header" => "something"
},
nil
@@ -46,17 +47,30 @@ def self.name
let(:resource_instance) { resource.new(request, response) }
let(:response) { Webmachine::Response.new }
let(:fsm) { Webmachine::Decision::FSM.new(resource_instance, request, response) }
- before { start_agent }
- around { |example| keep_transactions { example.run } }
describe "#run" do
- it "creates a transaction" do
- expect { fsm.run }.to(change { created_transactions.count }.by(1))
+ def perform
+ fsm.run
end
- it "sets the action" do
- fsm.run
- expect(last_transaction).to have_action("MyResource#GET")
+ it_in_both_modes "creates a transaction" do
+ expect { perform }.to(change { created_transactions.count }.by(1))
+ end
+
+ describe "sets the action" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ expect(last_transaction).to have_action("MyResource#GET")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ expect(root_span.name).to eq("MyResource#GET")
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MyResource#GET")
+ end
end
context "with action already set" do
@@ -69,52 +83,155 @@ def to_html
end
end
- it "doesn't overwrite the action" do
- fsm.run
- expect(last_transaction).to have_action("Custom Action")
+ describe "doesn't overwrite the action" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ expect(last_transaction).to have_action("Custom Action")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ expect(root_span.name).to eq("Custom Action")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("Custom Action")
+ end
end
end
- it "records an instrumentation event" do
- fsm.run
- expect(last_transaction).to include_event("name" => "process_action.webmachine")
+ describe "records an instrumentation event" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ expect(last_transaction).to include_event("name" => "process_action.webmachine")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ span = event_spans.find { |s| s.name == "process_action.webmachine" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ end
end
- it "sets the params" do
- fsm.run
- expect(last_transaction).to include_params("param1" => "value1", "param2" => "value2")
+ describe "sets the params" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ expect(last_transaction).to include_params("param1" => "value1", "param2" => "value2")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ params = JSON.parse(root_span.attributes["appsignal.request.payload"])
+ expect(params).to include("param1" => "value1", "param2" => "value2")
+ end
end
- it "sets the headers" do
- fsm.run
- expect(last_transaction).to include_environment(
- "REQUEST_METHOD" => "GET",
- "PATH_INFO" => "/some/path"
- )
+ describe "sets the headers" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ expect(last_transaction).to include_environment(
+ "REQUEST_METHOD" => "GET",
+ "PATH_INFO" => "/some/path",
+ "HTTP_ACCEPT" => "application/json"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ # Only true HTTP headers map to `http.request.header.*`; the non-header
+ # CGI vars (REQUEST_METHOD, PATH_INFO) are intentionally dropped.
+ expect(root_span.attributes["http.request.header.accept"]).to eq("application/json")
+ expect(root_span.attributes.keys).to_not include("http.request.header.request-method")
+ end
end
- it "closes the transaction" do
- fsm.run
+ it_in_both_modes "closes the transaction" do
+ perform
expect(last_transaction).to be_completed
expect(current_transaction?).to be_falsy
end
+ describe "incoming trace context" do
+ let(:trace_id_hex) { "0af7651916cd43dd8448eb211c80319c" }
+ let(:span_id_hex) { "b7ad6b7169203331" }
+
+ it "continues the upstream trace when a traceparent is present", :collector_mode do
+ # The real Webmachine adapter builds a case-insensitive
+ # `Webmachine::Headers`; the plain Hash here uses the same lowercased
+ # header name, which the default getter reads identically.
+ request.headers["traceparent"] = "00-#{trace_id_hex}-#{span_id_hex}-01"
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.hex_trace_id).to eq(trace_id_hex)
+ expect(root_span.parent_span_id.unpack1("H*")).to eq(span_id_hex)
+ end
+
+ it "starts a fresh root trace when no traceparent is present", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.parent_span_id).to eq(::OpenTelemetry::Trace::INVALID_SPAN_ID)
+ end
+ end
+
context "with parent transaction" do
let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
+ # The parent is set inside each example rather than in a `before`: in
+ # collector mode the transaction must be created after the example body
+ # has enabled collector mode (via `start_collector_agent`), so it gets
+ # the OpenTelemetry backend.
- it "sets the action" do
- fsm.run
- expect(last_transaction).to have_action("MyResource#GET")
+ describe "sets the action" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+ expect(last_transaction).to have_action("MyResource#GET")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ # The parent transaction is not closed by `fsm.run`; finish it so
+ # its span is exported.
+ transaction.complete
+ expect(root_span.name).to eq("MyResource#GET")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MyResource#GET")
+ end
end
- it "sets the params" do
- fsm.run
- last_transaction._sample
- expect(last_transaction).to include_params("param1" => "value1", "param2" => "value2")
+ describe "sets the params" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+ last_transaction._sample
+ expect(last_transaction).to include_params("param1" => "value1", "param2" => "value2")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ # The parent transaction is not closed by `fsm.run`; finish it so
+ # its span is exported.
+ transaction.complete
+ params = JSON.parse(root_span.attributes["appsignal.request.payload"])
+ expect(params).to include("param1" => "value1", "param2" => "value2")
+ end
end
- it "does not close the transaction" do
+ it_in_both_modes "does not close the transaction" do
+ set_current_transaction(transaction)
expect(last_transaction).to_not be_completed
end
end
@@ -124,12 +241,32 @@ def to_html
let(:error) { ExampleException.new("error message") }
let(:transaction) { http_request_transaction }
- it "tracks the error" do
- with_current_transaction(transaction) do
- fsm.send(:handle_exceptions) { raise error }
+ describe "tracks the error" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ with_current_transaction(transaction) do
+ fsm.send(:handle_exceptions) { raise error }
+ end
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
end
- expect(last_transaction).to have_error("ExampleException", "error message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ with_current_transaction(transaction) do
+ fsm.send(:handle_exceptions) { raise error }
+ end
+ # Not completed by `handle_exceptions`; finish it to export the span.
+ transaction.complete
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
end
diff --git a/spec/lib/appsignal/loaders/grape_spec.rb b/spec/lib/appsignal/loaders/grape_spec.rb
index 439910b54..4be6f83cb 100644
--- a/spec/lib/appsignal/loaders/grape_spec.rb
+++ b/spec/lib/appsignal/loaders/grape_spec.rb
@@ -1,5 +1,5 @@
if DependencyHelper.grape_present?
- describe "Appsignal::Loaders::PadrinoLoader" do
+ describe "Appsignal::Loaders::GrapeLoader" do
describe "#on_load" do
it "ensures the Grape middleware is loaded" do
load_loader(:grape)
diff --git a/spec/lib/appsignal/loaders/hanami_spec.rb b/spec/lib/appsignal/loaders/hanami_spec.rb
index 5b3e93f2e..5c7383ab6 100644
--- a/spec/lib/appsignal/loaders/hanami_spec.rb
+++ b/spec/lib/appsignal/loaders/hanami_spec.rb
@@ -66,11 +66,9 @@ def hanami_middleware_options
describe "Appsignal::Loaders::HanamiLoader::HanamiIntegration" do
let(:transaction) { http_request_transaction }
let(:app) { HanamiApp::Actions::Books::Index }
- around { |example| keep_transactions { example.run } }
before do
expect(::Hanami.app.config).to receive(:root).and_return(project_fixture_path)
Appsignal.load(:hanami)
- start_agent
end
def make_request(env)
@@ -82,10 +80,25 @@ def make_request(env)
context "without an active transaction" do
let(:env) { {} }
- it "does not set the action name" do
- make_request(env)
+ describe "does not set the action name" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
- expect(transaction).to_not have_action
+ expect(transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
end
@@ -93,17 +106,49 @@ def make_request(env)
let(:env) { { Appsignal::Rack::APPSIGNAL_TRANSACTION => transaction } }
if DependencyHelper.hanami2_2_present?
- it "does not set an action name on the transaction" do
- # This is done by the middleware instead
- make_request(env)
+ # The action name is set by the middleware instead.
+ describe "does not set an action name on the transaction" do
+ def perform
+ make_request(env)
+ end
- expect(transaction).to_not have_action
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
else
- it "sets action name on the transaction" do
- make_request(env)
+ describe "sets action name on the transaction" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(transaction).to have_action("HanamiApp::Actions::Books::Index")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- expect(transaction).to have_action("HanamiApp::Actions::Books::Index")
+ expect(root_span.name).to eq("HanamiApp::Actions::Books::Index")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("HanamiApp::Actions::Books::Index")
+ end
end
end
end
diff --git a/spec/lib/appsignal/loaders/padrino_spec.rb b/spec/lib/appsignal/loaders/padrino_spec.rb
index 0ceb319e3..eb6242d8b 100644
--- a/spec/lib/appsignal/loaders/padrino_spec.rb
+++ b/spec/lib/appsignal/loaders/padrino_spec.rb
@@ -66,7 +66,6 @@ class PadrinoClassWithRouter
let(:env) { {} }
# TODO: use an instance double
let(:settings) { double(:name => "TestApp") }
- around { |example| keep_transactions { example.run } }
describe "routes" do
let(:env) do
@@ -114,9 +113,12 @@ def fetch_body(body)
context "when AppSignal is not active" do
let(:path) { "/foo" }
+ let(:appsignal_env) { :inactive_env }
+ # Pass the inactive env through to the mode contexts' `start_agent`.
+ let(:start_agent_args) { { :env => appsignal_env } }
before { app.controllers { get(:foo) { "content" } } }
- it "does not instrument the request" do
+ it_in_both_modes "does not instrument the request" do
expect do
expect(response).to match_response(200, "content")
end.to_not(change { created_transactions.count })
@@ -124,18 +126,41 @@ def fetch_body(body)
end
context "when AppSignal is active" do
- let(:transaction) { http_request_transaction }
- before do
- start_agent
- set_current_transaction(transaction)
+ # The Padrino integration sets the action on the current transaction,
+ # so build it in the example body (after the agent starts, so it is
+ # backed by the right backend) and set it as current before the
+ # request. `response` triggers `app.call(env)`.
+ def perform(status, body)
+ set_current_transaction(http_request_transaction)
+ expect(response).to match_response(status, body)
+ end
+
+ # In collector mode the action lands as the OTel span name and the
+ # `appsignal.action_name` attribute. Complete the transaction first so
+ # the root span is exported and readable.
+ def expect_collector_action(action)
+ Appsignal::Transaction.complete_current!
+ expect(root_span.name).to eq(action)
+ expect(root_span.attributes["appsignal.action_name"]).to eq(action)
end
context "with not existing route" do
let(:path) { "/404" }
- it "instruments the request" do
- expect(response).to match_response(404, /^GET /404/)
- expect(last_transaction).to have_action("PadrinoTestApp#unknown")
+ describe "sets the action to the app name and unknown action" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(404, /^GET /404/)
+
+ expect(last_transaction).to have_action("PadrinoTestApp#unknown")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(404, /^GET /404/)
+
+ expect_collector_action("PadrinoTestApp#unknown")
+ end
end
end
@@ -146,9 +171,21 @@ def fetch_body(body)
app.controllers { get(:static) { "Static!" } }
end
- it "does not instrument the request" do
- expect(response).to match_response(200, "Static!")
- expect(last_transaction).to_not have_action
+ describe "does not set an action name" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(200, "Static!")
+
+ expect(last_transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(200, "Static!")
+ Appsignal::Transaction.complete_current!
+
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
end
@@ -160,9 +197,20 @@ def fetch_body(body)
app.controllers { get(:my_original_path, :with => :id) { "content" } }
end
- it "falls back on Sinatra::Request#route_obj.original_path" do
- expect(response).to match_response(200, "content")
- expect(last_transaction).to have_action("PadrinoTestApp:/my_original_path/:id")
+ describe "falls back on Sinatra::Request#route_obj.original_path" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(200, "content")
+
+ expect(last_transaction).to have_action("PadrinoTestApp:/my_original_path/:id")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(200, "content")
+
+ expect_collector_action("PadrinoTestApp:/my_original_path/:id")
+ end
end
end
@@ -174,17 +222,25 @@ def fetch_body(body)
app.controllers { get(:my_original_path) { "content" } }
end
- it "falls back on app name" do
- expect(response).to match_response(200, "content")
- expect(last_transaction).to have_action("PadrinoTestApp#unknown")
+ describe "falls back on app name" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(200, "content")
+
+ expect(last_transaction).to have_action("PadrinoTestApp#unknown")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(200, "content")
+
+ expect_collector_action("PadrinoTestApp#unknown")
+ end
end
end
context "with existing route" do
let(:path) { "/" }
- def make_request
- expect(response).to match_response(200, "content")
- end
context "with action name as symbol" do
context "with :index helper" do
@@ -193,9 +249,20 @@ def make_request
app.controllers { get(:index) { "content" } }
end
- it "sets the action with the app name and action name" do
- make_request
- expect(last_transaction).to have_action("PadrinoTestApp:#index")
+ describe "sets the action with the app name and action name" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(200, "content")
+
+ expect(last_transaction).to have_action("PadrinoTestApp:#index")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(200, "content")
+
+ expect_collector_action("PadrinoTestApp:#index")
+ end
end
end
@@ -205,9 +272,20 @@ def make_request
app.controllers { get(:foo) { "content" } }
end
- it "sets the action with the app name and action name" do
- make_request
- expect(last_transaction).to have_action("PadrinoTestApp:#foo")
+ describe "sets the action with the app name and action name" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(200, "content")
+
+ expect(last_transaction).to have_action("PadrinoTestApp:#foo")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(200, "content")
+
+ expect_collector_action("PadrinoTestApp:#foo")
+ end
end
end
end
@@ -219,9 +297,20 @@ def make_request
app.controllers { get("/") { "content" } }
end
- it "sets the action with the app name and action path" do
- make_request
- expect(last_transaction).to have_action("PadrinoTestApp:#/")
+ describe "sets the action with the app name and action path" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(200, "content")
+
+ expect(last_transaction).to have_action("PadrinoTestApp:#/")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(200, "content")
+
+ expect_collector_action("PadrinoTestApp:#/")
+ end
end
end
@@ -231,9 +320,20 @@ def make_request
app.controllers { get("/foo") { "content" } }
end
- it "sets the action with the app name and action path" do
- make_request
- expect(last_transaction).to have_action("PadrinoTestApp:#/foo")
+ describe "sets the action with the app name and action path" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(200, "content")
+
+ expect(last_transaction).to have_action("PadrinoTestApp:#/foo")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(200, "content")
+
+ expect_collector_action("PadrinoTestApp:#/foo")
+ end
end
end
end
@@ -247,9 +347,20 @@ def make_request
app.controllers(:my_controller) { get(:index) { "content" } }
end
- it "sets the action with the app name, controller name and action name" do
- make_request
- expect(last_transaction).to have_action("PadrinoTestApp:my_controller#index")
+ describe "sets the action with the app name, controller name and action name" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(200, "content")
+
+ expect(last_transaction).to have_action("PadrinoTestApp:my_controller#index")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(200, "content")
+
+ expect_collector_action("PadrinoTestApp:my_controller#index")
+ end
end
end
@@ -259,9 +370,20 @@ def make_request
app.controllers("/my_controller") { get(:index) { "content" } }
end
- it "sets the action with the app name, controller name and action path" do
- make_request
- expect(last_transaction).to have_action("PadrinoTestApp:/my_controller#index")
+ describe "sets the action with the app name, controller name and action path" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(200, "content")
+
+ expect(last_transaction).to have_action("PadrinoTestApp:/my_controller#index")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(200, "content")
+
+ expect_collector_action("PadrinoTestApp:/my_controller#index")
+ end
end
end
end
diff --git a/spec/lib/appsignal/logger/opentelemetry_backend_spec.rb b/spec/lib/appsignal/logger/opentelemetry_backend_spec.rb
new file mode 100644
index 000000000..ce19f573e
--- /dev/null
+++ b/spec/lib/appsignal/logger/opentelemetry_backend_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require "opentelemetry/sdk" if DependencyHelper.opentelemetry_present?
+require "opentelemetry-logs-sdk" if DependencyHelper.opentelemetry_present?
+
+describe Appsignal::Logger::OpenTelemetryBackend, :if => DependencyHelper.opentelemetry_present? do
+ let(:exporter) { ::OpenTelemetry::SDK::Logs::Export::InMemoryLogRecordExporter.new }
+ let(:logger_provider) do
+ provider = ::OpenTelemetry::SDK::Logs::LoggerProvider.new
+ provider.add_log_record_processor(
+ ::OpenTelemetry::SDK::Logs::Export::SimpleLogRecordProcessor.new(exporter)
+ )
+ provider
+ end
+
+ before do
+ ::OpenTelemetry.logger_provider = logger_provider
+ described_class.reset!
+ end
+
+ after { described_class.reset! }
+
+ def emitted_records
+ exporter.emitted_log_records
+ end
+
+ describe ".emit" do
+ it "emits a log record carrying the formatted body and severity" do
+ described_class.emit("my-group", ::Logger::INFO, Appsignal::Logger::JSON, "hello world", {})
+
+ record = emitted_records.first
+ expect(record.body).to eq("hello world")
+ expect(record.severity_number).to eq(9)
+ expect(record.severity_text).to eq("INFO")
+ end
+
+ it "attaches appsignal.group and appsignal.format on every record" do
+ described_class.emit(
+ "my-group",
+ ::Logger::WARN,
+ Appsignal::Logger::LOGFMT,
+ "msg",
+ {}
+ )
+
+ attrs = emitted_records.first.attributes
+ expect(attrs["appsignal.group"]).to eq("my-group")
+ expect(attrs["appsignal.format"]).to eq("logfmt")
+ end
+
+ it "maps every supported format flag to its lowercase name" do
+ {
+ Appsignal::Logger::PLAINTEXT => "plaintext",
+ Appsignal::Logger::LOGFMT => "logfmt",
+ Appsignal::Logger::JSON => "json",
+ Appsignal::Logger::AUTODETECT => "autodetect"
+ }.each do |flag, name|
+ described_class.emit("g", ::Logger::INFO, flag, "m", {})
+ expect(emitted_records.last.attributes["appsignal.format"]).to eq(name)
+ end
+ end
+
+ it "carries user attributes through with coerced keys and values" do
+ described_class.emit(
+ "g",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "msg",
+ {
+ :string => "value",
+ "symbol" => :sym,
+ :integer => 42,
+ :float => 1.5,
+ :truthy => true,
+ :falsy => false,
+ :other => Time.utc(2026, 1, 2, 3, 4, 5)
+ }
+ )
+
+ attrs = emitted_records.first.attributes
+ expect(attrs).to include(
+ "string" => "value",
+ "symbol" => "sym",
+ "integer" => 42,
+ "float" => 1.5,
+ "truthy" => true,
+ "falsy" => false,
+ "other" => "2026-01-02 03:04:05 UTC"
+ )
+ end
+
+ it "does not let user attributes override the appsignal.* keys" do
+ described_class.emit(
+ "the-group",
+ ::Logger::INFO,
+ Appsignal::Logger::JSON,
+ "msg",
+ { "appsignal.group" => "spoofed", "appsignal.format" => "spoofed" }
+ )
+
+ attrs = emitted_records.first.attributes
+ expect(attrs["appsignal.group"]).to eq("the-group")
+ expect(attrs["appsignal.format"]).to eq("json")
+ end
+
+ it "maps every Ruby Logger severity to the right OTel SeverityNumber" do
+ expected = {
+ ::Logger::DEBUG => [5, "DEBUG"],
+ ::Logger::INFO => [9, "INFO"],
+ ::Logger::WARN => [13, "WARN"],
+ ::Logger::ERROR => [17, "ERROR"],
+ ::Logger::FATAL => [21, "FATAL"]
+ }
+ expected.each do |severity, (number, text)|
+ described_class.emit("g", severity, Appsignal::Logger::PLAINTEXT, "m", {})
+ record = emitted_records.last
+ expect(record.severity_number).to eq(number)
+ expect(record.severity_text).to eq(text)
+ end
+ end
+
+ it "uses the 'appsignal-logger' instrumentation scope name" do
+ described_class.emit("g", ::Logger::INFO, Appsignal::Logger::PLAINTEXT, "msg", {})
+
+ expect(emitted_records.first.instrumentation_scope.name).to eq("appsignal-logger")
+ end
+ end
+
+ describe "logger caching" do
+ it "fetches the OTel logger once and reuses it across emits" do
+ expect(::OpenTelemetry.logger_provider).to receive(:logger)
+ .with(:name => "appsignal-logger").once.and_call_original
+
+ described_class.emit("g", ::Logger::INFO, Appsignal::Logger::PLAINTEXT, "a", {})
+ described_class.emit("g", ::Logger::INFO, Appsignal::Logger::PLAINTEXT, "b", {})
+ end
+
+ it "rebuilds the logger after reset! to pick up a new provider" do
+ described_class.emit("g", ::Logger::INFO, Appsignal::Logger::PLAINTEXT, "a", {})
+ described_class.reset!
+
+ new_provider = ::OpenTelemetry::SDK::Logs::LoggerProvider.new
+ new_exporter = ::OpenTelemetry::SDK::Logs::Export::InMemoryLogRecordExporter.new
+ new_provider.add_log_record_processor(
+ ::OpenTelemetry::SDK::Logs::Export::SimpleLogRecordProcessor.new(new_exporter)
+ )
+ ::OpenTelemetry.logger_provider = new_provider
+
+ described_class.emit("g", ::Logger::INFO, Appsignal::Logger::PLAINTEXT, "b", {})
+ expect(new_exporter.emitted_log_records.map(&:body)).to eq(["b"])
+ end
+ end
+end
diff --git a/spec/lib/appsignal/logger_spec.rb b/spec/lib/appsignal/logger_spec.rb
index 21da245b4..f6e050e4d 100644
--- a/spec/lib/appsignal/logger_spec.rb
+++ b/spec/lib/appsignal/logger_spec.rb
@@ -1,132 +1,272 @@
shared_examples "tagged logging" do
- it "logs messages with tags from logger.tagged" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "[My tag] [My other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
-
- logger.tagged("My tag", "My other tag") do
- logger.info("Some message")
+ describe "with tags from logger.tagged" do
+ def perform
+ logger.tagged("My tag", "My other tag") do
+ logger.info("Some message")
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[My tag] [My other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[My tag] [My other tag] Some message\n",
+ {}
+ )
+ perform
end
end
- it "logs messages with nested tags from logger.tagged" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "[My tag] [My other tag] [Nested tag] [Nested other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
-
- logger.tagged("My tag", "My other tag") do
- logger.tagged("Nested tag", "Nested other tag") do
- logger.info("Some message")
+ describe "with nested tags from logger.tagged" do
+ def perform
+ logger.tagged("My tag", "My other tag") do
+ logger.tagged("Nested tag", "Nested other tag") do
+ logger.info("Some message")
+ end
end
end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[My tag] [My other tag] [Nested tag] [Nested other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[My tag] [My other tag] [Nested tag] [Nested other tag] Some message\n",
+ {}
+ )
+ perform
+ end
end
- it "logs messages with tags from Rails.application.config.log_tags" do
- allow(Appsignal::Extension).to receive(:log)
-
- # This is how Rails sets the `log_tags` values
- logger.push_tags(["Request tag", "Second tag"])
- logger.tagged("First message", "My other tag") { logger.info("Some message") }
- expect(Appsignal::Extension).to have_received(:log)
- .with(
- "group",
- 3,
- 3,
- "[Request tag] [Second tag] [First message] [My other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
-
- # Logs all messsages within the time between `push_tags` and `pop_tags`
- # with the same set tags
- logger.tagged("Second message") { logger.info("Some message") }
- expect(Appsignal::Extension).to have_received(:log)
- .with(
- "group",
- 3,
- 3,
- "[Request tag] [Second tag] [Second message] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
-
- # This is how Rails clears the `log_tags` values
- # It will no longer includes those tags in new log messages
- logger.pop_tags(2)
- logger.tagged("Third message") { logger.info("Some message") }
- expect(Appsignal::Extension).to have_received(:log)
- .with(
- "group",
- 3,
- 3,
- "[Third message] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "with tags from Rails.application.config.log_tags" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ allow(Appsignal::Extension).to receive(:log)
+
+ logger.push_tags(["Request tag", "Second tag"])
+ logger.tagged("First message", "My other tag") { logger.info("Some message") }
+ expect(Appsignal::Extension).to have_received(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[Request tag] [Second tag] [First message] [My other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+
+ logger.tagged("Second message") { logger.info("Some message") }
+ expect(Appsignal::Extension).to have_received(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[Request tag] [Second tag] [Second message] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+
+ logger.pop_tags(2)
+ logger.tagged("Third message") { logger.info("Some message") }
+ expect(Appsignal::Extension).to have_received(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[Third message] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ allow(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+
+ logger.push_tags(["Request tag", "Second tag"])
+ logger.tagged("First message", "My other tag") { logger.info("Some message") }
+ expect(Appsignal::Logger::OpenTelemetryBackend).to have_received(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[Request tag] [Second tag] [First message] [My other tag] Some message\n",
+ {}
+ )
+
+ logger.tagged("Second message") { logger.info("Some message") }
+ expect(Appsignal::Logger::OpenTelemetryBackend).to have_received(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[Request tag] [Second tag] [Second message] Some message\n",
+ {}
+ )
+
+ logger.pop_tags(2)
+ logger.tagged("Third message") { logger.info("Some message") }
+ expect(Appsignal::Logger::OpenTelemetryBackend).to have_received(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[Third message] Some message\n",
+ {}
+ )
+ end
end
- it "logs messages with tags from Rails 8 application.config.log_tags" do
- allow(Appsignal::Extension).to receive(:log)
-
- # This is how Rails sets the `log_tags` values
- logger.push_tags("Request tag", "Second tag")
- logger.tagged("First message", "My other tag") { logger.info("Some message") }
- expect(Appsignal::Extension).to have_received(:log)
- .with(
- "group",
- 3,
- 3,
- "[Request tag] [Second tag] [First message] [My other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "with tags from Rails 8 application.config.log_tags" do
+ def perform
+ logger.push_tags("Request tag", "Second tag")
+ logger.tagged("First message", "My other tag") { logger.info("Some message") }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ allow(Appsignal::Extension).to receive(:log)
+ perform
+ expect(Appsignal::Extension).to have_received(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[Request tag] [Second tag] [First message] [My other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ allow(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ perform
+ expect(Appsignal::Logger::OpenTelemetryBackend).to have_received(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[Request tag] [Second tag] [First message] [My other tag] Some message\n",
+ {}
+ )
+ end
end
- it "clears all tags with clear_tags!" do
- allow(Appsignal::Extension).to receive(:log)
-
- # This is how Rails sets the `log_tags` values
- logger.push_tags(["Request tag", "Second tag"])
- logger.tagged("First message", "My other tag") { logger.info("Some message") }
- expect(Appsignal::Extension).to have_received(:log)
- .with(
- "group",
- 3,
- 3,
- "[Request tag] [Second tag] [First message] [My other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
-
- logger.clear_tags!
- logger.tagged("First message", "My other tag") { logger.info("Some message") }
- expect(Appsignal::Extension).to have_received(:log)
- .with(
- "group",
- 3,
- 3,
- "[First message] [My other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "clearing all tags with clear_tags!" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ allow(Appsignal::Extension).to receive(:log)
+
+ logger.push_tags(["Request tag", "Second tag"])
+ logger.tagged("First message", "My other tag") { logger.info("Some message") }
+ expect(Appsignal::Extension).to have_received(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[Request tag] [Second tag] [First message] [My other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+
+ logger.clear_tags!
+ logger.tagged("First message", "My other tag") { logger.info("Some message") }
+ expect(Appsignal::Extension).to have_received(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[First message] [My other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ allow(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+
+ logger.push_tags(["Request tag", "Second tag"])
+ logger.tagged("First message", "My other tag") { logger.info("Some message") }
+ expect(Appsignal::Logger::OpenTelemetryBackend).to have_received(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[Request tag] [Second tag] [First message] [My other tag] Some message\n",
+ {}
+ )
+
+ logger.clear_tags!
+ logger.tagged("First message", "My other tag") { logger.info("Some message") }
+ expect(Appsignal::Logger::OpenTelemetryBackend).to have_received(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[First message] [My other tag] Some message\n",
+ {}
+ )
+ end
end
- it "accepts tags in #tagged as an array" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "[My tag] [My other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "with tags passed as an array" do
+ def perform
+ logger.tagged(["My tag", "My other tag"]) do
+ logger.info("Some message")
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[My tag] [My other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ perform
+ end
- logger.tagged(["My tag", "My other tag"]) do
- logger.info("Some message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[My tag] [My other tag] Some message\n",
+ {}
+ )
+ perform
end
end
@@ -136,87 +276,191 @@
# is present.
if !DependencyHelper.rails_present? || DependencyHelper.rails7_present?
describe "when calling #tagged without a block" do
- it "returns a new logger with the tags added" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "[My tag] [My other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "returns a new logger with the tags added" do
+ def perform
+ logger.tagged("My tag", "My other tag").info("Some message")
+ end
- logger.tagged("My tag", "My other tag").info("Some message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[My tag] [My other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[My tag] [My other tag] Some message\n",
+ {}
+ )
+ perform
+ end
end
- it "does not modify the original logger" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "[My tag] [My other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "does not modify the original logger" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[My tag] [My other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
- new_logger = logger.tagged("My tag", "My other tag")
- new_logger.info("Some message")
+ new_logger = logger.tagged("My tag", "My other tag")
+ new_logger.info("Some message")
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
- logger.info("Some message")
+ logger.info("Some message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[My tag] [My other tag] Some message\n",
+ {}
+ )
+
+ new_logger = logger.tagged("My tag", "My other tag")
+ new_logger.info("Some message")
+
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "Some message\n",
+ {}
+ )
+
+ logger.info("Some message")
+ end
end
- it "can be chained" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "[My tag] [My other tag] [My third tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "can be chained" do
+ def perform
+ logger.tagged("My tag", "My other tag").tagged("My third tag").info("Some message")
+ end
- logger.tagged("My tag", "My other tag").tagged("My third tag").info("Some message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[My tag] [My other tag] [My third tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[My tag] [My other tag] [My third tag] Some message\n",
+ {}
+ )
+ perform
+ end
end
- it "can be chained before a block invocation" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "[My tag] [My other tag] [My third tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "can be chained before a block invocation" do
+ def perform
+ # Use the logger passed to the block: the logger returned from
+ # the first #tagged invocation is a new instance.
+ logger.tagged("My tag", "My other tag").tagged("My third tag") do |logger|
+ logger.info("Some message")
+ end
+ end
- # We must explicitly use the logger passed to the block,
- # as the logger returned from the first #tagged invocation
- # is a new instance of the logger.
- logger.tagged("My tag", "My other tag").tagged("My third tag") do |logger|
- logger.info("Some message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[My tag] [My other tag] [My third tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[My tag] [My other tag] [My third tag] Some message\n",
+ {}
+ )
+ perform
end
end
- it "can be chained after a block invocation" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "[My tag] [My other tag] [My third tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "can be chained after a block invocation" do
+ def perform
+ logger.tagged("My tag", "My other tag") do
+ logger.tagged("My third tag").info("Some message")
+ end
+ end
- logger.tagged("My tag", "My other tag") do
- logger.tagged("My third tag").info("Some message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[My tag] [My other tag] [My third tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[My tag] [My other tag] [My third tag] Some message\n",
+ {}
+ )
+ perform
end
end
end
@@ -228,258 +472,503 @@
let(:logs) { log_contents(log_stream) }
let(:logger) { Appsignal::Logger.new("group", :level => ::Logger::DEBUG) }
- before do
- Appsignal.internal_logger = test_logger(log_stream)
- end
+ before do
+ Appsignal.internal_logger = test_logger(log_stream)
+ end
+
+ it "should not create a logger with a nil group" do
+ expect do
+ Appsignal::Logger.new(nil)
+ end.to raise_error(TypeError)
+ end
+
+ describe "format validation" do
+ # Constructor-only behaviour, independent of the active backend, so it
+ # should hold identically whether agent or collector mode booted.
+ describe "the documented format constants" do
+ it_in_both_modes do
+ [
+ Appsignal::Logger::PLAINTEXT,
+ Appsignal::Logger::LOGFMT,
+ Appsignal::Logger::JSON,
+ Appsignal::Logger::AUTODETECT
+ ].each do |format|
+ expect(Appsignal.internal_logger).not_to receive(:warn)
+ logger = Appsignal::Logger.new("group", :format => format)
+ expect(logger.instance_variable_get(:@format)).to eq(format)
+ end
+ end
+ end
+
+ describe "an unknown format" do
+ it_in_both_modes do
+ expect(Appsignal.internal_logger).to receive(:warn)
+ .with(/Unknown Appsignal::Logger format 99; falling back to AUTODETECT/)
+
+ logger = Appsignal::Logger.new("group", :format => 99)
+ expect(logger.instance_variable_get(:@format)).to eq(Appsignal::Logger::AUTODETECT)
+ end
+ end
+ end
+
+ describe "#add" do
+ describe "with a level and message" do
+ def perform
+ logger.add(::Logger::INFO, "Log message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "Log message", {})
+ perform
+ end
+ end
+
+ describe "with a non-string message" do
+ def perform
+ logger.add(::Logger::INFO, 123)
+ logger.add(::Logger::INFO, {})
+ logger.add(::Logger::INFO, [])
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", 3, 3, "123", instance_of(Appsignal::Extension::Data))
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", 3, 3, "{}", instance_of(Appsignal::Extension::Data))
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", 3, 3, "[]", instance_of(Appsignal::Extension::Data))
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "123", {})
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "{}", {})
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "[]", {})
+ perform
+ end
+ end
- it "should not create a logger with a nil group" do
- expect do
- Appsignal::Logger.new(nil)
- end.to raise_error(TypeError)
- end
+ describe "with a block" do
+ def perform
+ logger.add(::Logger::INFO) { "Log message" }
+ end
- describe "#add" do
- it "should log with a level and message" do
- expect(Appsignal::Extension).to receive(:log)
- .with("group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
- logger.add(::Logger::INFO, "Log message")
- end
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ perform
+ end
- it "calls #to_s on the message if it is not a string" do
- expect(Appsignal::Extension).to receive(:log)
- .with("group", 3, 3, "123", instance_of(Appsignal::Extension::Data))
- expect(Appsignal::Extension).to receive(:log)
- .with("group", 3, 3, "{}", instance_of(Appsignal::Extension::Data))
- expect(Appsignal::Extension).to receive(:log)
- .with("group", 3, 3, "[]", instance_of(Appsignal::Extension::Data))
- logger.add(::Logger::INFO, 123)
- logger.add(::Logger::INFO, {})
- logger.add(::Logger::INFO, [])
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "Log message", {})
+ perform
+ end
end
- it "should log with a block" do
- expect(Appsignal::Extension).to receive(:log)
- .with("group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
- logger.add(::Logger::INFO) do
- "Log message"
+ describe "with a level, message and group" do
+ def perform
+ logger.add(::Logger::INFO, "Log message", "other_group")
end
- end
- it "should log with a level, message and group" do
- expect(Appsignal::Extension).to receive(:log)
- .with("other_group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
- logger.add(::Logger::INFO, "Log message", "other_group")
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with("other_group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("other_group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "Log message", {})
+ perform
+ end
end
- context "with info log level" do
+ describe "with info log level" do
let(:logger) { Appsignal::Logger.new("group", :level => ::Logger::INFO) }
- it "should skip logging if the level is too low" do
- expect(Appsignal::Extension).not_to receive(:log)
- logger.add(::Logger::DEBUG, "Log message")
+ describe "when the call's level is too low" do
+ def perform
+ logger.add(::Logger::DEBUG, "Log message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).not_to receive(:log)
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).not_to receive(:emit)
+ perform
+ end
end
end
- context "with the PLAINTEXT format set" do
+ describe "with the PLAINTEXT format set" do
let(:logger) { Appsignal::Logger.new("group", :format => Appsignal::Logger::PLAINTEXT) }
- it "should log and pass the format flag" do
+ def perform
+ logger.add(::Logger::INFO, "Log message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
expect(Appsignal::Extension).to receive(:log)
.with("group", 3, 0, "Log message", instance_of(Appsignal::Extension::Data))
- logger.add(::Logger::INFO, "Log message")
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::PLAINTEXT, "Log message", {})
+ perform
end
end
- context "with the logfmt format set" do
+ describe "with the logfmt format set" do
let(:logger) { Appsignal::Logger.new("group", :format => Appsignal::Logger::LOGFMT) }
- it "should log and pass the format flag" do
+ def perform
+ logger.add(::Logger::INFO, "Log message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
expect(Appsignal::Extension).to receive(:log)
.with("group", 3, 1, "Log message", instance_of(Appsignal::Extension::Data))
- logger.add(::Logger::INFO, "Log message")
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::LOGFMT, "Log message", {})
+ perform
end
end
- context "with the JSON format set" do
+ describe "with the JSON format set" do
let(:logger) { Appsignal::Logger.new("group", :format => Appsignal::Logger::JSON) }
- it "should log and pass the format flag" do
+ def perform
+ logger.add(::Logger::INFO, "Log message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
expect(Appsignal::Extension).to receive(:log)
.with("group", 3, 2, "Log message", instance_of(Appsignal::Extension::Data))
- logger.add(::Logger::INFO, "Log message")
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::JSON, "Log message", {})
+ perform
end
end
- context "with a formatter set" do
+ describe "with a formatter set" do
before do
logger.formatter = proc do |_level, _timestamp, _appname, message|
"formatted: '#{message}'"
end
end
- it "should log with a level, message and group" do
- expect(Appsignal::Extension).to receive(:log).with(
- "other_group",
- 3,
- 3,
- "formatted: 'Log message'",
- instance_of(Appsignal::Extension::Data)
- )
- logger.add(::Logger::INFO, "Log message", "other_group")
- end
+ describe "logs with a level, message and group" do
+ def perform
+ logger.add(::Logger::INFO, "Log message", "other_group")
+ end
- it "calls the formatter with the original message" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log).with(
+ "other_group",
3,
3,
- a_string_starting_with("formatted:"),
+ "formatted: 'Log message'",
instance_of(Appsignal::Extension::Data)
)
- expect(logger.formatter).to receive(:call)
- .with(::Logger::INFO, instance_of(Time), "group", { :a => "b" })
- .and_call_original
- logger.add(::Logger::INFO, { :a => "b" })
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit).with(
+ "other_group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "formatted: 'Log message'",
+ {}
+ )
+ perform
+ end
end
- it "calls #to_s on the formatter output if it is not a string" do
- expect(Appsignal::Extension).to receive(:log)
- .with("group", 3, 3, "123", instance_of(Appsignal::Extension::Data))
- expect(logger.formatter).to receive(:call)
- .with(::Logger::INFO, instance_of(Time), "group", 123)
- .and_return(123)
- logger.add(::Logger::INFO, 123)
+ describe "calls the formatter with the original message" do
+ def perform
+ logger.add(::Logger::INFO, { :a => "b" })
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ a_string_starting_with("formatted:"),
+ instance_of(Appsignal::Extension::Data)
+ )
+ expect(logger.formatter).to receive(:call)
+ .with(::Logger::INFO, instance_of(Time), "group", { :a => "b" })
+ .and_call_original
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ a_string_starting_with("formatted:"),
+ {}
+ )
+ expect(logger.formatter).to receive(:call)
+ .with(::Logger::INFO, instance_of(Time), "group", { :a => "b" })
+ .and_call_original
+ perform
+ end
+ end
+
+ describe "calls #to_s on the formatter output if it is not a string" do
+ def perform
+ logger.add(::Logger::INFO, 123)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", 3, 3, "123", instance_of(Appsignal::Extension::Data))
+ expect(logger.formatter).to receive(:call)
+ .with(::Logger::INFO, instance_of(Time), "group", 123)
+ .and_return(123)
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "123", {})
+ expect(logger.formatter).to receive(:call)
+ .with(::Logger::INFO, instance_of(Time), "group", 123)
+ .and_return(123)
+ perform
+ end
end
end
end
describe "#silence" do
- it "calls the given block" do
- num = 1
-
- logger.silence do
- num += 1
+ describe "calls the given block" do
+ it_in_both_modes do
+ num = 1
+ logger.silence { num += 1 }
+ expect(num).to eq(2)
end
-
- expect(num).to eq(2)
- expect(Appsignal::Extension).not_to receive(:log)
end
- it "silences the logger up to, but not including, the given level" do
- # Expect not to receive info
- expect(Appsignal::Extension).not_to receive(:log)
- .with("group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ describe "silences the logger up to, but not including, the given level" do
+ def perform
+ logger.silence(::Logger::WARN) do
+ logger.info("Log message")
+ logger.warn("Log message")
+ end
+ end
- # Expect to receive warn
- expect(Appsignal::Extension).to receive(:log)
- .with("group", 5, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).not_to receive(:log)
+ .with("group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", 5, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ perform
+ end
- logger.silence(::Logger::WARN) do
- logger.info("Log message")
- logger.warn("Log message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).not_to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "Log message", {})
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::WARN, Appsignal::Logger::AUTODETECT, "Log message", {})
+ perform
end
end
- it "silences the logger to error level by default" do
- # Expect not to receive debug, info or warn
- [2, 3, 5].each do |severity|
- expect(Appsignal::Extension).not_to receive(:log)
- .with("group", severity, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ describe "silences the logger to error level by default" do
+ def perform
+ logger.silence do
+ logger.debug("Log message")
+ logger.info("Log message")
+ logger.warn("Log message")
+ logger.error("Log message")
+ logger.fatal("Log message")
+ end
end
- # Expect to receive error and fatal
- [6, 7].each do |severity|
- expect(Appsignal::Extension).to receive(:log)
- .with("group", severity, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ it "in agent mode", :agent_mode do
+ start_agent
+ [2, 3, 5].each do |severity|
+ expect(Appsignal::Extension).not_to receive(:log)
+ .with("group", severity, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ end
+ [6, 7].each do |severity|
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", severity, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ end
+ perform
end
- logger.silence do
- logger.debug("Log message")
- logger.info("Log message")
- logger.warn("Log message")
- logger.error("Log message")
- logger.fatal("Log message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ [::Logger::DEBUG, ::Logger::INFO, ::Logger::WARN].each do |severity|
+ expect(Appsignal::Logger::OpenTelemetryBackend).not_to receive(:emit)
+ .with("group", severity, Appsignal::Logger::AUTODETECT, "Log message", {})
+ end
+ [::Logger::ERROR, ::Logger::FATAL].each do |severity|
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", severity, Appsignal::Logger::AUTODETECT, "Log message", {})
+ end
+ perform
end
end
end
describe "#broadcast_to" do
- it "broadcasts the message to the given logger" do
- other_device = StringIO.new
- other_logger = ::Logger.new(other_device)
-
- logger.broadcast_to(other_logger)
+ describe "broadcasts the message to the given logger" do
+ let(:other_device) { StringIO.new }
+ let(:other_logger) { ::Logger.new(other_device) }
+ before { logger.broadcast_to(other_logger) }
- expect(Appsignal::Extension).to receive(:log)
- .with("group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ def perform
+ logger.info("Log message")
+ expect(other_device.string).to include("INFO -- group: Log message")
+ end
- logger.info("Log message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", 3, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ perform
+ end
- expect(other_device.string).to include("INFO -- group: Log message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "Log message", {})
+ perform
+ end
end
- it "broadcasts the message to the given logger when it's below the log level" do
- logger = Appsignal::Logger.new("group", :level => ::Logger::INFO)
-
- other_device = StringIO.new
- other_logger = ::Logger.new(other_device)
-
- logger.broadcast_to(other_logger)
+ describe "broadcasts the message to the given logger when it's below the log level" do
+ let(:logger) { Appsignal::Logger.new("group", :level => ::Logger::INFO) }
+ let(:other_device) { StringIO.new }
+ let(:other_logger) { ::Logger.new(other_device) }
+ before { logger.broadcast_to(other_logger) }
- expect(Appsignal::Extension).not_to receive(:log)
+ def perform
+ logger.debug("Log message")
+ expect(other_device.string).to include("DEBUG -- group: Log message")
+ end
- logger.debug("Log message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).not_to receive(:log)
+ perform
+ end
- expect(other_device.string).to include("DEBUG -- group: Log message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).not_to receive(:emit)
+ perform
+ end
end
- it "does not broadcast the message to the given logger when silenced" do
- other_device = StringIO.new
- other_logger = ::Logger.new(other_device)
+ describe "does not broadcast the message to the given logger when silenced" do
+ let(:other_device) { StringIO.new }
+ let(:other_logger) { ::Logger.new(other_device) }
+ before { logger.broadcast_to(other_logger) }
- logger.broadcast_to(other_logger)
-
- expect(Appsignal::Extension).not_to receive(:log)
+ def perform
+ logger.silence { logger.info("Log message") }
+ expect(other_device.string).to eq("")
+ end
- logger.silence do
- logger.info("Log message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).not_to receive(:log)
+ perform
end
- expect(other_device.string).to eq("")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).not_to receive(:emit)
+ perform
+ end
end
context "with a formatter" do
- it "sets the formatter on broadcasted loggers that support it" do
- other_device = StringIO.new
- other_logger = ::Logger.new(other_device)
+ describe "sets the formatter on broadcasted loggers that support it" do
+ it_in_both_modes do
+ other_device = StringIO.new
+ other_logger = ::Logger.new(other_device)
+ logger.broadcast_to(other_logger)
- logger.broadcast_to(other_logger)
+ formatter = proc { |_level, _timestamp, _appname, message| "custom: #{message}" }
+ logger.formatter = formatter
- formatter = proc do |_level, _timestamp, _appname, message|
- "custom: #{message}"
+ expect(logger.formatter).to eq(formatter)
+ expect(other_logger.formatter).to eq(formatter)
end
-
- logger.formatter = formatter
-
- expect(logger.formatter).to eq(formatter)
- expect(other_logger.formatter).to eq(formatter)
end
- it "does not raise an error when a broadcasted logger does not support formatter=" do
- logger_without_formatter = double("logger without formatter")
- allow(logger_without_formatter).to receive(:respond_to?).with(:formatter=).and_return(false)
- allow(logger_without_formatter).to receive(:add)
+ describe "does not raise an error when a broadcasted logger does not support formatter=" do
+ it_in_both_modes do
+ logger_without_formatter = double("logger without formatter")
+ allow(logger_without_formatter).to receive(:respond_to?)
+ .with(:formatter=).and_return(false)
+ allow(logger_without_formatter).to receive(:add)
- logger.broadcast_to(logger_without_formatter)
+ logger.broadcast_to(logger_without_formatter)
- formatter = proc do |_level, _timestamp, _appname, message|
- "custom: #{message}"
+ formatter = proc { |_level, _timestamp, _appname, message| "custom: #{message}" }
+ logger.formatter = formatter
+ expect(logger.formatter).to eq(formatter)
end
-
- # Does not raise an error
- logger.formatter = formatter
- expect(logger.formatter).to eq(formatter)
end
end
@@ -494,71 +983,149 @@
ActiveSupport::TaggedLogging.new(appsignal_logger)
end
- it "broadcasts a tagged message to the given logger" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "[My tag] [My other tag] Some message\n",
- Appsignal::Utils::Data.generate({})
- )
+ describe "broadcasts a tagged message to the given logger" do
+ def perform
+ logger.tagged("My tag", "My other tag") do
+ logger.info("Some message")
+ end
+ expect(other_stream.string).to eq("[My tag] [My other tag] Some message\n")
+ end
- logger.tagged("My tag", "My other tag") do
- logger.info("Some message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 3,
+ 3,
+ "[My tag] [My other tag] Some message\n",
+ Appsignal::Utils::Data.generate({})
+ )
+ perform
end
- expect(other_stream.string)
- .to eq("[My tag] [My other tag] Some message\n")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "[My tag] [My other tag] Some message\n",
+ {}
+ )
+ perform
+ end
end
end
end
end
[
- ["debug", 2, ::Logger::INFO],
- ["info", 3, ::Logger::WARN],
- ["warn", 5, ::Logger::ERROR],
- ["error", 6, ::Logger::FATAL],
- ["fatal", 7, nil]
+ ["debug", 2, ::Logger::DEBUG, ::Logger::INFO],
+ ["info", 3, ::Logger::INFO, ::Logger::WARN],
+ ["warn", 5, ::Logger::WARN, ::Logger::ERROR],
+ ["error", 6, ::Logger::ERROR, ::Logger::FATAL],
+ ["fatal", 7, ::Logger::FATAL, nil]
].each do |permutation|
- method, extension_level, higher_level = permutation
+ method, extension_level, logger_level, higher_level = permutation
describe "##{method}" do
- it "should log with a message" do
- expect(Appsignal::Utils::Data).to receive(:generate)
- .with({ :attribute => "value" })
- .and_call_original
- expect(Appsignal::Extension).to receive(:log)
- .with("group", extension_level, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ describe "with a message and attributes" do
+ # `define_method` (rather than `def`) so the block captures the
+ # enclosing closure -- `method` is a block-local of the
+ # `.each do |permutation|` loop and isn't visible from `def`.
+ define_method(:perform) do
+ logger.send(method, "Log message", :attribute => "value")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Utils::Data).to receive(:generate)
+ .with({ :attribute => "value" })
+ .and_call_original
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group", extension_level, 3, "Log message",
+ instance_of(Appsignal::Extension::Data)
+ )
+ perform
+ end
- logger.send(method, "Log message", :attribute => "value")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ logger_level,
+ Appsignal::Logger::AUTODETECT,
+ "Log message",
+ { :attribute => "value" }
+ )
+ perform
+ end
end
- it "should log with a block" do
- expect(Appsignal::Utils::Data).to receive(:generate)
- .with({})
- .and_call_original
- expect(Appsignal::Extension).to receive(:log)
- .with("group", extension_level, 3, "Log message", instance_of(Appsignal::Extension::Data))
+ describe "with a block" do
+ define_method(:perform) do
+ logger.send(method) { "Log message" }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Utils::Data).to receive(:generate)
+ .with({})
+ .and_call_original
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group", extension_level, 3, "Log message",
+ instance_of(Appsignal::Extension::Data)
+ )
+ perform
+ end
- logger.send(method) do
- "Log message"
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", logger_level, Appsignal::Logger::AUTODETECT, "Log message", {})
+ perform
end
end
- it "should return with a nil message" do
- expect(Appsignal::Extension).not_to receive(:log)
- logger.send(method)
+ describe "with a nil message" do
+ define_method(:perform) { logger.send(method) }
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).not_to receive(:log)
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).not_to receive(:emit)
+ perform
+ end
end
if higher_level
context "with a lower log level" do
let(:logger) { Appsignal::Logger.new("group", :level => higher_level) }
- it "should skip logging if the level is too low" do
- expect(Appsignal::Extension).not_to receive(:log)
- logger.send(method, "Log message")
+ describe "skips logging when the level is too low" do
+ define_method(:perform) { logger.send(method, "Log message") }
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).not_to receive(:log)
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).not_to receive(:emit)
+ perform
+ end
end
end
end
@@ -566,108 +1133,212 @@
context "with a formatter set" do
before do
Timecop.freeze(Time.local(2023))
- logger.formatter = logger.formatter = proc do |_level, timestamp, _appname, message|
- # This line replicates the behaviour of the Ruby default Logger::Formatter
- # which expects a timestamp object as a second argument
- # https://github.com/ruby/ruby/blob/master/lib/logger/formatter.rb#L15-L17
+ # The Ruby default Logger::Formatter expects a timestamp object as
+ # the second argument (https://github.com/ruby/ruby/blob/master/lib/logger/formatter.rb#L15-L17).
+ logger.formatter = proc do |_level, timestamp, _appname, message|
time = timestamp.strftime("%Y-%m-%dT%H:%M:%S.%6N")
"formatted: #{time} '#{message}'"
end
end
- after do
- Timecop.return
- end
+ after { Timecop.return }
+
+ describe "logs the formatted message" do
+ define_method(:perform) { logger.send(method, "Log message") }
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ extension_level,
+ 3,
+ "formatted: 2023-01-01T00:00:00.000000 'Log message'",
+ instance_of(Appsignal::Extension::Data)
+ )
+ perform
+ end
- it "should log with a level, message and group" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- extension_level,
- 3,
- "formatted: 2023-01-01T00:00:00.000000 'Log message'",
- instance_of(Appsignal::Extension::Data)
- )
- logger.send(method, "Log message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ logger_level,
+ Appsignal::Logger::AUTODETECT,
+ "formatted: 2023-01-01T00:00:00.000000 'Log message'",
+ {}
+ )
+ perform
+ end
end
end
end
end
describe "a logger with default attributes" do
- it "adds the attributes when a message is logged" do
- logger = Appsignal::Logger.new("group", :attributes => { :some_key => "some_value" })
+ let(:logger) { Appsignal::Logger.new("group", :attributes => { :some_key => "some_value" }) }
- expect(Appsignal::Extension).to receive(:log).with("group", 6, 3, "Some message",
- Appsignal::Utils::Data.generate({ :other_key => "other_value", :some_key => "some_value" }))
- logger.error("Some message", { :other_key => "other_value" })
+ describe "adds the attributes when a message is logged" do
+ def perform
+ logger.error("Some message", { :other_key => "other_value" })
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log).with(
+ "group", 6, 3, "Some message",
+ Appsignal::Utils::Data.generate(
+ { :other_key => "other_value", :some_key => "some_value" }
+ )
+ )
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit).with(
+ "group",
+ ::Logger::ERROR,
+ Appsignal::Logger::AUTODETECT,
+ "Some message",
+ { :other_key => "other_value", :some_key => "some_value" }
+ )
+ perform
+ end
end
- it "does not modify the original attribute hashes passed" do
- default_attributes = { :some_key => "some_value" }
- logger = Appsignal::Logger.new("group", :attributes => default_attributes)
+ describe "does not modify the original attribute hashes passed" do
+ it_in_both_modes do
+ default_attributes = { :some_key => "some_value" }
+ logger = Appsignal::Logger.new("group", :attributes => default_attributes)
- line_attributes = { :other_key => "other_value" }
- logger.error("Some message", line_attributes)
+ line_attributes = { :other_key => "other_value" }
+ logger.error("Some message", line_attributes)
- expect(default_attributes).to eq({ :some_key => "some_value" })
- expect(line_attributes).to eq({ :other_key => "other_value" })
+ expect(default_attributes).to eq({ :some_key => "some_value" })
+ expect(line_attributes).to eq({ :other_key => "other_value" })
+ end
end
- it "prioritises line attributes over default attributes" do
- logger = Appsignal::Logger.new("group", :attributes => { :some_key => "some_value" })
+ describe "prioritises line attributes over default attributes" do
+ def perform
+ logger.error("Some message", { :some_key => "other_value" })
+ end
- expect(Appsignal::Extension).to receive(:log).with("group", 6, 3, "Some message",
- Appsignal::Utils::Data.generate({ :some_key => "other_value" }))
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log).with(
+ "group", 6, 3, "Some message",
+ Appsignal::Utils::Data.generate({ :some_key => "other_value" })
+ )
+ perform
+ end
- logger.error("Some message", { :some_key => "other_value" })
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit).with(
+ "group",
+ ::Logger::ERROR,
+ Appsignal::Logger::AUTODETECT,
+ "Some message",
+ { :some_key => "other_value" }
+ )
+ perform
+ end
end
- it "adds the default attributes when #add is called" do
- logger = Appsignal::Logger.new("group", :attributes => { :some_key => "some_value" })
+ describe "adds the default attributes when #add is called" do
+ def perform
+ logger.add(::Logger::INFO, "Log message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log).with(
+ "group", 3, 3, "Log message",
+ Appsignal::Utils::Data.generate({ :some_key => "some_value" })
+ )
+ perform
+ end
- expect(Appsignal::Extension).to receive(:log).with("group", 3, 3, "Log message",
- Appsignal::Utils::Data.generate({ :some_key => "some_value" }))
- logger.add(::Logger::INFO, "Log message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit).with(
+ "group",
+ ::Logger::INFO,
+ Appsignal::Logger::AUTODETECT,
+ "Log message",
+ { :some_key => "some_value" }
+ )
+ perform
+ end
end
end
describe "#error with exception object" do
- it "logs the exception class and its message" do
- error =
- begin
- raise ExampleStandardError, "oh no!"
- rescue => e
- # This makes the exception include a backtrace, so we can assert its
- # first line is included
- e
- end
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 6,
- 3,
- a_string_matching(/ExampleStandardError: oh no! \(.*logger_spec.rb.*\)/),
- instance_of(Appsignal::Extension::Data)
- )
- logger.error(error)
+ describe "logs the exception class and its message" do
+ let(:error) do
+ raise ExampleStandardError, "oh no!"
+ rescue => e
+ # Re-raise capture so the exception carries a backtrace, letting
+ # us assert that its first line is part of the logged string.
+ e
+ end
+
+ def perform
+ logger.error(error)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with(
+ "group",
+ 6,
+ 3,
+ a_string_matching(/ExampleStandardError: oh no! \(.*logger_spec.rb.*\)/),
+ instance_of(Appsignal::Extension::Data)
+ )
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with(
+ "group",
+ ::Logger::ERROR,
+ Appsignal::Logger::AUTODETECT,
+ a_string_matching(/ExampleStandardError: oh no! \(.*logger_spec.rb.*\)/),
+ {}
+ )
+ perform
+ end
end
end
describe "#<<" do
- it "writes an info message and returns the number of characters written" do
- expect(Appsignal::Extension).to receive(:log)
- .with(
- "group",
- 3,
- 3,
- "hello there",
- instance_of(Appsignal::Extension::Data)
- )
+ describe "writes an info message and returns the number of characters written" do
+ def perform
+ message = "hello there"
+ result = logger << message
+ expect(result).to eq(message.length)
+ end
- message = "hello there"
- result = logger << message
- expect(result).to eq(message.length)
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log)
+ .with("group", 3, 3, "hello there", instance_of(Appsignal::Extension::Data))
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit)
+ .with("group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "hello there", {})
+ perform
+ end
end
context "with a formatter set" do
@@ -677,18 +1348,29 @@
end
end
- # This documents how the logger currently behaves in this scenario.
- # Normally a Ruby logger would ignore the logger.
- # We would recommend not setting a logger on the AppSignal logger.
- it "logs a formatted message" do
- expect(Appsignal::Extension).to receive(:log).with(
- "group",
- 3,
- 3,
- "formatted: 'Log message'",
- instance_of(Appsignal::Extension::Data)
- )
- logger << "Log message"
+ # Documents how the logger currently behaves: a Ruby logger would
+ # normally bypass the formatter for `<<`. We recommend against setting
+ # a formatter on the AppSignal logger.
+ describe "logs a formatted message" do
+ def perform
+ logger << "Log message"
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:log).with(
+ "group", 3, 3, "formatted: 'Log message'", instance_of(Appsignal::Extension::Data)
+ )
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(Appsignal::Logger::OpenTelemetryBackend).to receive(:emit).with(
+ "group", ::Logger::INFO, Appsignal::Logger::AUTODETECT, "formatted: 'Log message'", {}
+ )
+ perform
+ end
end
end
end
diff --git a/spec/lib/appsignal/metrics/opentelemetry_backend_spec.rb b/spec/lib/appsignal/metrics/opentelemetry_backend_spec.rb
new file mode 100644
index 000000000..a8aa30b91
--- /dev/null
+++ b/spec/lib/appsignal/metrics/opentelemetry_backend_spec.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require "opentelemetry/sdk" if DependencyHelper.opentelemetry_present?
+require "opentelemetry-metrics-sdk" if DependencyHelper.opentelemetry_present?
+
+describe Appsignal::Metrics::OpenTelemetryBackend, :if => DependencyHelper.opentelemetry_present? do
+ let(:exporter) { ::OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new }
+ let(:meter_provider) do
+ provider = ::OpenTelemetry::SDK::Metrics::MeterProvider.new
+ provider.add_metric_reader(exporter)
+ provider
+ end
+
+ before do
+ ::OpenTelemetry.meter_provider = meter_provider
+ described_class.reset!
+ end
+
+ after { described_class.reset! }
+
+ def collect_snapshots
+ exporter.pull
+ snapshots = exporter.metric_snapshots.dup
+ exporter.reset
+ snapshots
+ end
+
+ def snapshot_for(name)
+ collect_snapshots.find { |snapshot| snapshot.name == name }
+ end
+
+ describe ".set_gauge" do
+ it "emits a Gauge snapshot with the recorded value and attributes" do
+ described_class.set_gauge("my_gauge", 42.5, { :host => "node-1" })
+
+ snapshot = snapshot_for("my_gauge")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.instrument_kind).to eq(:gauge)
+ expect(snapshot.data_points.first.value).to eq(42.5)
+ expect(snapshot.data_points.first.attributes).to eq("host" => "node-1")
+ end
+
+ it "coerces integer values to float" do
+ described_class.set_gauge("my_gauge", 10, {})
+
+ snapshot = snapshot_for("my_gauge")
+ expect(snapshot.data_points.first.value).to eq(10.0)
+ end
+ end
+
+ describe ".increment_counter" do
+ it "emits an UpDownCounter snapshot whose sum tracks repeated calls" do
+ described_class.increment_counter("my_counter", 1, { :endpoint => "/" })
+ described_class.increment_counter("my_counter", 3, { :endpoint => "/" })
+
+ snapshot = snapshot_for("my_counter")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.instrument_kind).to eq(:up_down_counter)
+ expect(snapshot.data_points.first.value).to eq(4.0)
+ expect(snapshot.data_points.first.attributes).to eq("endpoint" => "/")
+ end
+
+ it "accepts negative increments" do
+ described_class.increment_counter("my_counter", -5, {})
+
+ snapshot = snapshot_for("my_counter")
+ expect(snapshot.data_points.first.value).to eq(-5.0)
+ end
+ end
+
+ describe ".add_distribution_value" do
+ it "emits a Histogram snapshot capturing the recorded values" do
+ described_class.add_distribution_value("my_distribution", 0.1, { :route => "/login" })
+ described_class.add_distribution_value("my_distribution", 0.2, { :route => "/login" })
+
+ snapshot = snapshot_for("my_distribution")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.instrument_kind).to eq(:histogram)
+ data_point = snapshot.data_points.first
+ expect(data_point.count).to eq(2)
+ expect(data_point.sum).to be_within(0.0001).of(0.3)
+ expect(data_point.attributes).to eq("route" => "/login")
+ end
+ end
+
+ describe "attribute coercion" do
+ it "stringifies symbol keys and symbol values, preserves primitives" do
+ described_class.set_gauge(
+ "my_gauge",
+ 1.0,
+ {
+ :string => "value",
+ "symbol" => :sym,
+ :integer => 42,
+ :float => 1.5,
+ :truthy => true,
+ :falsy => false
+ }
+ )
+
+ attrs = snapshot_for("my_gauge").data_points.first.attributes
+ expect(attrs).to eq(
+ "string" => "value",
+ "symbol" => "sym",
+ "integer" => 42,
+ "float" => 1.5,
+ "truthy" => true,
+ "falsy" => false
+ )
+ end
+
+ it "coerces other tag value types via to_s" do
+ described_class.set_gauge("my_gauge", 1.0, { :time => Time.utc(2026, 1, 2, 3, 4, 5) })
+
+ attrs = snapshot_for("my_gauge").data_points.first.attributes
+ expect(attrs["time"]).to eq("2026-01-02 03:04:05 UTC")
+ end
+
+ it "treats an empty tags hash as no attributes" do
+ described_class.increment_counter("my_counter", 1, {})
+
+ attrs = snapshot_for("my_counter").data_points.first.attributes
+ expect(attrs).to eq({})
+ end
+ end
+
+ describe "instrument caching" do
+ it "reuses the same instrument across calls for a given metric name" do
+ meter = ::OpenTelemetry.meter_provider.meter("appsignal-helpers")
+ expect(meter).to receive(:create_up_down_counter).once.and_call_original
+
+ described_class.increment_counter("cached_counter", 1, {})
+ described_class.increment_counter("cached_counter", 1, {})
+ end
+
+ it "uses the 'appsignal-helpers' meter scope name" do
+ described_class.set_gauge("scoped_gauge", 1.0, {})
+
+ snapshot = snapshot_for("scoped_gauge")
+ expect(snapshot.instrumentation_scope.name).to eq("appsignal-helpers")
+ end
+ end
+end
diff --git a/spec/lib/appsignal/opentelemetry_spec.rb b/spec/lib/appsignal/opentelemetry_spec.rb
new file mode 100644
index 000000000..c35e212be
--- /dev/null
+++ b/spec/lib/appsignal/opentelemetry_spec.rb
@@ -0,0 +1,340 @@
+# frozen_string_literal: true
+
+# The configure/shutdown/started behavior is gated on Ruby 3.1+ (the OTel
+# SDK ships fork hooks via Process._fork). On older Rubies these unit
+# specs are skipped; the config-level gate is covered in `config_spec`.
+if DependencyHelper.opentelemetry_present?
+ require "opentelemetry/sdk"
+ require "opentelemetry-metrics-sdk"
+ require "opentelemetry-logs-sdk"
+
+ describe Appsignal::OpenTelemetry do
+ let(:config) do
+ build_config(
+ :options => {
+ :name => "collector-mode-spec",
+ :push_api_key => "abc",
+ :collector_endpoint => "http://127.0.0.1:9090"
+ }
+ )
+ end
+
+ before { described_class.reset! }
+ after { described_class.reset! }
+
+ describe ".configure" do
+ context "on success" do
+ it "sets started? to true" do
+ described_class.configure(config)
+
+ expect(described_class.started?).to be(true)
+ end
+
+ it "installs meter and logger providers on the global ::OpenTelemetry" do
+ described_class.configure(config)
+
+ expect(::OpenTelemetry.meter_provider)
+ .to be_a(::OpenTelemetry::SDK::Metrics::MeterProvider)
+ expect(::OpenTelemetry.logger_provider)
+ .to be_a(::OpenTelemetry::SDK::Logs::LoggerProvider)
+ end
+
+ it "uses the same merged resource (AppSignal + SDK defaults) for all providers" do
+ described_class.configure(config)
+
+ tracer_attrs = resource_attrs(::OpenTelemetry.tracer_provider.resource)
+ meter_attrs = resource_attrs(::OpenTelemetry.meter_provider.resource)
+ # LoggerProvider doesn't expose a public `resource` accessor; read
+ # the instance variable directly. Switch to a public method if/when
+ # the OTel logs SDK exposes one.
+ logger_attrs = resource_attrs(
+ ::OpenTelemetry.logger_provider.instance_variable_get(:@resource)
+ )
+
+ expect(tracer_attrs).to eq(meter_attrs)
+ expect(tracer_attrs).to eq(logger_attrs)
+
+ # AppSignal attrs are present.
+ expect(meter_attrs["appsignal.config.name"]).to eq("collector-mode-spec")
+ # SDK default attrs survived the merge.
+ expect(meter_attrs["telemetry.sdk.name"]).to eq("opentelemetry")
+ expect(meter_attrs["telemetry.sdk.language"]).to eq("ruby")
+ end
+ end
+
+ context "when an SDK gem can't be loaded" do
+ let(:err_stream) { std_stream }
+
+ it "logs the error, doesn't raise, and leaves started? false" do
+ allow(described_class).to receive(:require)
+ .with("opentelemetry/sdk")
+ .and_raise(LoadError, "fake load failure")
+
+ logs =
+ capture_logs do
+ capture_std_streams(std_stream, err_stream) do
+ expect { described_class.configure(config) }.not_to raise_error
+ end
+ end
+
+ expect(described_class.started?).to be(false)
+ expect(logs).to include("Cannot configure OpenTelemetry SDK")
+ expect(logs).to include("fake load failure")
+ expect(err_stream.read).to include("appsignal ERROR")
+ end
+ end
+
+ context "when SDK setup raises a non-LoadError" do
+ let(:err_stream) { std_stream }
+
+ it "logs the error, doesn't raise, and leaves started? false" do
+ allow(::OpenTelemetry::SDK).to receive(:configure)
+ .and_raise(RuntimeError, "boom")
+
+ logs =
+ capture_logs do
+ capture_std_streams(std_stream, err_stream) do
+ expect { described_class.configure(config) }.not_to raise_error
+ end
+ end
+
+ expect(described_class.started?).to be(false)
+ expect(logs).to include("Error configuring OpenTelemetry SDK")
+ expect(logs).to include("boom")
+ expect(err_stream.read).to include("appsignal ERROR")
+ end
+ end
+
+ describe "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" do
+ before { ENV.delete("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE") }
+ after { ENV.delete("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE") }
+
+ it "defaults to 'delta' when unset" do
+ described_class.configure(config)
+
+ expect(ENV.fetch("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"))
+ .to eq("delta")
+ end
+
+ it "preserves a user-set value" do
+ ENV["OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"] = "cumulative"
+
+ described_class.configure(config)
+
+ expect(ENV.fetch("OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"))
+ .to eq("cumulative")
+ end
+ end
+
+ describe "endpoint normalization" do
+ it "strips trailing slashes before appending the OTLP path" do
+ trailing = build_config(
+ :options => {
+ :name => "collector-mode-spec",
+ :push_api_key => "abc",
+ :collector_endpoint => "http://127.0.0.1:9090//"
+ }
+ )
+ # Capture the endpoint each OTLP exporter is constructed with so we
+ # can prove the slashes were stripped before "/v1/" was
+ # appended. The SDK may construct exporters of its own without
+ # passing :endpoint (it falls back to env vars in that case), so we
+ # only assert on the endpoints we explicitly pass through.
+ endpoints = []
+ [
+ ::OpenTelemetry::Exporter::OTLP::Exporter,
+ ::OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter,
+ ::OpenTelemetry::Exporter::OTLP::Logs::LogsExporter
+ ].each do |klass|
+ allow(klass).to receive(:new).and_wrap_original do |original, **kwargs|
+ endpoints << kwargs[:endpoint] if kwargs[:endpoint]
+ original.call(**kwargs)
+ end
+ end
+
+ described_class.configure(trailing)
+
+ expect(endpoints).to contain_exactly(
+ "http://127.0.0.1:9090/v1/traces",
+ "http://127.0.0.1:9090/v1/metrics",
+ "http://127.0.0.1:9090/v1/logs"
+ )
+ end
+ end
+ end
+
+ describe ".started?" do
+ it "is false before configure has been called" do
+ expect(described_class.started?).to be(false)
+ end
+
+ it "is true after a successful configure" do
+ described_class.configure(config)
+
+ expect(described_class.started?).to be(true)
+ end
+
+ it "is reset! back to false on demand" do
+ described_class.configure(config)
+ described_class.reset!
+
+ expect(described_class.started?).to be(false)
+ end
+ end
+
+ describe ".shutdown" do
+ it "is a no-op when not started" do
+ # No SDK is wired up; the API-gem proxy providers raise on shutdown.
+ # The guard in shutdown should short-circuit before touching them.
+ expect { described_class.shutdown }.not_to raise_error
+ end
+
+ it "calls shutdown on all three providers when started" do
+ described_class.configure(config)
+
+ expect(::OpenTelemetry.tracer_provider).to receive(:shutdown)
+ expect(::OpenTelemetry.meter_provider).to receive(:shutdown)
+ expect(::OpenTelemetry.logger_provider).to receive(:shutdown)
+
+ described_class.shutdown
+ end
+
+ it "logs and swallows errors raised by a provider's shutdown" do
+ described_class.configure(config)
+
+ allow(::OpenTelemetry.meter_provider).to receive(:shutdown)
+ .and_raise(RuntimeError, "meter shutdown failed")
+
+ logs = capture_logs { expect { described_class.shutdown }.not_to raise_error }
+
+ expect(logs).to include("Error shutting down OpenTelemetry SDK")
+ expect(logs).to include("meter shutdown failed")
+ end
+ end
+
+ describe ".extract_rack_context" do
+ let(:env) do
+ { "HTTP_TRACEPARENT" => "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" }
+ end
+
+ it "returns nil when the SDK has not booted" do
+ expect(described_class.started?).to be(false)
+ expect(described_class.extract_rack_context(env)).to be_nil
+ end
+
+ it "extracts from the env with the Rack getter when started" do
+ require "opentelemetry-common"
+ allow(described_class).to receive(:started?).and_return(true)
+
+ expect(::OpenTelemetry.propagation).to receive(:extract)
+ .with(env, :getter => ::OpenTelemetry::Common::Propagation.rack_env_getter)
+
+ described_class.extract_rack_context(env)
+ end
+ end
+
+ describe ".if_started" do
+ it "does not run the block and returns nil when the SDK has not booted" do
+ expect(described_class.started?).to be(false)
+
+ ran = false
+ result = described_class.if_started { ran = true }
+
+ expect(ran).to be(false)
+ expect(result).to be_nil
+ end
+
+ it "runs the block and returns its result when started" do
+ allow(described_class).to receive(:started?).and_return(true)
+
+ expect(described_class.if_started { :value }).to eq(:value)
+ end
+ end
+
+ describe ".build_resource" do
+ it "maps AppSignal config attributes onto the resource" do
+ resource = described_class.build_resource(
+ build_config(
+ :options => {
+ :name => "my-app",
+ :push_api_key => "abc",
+ :revision => "deadbeef",
+ :hostname => "host-1",
+ :service_name => "my-service",
+ :filter_attributes => ["password"],
+ :ignore_actions => ["IgnoredController#action"]
+ }
+ )
+ )
+ attrs = resource_attrs(resource)
+
+ expect(attrs["appsignal.config.name"]).to eq("my-app")
+ expect(attrs["appsignal.config.push_api_key"]).to eq("abc")
+ expect(attrs["appsignal.config.revision"]).to eq("deadbeef")
+ expect(attrs["appsignal.config.language_integration"]).to eq("ruby")
+ expect(attrs["service.name"]).to eq("my-service")
+ expect(attrs["host.name"]).to eq("host-1")
+ expect(attrs["appsignal.config.filter_attributes"]).to eq(["password"])
+ expect(attrs["appsignal.config.ignore_actions"])
+ .to eq(["IgnoredController#action"])
+ end
+
+ it "falls back to 'unknown' for empty revision, service_name, and hostname" do
+ # Other specs in the suite set `ENV["APP_REVISION"]` without clearing
+ # it (the spec_helper before-block only resets APPSIGNAL_* and
+ # _APPSIGNAL_* prefixed vars). Clear it locally so this test is
+ # robust to spec ordering.
+ ENV.delete("APP_REVISION")
+
+ resource = described_class.build_resource(
+ build_config(
+ :options => {
+ :name => "my-app",
+ :push_api_key => "abc",
+ :revision => nil,
+ :service_name => nil,
+ :hostname => nil
+ }
+ )
+ )
+ attrs = resource_attrs(resource)
+
+ expect(attrs["appsignal.config.revision"]).to eq("unknown")
+ expect(attrs["service.name"]).to eq("unknown")
+ expect(attrs["host.name"]).to eq("unknown")
+ end
+
+ it "omits attributes whose underlying option is nil or empty" do
+ resource = described_class.build_resource(
+ build_config(
+ :options => {
+ :name => "my-app",
+ :push_api_key => "abc"
+ }
+ )
+ )
+ attrs = resource_attrs(resource)
+
+ # These all default to nil or [] and should be dropped so the
+ # collector can apply its own defaults.
+ %w[
+ appsignal.config.filter_function_parameters
+ appsignal.config.filter_request_query_parameters
+ appsignal.config.ignore_errors
+ appsignal.config.response_headers
+ appsignal.config.send_function_parameters
+ appsignal.config.send_request_query_parameters
+ appsignal.config.send_request_payload
+ ].each do |key|
+ expect(attrs).not_to have_key(key)
+ end
+ end
+ end
+
+ # Pull the attributes out of an OTel Resource as a plain hash so specs
+ # can assert on them without touching the SDK's internals.
+ def resource_attrs(resource)
+ resource.attribute_enumerator.to_h
+ end
+ end
+end
diff --git a/spec/lib/appsignal/probes/gvl_spec.rb b/spec/lib/appsignal/probes/gvl_spec.rb
index 8a9341525..690fba4c3 100644
--- a/spec/lib/appsignal/probes/gvl_spec.rb
+++ b/spec/lib/appsignal/probes/gvl_spec.rb
@@ -21,51 +21,122 @@ def gauges_for(metric)
end
end
- after { FakeGVLTools.reset }
-
- it "gauges the global timer delta" do
- FakeGVLTools::GlobalTimer.monotonic_time = 100_000_000
- probe.call
-
- expect(gauges_for("gvl_global_timer")).to be_empty
+ # A probe wired to the real Appsignal so `set_gauge` routes through the OTel
+ # metrics backend (collector mode) instead of the in-memory AppsignalMock.
+ def collector_probe
+ described_class.new(:appsignal => Appsignal, :gvl_tools => FakeGVLTools)
+ end
- FakeGVLTools::GlobalTimer.monotonic_time = 300_000_000
- probe.call
+ # Assert the collector-mode counterpart of the agent-mode two-entry gauge: the
+ # probe emits each metric twice, once tagged with the process and once with
+ # only the hostname. With the real Appsignal the hostname is the host's own,
+ # so it is only checked for presence.
+ def expect_dual_gauge_points(name, value, process_name:)
+ snapshot = metric_snapshot(name)
+ expect(snapshot).not_to be_nil
+ expect(snapshot.instrument_kind).to eq(:gauge)
+ expect(snapshot.data_points.size).to eq(2)
+ expect(snapshot.data_points.map(&:value)).to all(eq(value))
+ expect_process_tag_split(snapshot, process_name)
+ end
- expect(gauges_for("gvl_global_timer")).to eq [
- [200, {
- :hostname => hostname,
- :process_name => "rspec",
- :process_id => Process.pid
- }],
- [200, { :hostname => hostname }]
- ]
+ def expect_process_tag_split(snapshot, process_name)
+ with_process = snapshot.data_points.find { |point| point.attributes.key?("process_name") }
+ expect(with_process).not_to be_nil
+ expect(with_process.attributes).to include(
+ "process_name" => process_name,
+ "process_id" => Process.pid,
+ "hostname" => kind_of(String)
+ )
+
+ without_process = snapshot.data_points.find { |point| !point.attributes.key?("process_name") }
+ expect(without_process).not_to be_nil
+ expect(without_process.attributes.keys).to eq(["hostname"])
end
- context "when the delta is negative" do
- it "does not gauge the global timer delta" do
+ after { FakeGVLTools.reset }
+
+ describe "the global timer delta gauge" do
+ def perform(probe)
+ FakeGVLTools::GlobalTimer.monotonic_time = 100_000_000
+ probe.call
FakeGVLTools::GlobalTimer.monotonic_time = 300_000_000
probe.call
+ end
- expect(gauges_for("gvl_global_timer")).to be_empty
-
- FakeGVLTools::GlobalTimer.monotonic_time = 0
- probe.call
+ it "in agent mode", :agent_mode do
+ start_agent
+ # The two-entry match also proves the first call emits nothing: a gauge
+ # on the first call would add a third entry.
+ perform(probe)
- expect(gauges_for("gvl_global_timer")).to be_empty
+ expect(gauges_for("gvl_global_timer")).to eq [
+ [200, {
+ :hostname => hostname,
+ :process_name => "rspec",
+ :process_id => Process.pid
+ }],
+ [200, { :hostname => hostname }]
+ ]
end
- end
- context "when the delta is zero" do
- it "does not gauge the global timer delta" do
- FakeGVLTools::GlobalTimer.monotonic_time = 300_000_000
- probe.call
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
- expect(gauges_for("gvl_global_timer")).to be_empty
+ # The probe emits the gauge twice: once tagged with the process, once
+ # with only the hostname. Asserting exactly two points also proves the
+ # first call emitted nothing.
+ expect_dual_gauge_points("gvl_global_timer", 200, :process_name => "rspec")
+ end
+ end
- probe.call
+ context "when the delta is negative" do
+ describe "does not gauge the global timer delta" do
+ def perform(probe)
+ FakeGVLTools::GlobalTimer.monotonic_time = 300_000_000
+ probe.call
+ FakeGVLTools::GlobalTimer.monotonic_time = 0
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+
+ expect(gauges_for("gvl_global_timer")).to be_empty
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+
+ expect(metric_snapshot("gvl_global_timer")).to be_nil
+ end
+ end
+ end
- expect(gauges_for("gvl_global_timer")).to be_empty
+ context "when the delta is zero" do
+ describe "does not gauge the global timer delta" do
+ def perform(probe)
+ FakeGVLTools::GlobalTimer.monotonic_time = 300_000_000
+ probe.call
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+
+ expect(gauges_for("gvl_global_timer")).to be_empty
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+
+ expect(metric_snapshot("gvl_global_timer")).to be_nil
+ end
end
end
@@ -74,18 +145,32 @@ def gauges_for(metric)
FakeGVLTools::WaitingThreads.enabled = true
end
- it "gauges the waiting threads count" do
- FakeGVLTools::WaitingThreads.count = 3
- probe.call
-
- expect(gauges_for("gvl_waiting_threads")).to eq [
- [3, {
- :hostname => hostname,
- :process_name => "rspec",
- :process_id => Process.pid
- }],
- [3, { :hostname => hostname }]
- ]
+ describe "the waiting threads count gauge" do
+ def perform(probe)
+ FakeGVLTools::WaitingThreads.count = 3
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+
+ expect(gauges_for("gvl_waiting_threads")).to eq [
+ [3, {
+ :hostname => hostname,
+ :process_name => "rspec",
+ :process_id => Process.pid
+ }],
+ [3, { :hostname => hostname }]
+ ]
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+
+ expect_dual_gauge_points("gvl_waiting_threads", 3, :process_name => "rspec")
+ end
end
end
@@ -94,71 +179,130 @@ def gauges_for(metric)
FakeGVLTools::WaitingThreads.enabled = false
end
- it "does not gauge the waiting threads count" do
- FakeGVLTools::WaitingThreads.count = 3
- probe.call
+ describe "does not gauge the waiting threads count" do
+ def perform(probe)
+ FakeGVLTools::WaitingThreads.count = 3
+ probe.call
+ end
- expect(gauges_for("gvl_waiting_threads")).to be_empty
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+
+ expect(gauges_for("gvl_waiting_threads")).to be_empty
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+
+ expect(metric_snapshot("gvl_waiting_threads")).to be_nil
+ end
end
end
context "when the process name is a custom value" do
before do
FakeGVLTools::WaitingThreads.enabled = true
- end
-
- it "uses only the first word as the process name" do
+ # Set before the probe is built: the probe reads the process name at
+ # initialization, and the lazy `probe`/`collector_probe` is created in the
+ # example body after this hook runs.
$PROGRAM_NAME = "sidekiq 7.1.6 app [0 of 5 busy]"
- probe.call
+ end
- expect(gauges_for("gvl_waiting_threads")).to eq [
- [0, {
- :hostname => hostname,
- :process_name => "sidekiq",
- :process_id => Process.pid
- }],
- [0, { :hostname => hostname }]
- ]
+ describe "uses only the first word as the process name" do
+ def perform(probe)
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+
+ expect(gauges_for("gvl_waiting_threads")).to eq [
+ [0, {
+ :hostname => hostname,
+ :process_name => "sidekiq",
+ :process_id => Process.pid
+ }],
+ [0, { :hostname => hostname }]
+ ]
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+
+ expect_dual_gauge_points("gvl_waiting_threads", 0, :process_name => "sidekiq")
+ end
end
end
context "when the process name is a path" do
before do
FakeGVLTools::WaitingThreads.enabled = true
- end
-
- it "uses only the binary name as the process name" do
$PROGRAM_NAME = "/foo/folder with spaces/bin/rails"
- probe.call
+ end
- expect(gauges_for("gvl_waiting_threads")).to eq [
- [0, {
- :hostname => hostname,
- :process_name => "rails",
- :process_id => Process.pid
- }],
- [0, { :hostname => hostname }]
- ]
+ describe "uses only the binary name as the process name" do
+ def perform(probe)
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+
+ expect(gauges_for("gvl_waiting_threads")).to eq [
+ [0, {
+ :hostname => hostname,
+ :process_name => "rails",
+ :process_id => Process.pid
+ }],
+ [0, { :hostname => hostname }]
+ ]
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+
+ expect_dual_gauge_points("gvl_waiting_threads", 0, :process_name => "rails")
+ end
end
end
context "when the process name is an empty string" do
before do
FakeGVLTools::WaitingThreads.enabled = true
- end
-
- it "uses [unknown process] as the process name" do
$PROGRAM_NAME = ""
- probe.call
+ end
- expect(gauges_for("gvl_waiting_threads")).to eq [
- [0, {
- :hostname => hostname,
- :process_name => "[unknown process]",
- :process_id => Process.pid
- }],
- [0, { :hostname => hostname }]
- ]
+ describe "uses [unknown process] as the process name" do
+ def perform(probe)
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+
+ expect(gauges_for("gvl_waiting_threads")).to eq [
+ [0, {
+ :hostname => hostname,
+ :process_name => "[unknown process]",
+ :process_id => Process.pid
+ }],
+ [0, { :hostname => hostname }]
+ ]
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+
+ expect_dual_gauge_points("gvl_waiting_threads", 0, :process_name => "[unknown process]")
+ end
end
end
end
diff --git a/spec/lib/appsignal/probes/mri_spec.rb b/spec/lib/appsignal/probes/mri_spec.rb
index 90d584aaa..121b4341a 100644
--- a/spec/lib/appsignal/probes/mri_spec.rb
+++ b/spec/lib/appsignal/probes/mri_spec.rb
@@ -25,116 +25,300 @@
allow(GC::Profiler).to receive(:enabled?).and_return(true)
end
- it "should track vm cache metrics" do
- probe.call
+ # The two metric tags depend on the Ruby version.
+ def vm_cache_metrics
if DependencyHelper.ruby_3_2_or_newer?
- expect_gauge_value("ruby_vm", :tags => { :metric => :constant_cache_invalidations })
- expect_gauge_value("ruby_vm", :tags => { :metric => :constant_cache_misses })
+ [:constant_cache_invalidations, :constant_cache_misses]
else
- expect_gauge_value("ruby_vm", :tags => { :metric => :class_serial })
- expect_gauge_value("ruby_vm", :tags => { :metric => :global_constant_state })
+ [:class_serial, :global_constant_state]
end
end
- it "tracks thread counts" do
- probe.call
- expect_gauge_value("thread_count")
+ describe "the vm cache gauges" do
+ def perform(probe)
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ vm_cache_metrics.each do |metric|
+ expect_gauge_value("ruby_vm", :tags => { :metric => metric })
+ end
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+
+ snapshots = metric_snapshots
+ vm_cache_metrics.each do |metric|
+ point = find_gauge_point(snapshots, "ruby_vm", :metric => metric)
+ expect(point.value).to be_a(Numeric)
+ end
+ end
+ end
+
+ describe "the thread count gauge" do
+ def perform(probe)
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ expect_gauge_value("thread_count")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+
+ snapshot = metric_snapshot("thread_count")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.instrument_kind).to eq(:gauge)
+ data_point = snapshot.data_points.first
+ expect(data_point.value).to be_a(Numeric)
+ expect(data_point.attributes).to include("hostname" => kind_of(String))
+ end
end
- it "tracks GC time between measurements" do
- expect(gc_profiler_mock).to receive(:total_time).and_return(10, 15)
- probe.call
- probe.call
- expect_gauge_value("gc_time", 5)
+ describe "the gc time gauge" do
+ # The gauge reports the delta between measurements, so call twice.
+ def perform(probe)
+ expect(gc_profiler_mock).to receive(:total_time).and_return(10, 15)
+ probe.call
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ expect_gauge_value("gc_time", 5)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+ expect(find_gauge_point(metric_snapshots, "gc_time").value).to eq(5)
+ end
end
context "when GC total time overflows" do
- it "skips one report" do
- expect(gc_profiler_mock).to receive(:total_time).and_return(10, 15, 0, 10)
- probe.call # Normal call, create a cache
- probe.call # Report delta value based on cached value
- probe.call # The value overflows and reports no value. Then stores 0 in the cache
- probe.call # Report new value based on cache of 0
- expect_gauges([["gc_time", 5], ["gc_time", 10]])
+ describe "skips one report" do
+ def perform(probe)
+ expect(gc_profiler_mock).to receive(:total_time).and_return(10, 15, 0, 10)
+ probe.call # Normal call, create a cache
+ probe.call # Report delta value based on cached value
+ probe.call # The value overflows and reports no value. Then stores 0 in the cache
+ probe.call # Report new value based on cache of 0
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ expect_gauges([["gc_time", 5], ["gc_time", 10]])
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+ # An OTel gauge keeps only its last value, so assert the final
+ # post-overflow value (10) rather than the agent's [5, 10] sequence.
+ # This still confirms the metric is emitted through the overflow.
+ expect(find_gauge_point(metric_snapshots, "gc_time").value).to eq(10)
+ end
end
end
context "when GC profiling is disabled" do
- it "does not report a gc_time metric" do
- allow(GC::Profiler).to receive(:enabled?).and_return(false)
- expect(gc_profiler_mock).to_not receive(:total_time)
- probe.call # Normal call, create a cache
- probe.call # Report delta value based on cached value
- metrics = appsignal_mock.gauges.map { |(key)| key }
- expect(metrics).to_not include("gc_time")
- end
-
- it "does not report a gc_time metric while temporarily disabled" do
- # While enabled
- allow(GC::Profiler).to receive(:enabled?).and_return(true)
- expect(gc_profiler_mock).to receive(:total_time).and_return(10, 15)
- probe.call # Normal call, create a cache
- probe.call # Report delta value based on cached value
- expect_gauges([["gc_time", 5]])
+ describe "the gc time gauge" do
+ def perform(probe)
+ allow(GC::Profiler).to receive(:enabled?).and_return(false)
+ expect(gc_profiler_mock).to_not receive(:total_time)
+ probe.call # Normal call, create a cache
+ probe.call # Report delta value based on cached value
+ end
- # While disabled
- allow(GC::Profiler).to receive(:enabled?).and_return(false)
- probe.call # Call twice to make sure any caches resets wouldn't mess up the assertion
- probe.call
- # Does not include any newly reported metrics
- expect_gauges([["gc_time", 5]])
+ it "does not report a gc_time metric in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ metrics = appsignal_mock.gauges.map { |(key)| key }
+ expect(metrics).to_not include("gc_time")
+ end
- # When enabled after being disabled for a while, it only reports the
- # newly reported time since it was renabled
- allow(GC::Profiler).to receive(:enabled?).and_return(true)
- expect(gc_profiler_mock).to receive(:total_time).and_return(25)
- probe.call
- expect_gauges([["gc_time", 5], ["gc_time", 10]])
+ it "does not report a gc_time metric in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+ expect(metric_snapshots.map(&:name)).to_not include("gc_time")
+ end
+ end
+
+ describe "does not report a gc_time metric while temporarily disabled" do
+ def perform(probe)
+ # While enabled
+ allow(GC::Profiler).to receive(:enabled?).and_return(true)
+ expect(gc_profiler_mock).to receive(:total_time).and_return(10, 15)
+ probe.call # Normal call, create a cache
+ probe.call # Report delta value based on cached value
+
+ # While disabled
+ allow(GC::Profiler).to receive(:enabled?).and_return(false)
+ probe.call # Call twice to make sure any cache resets wouldn't mess up the assertion
+ probe.call
+
+ # When enabled after being disabled for a while, it only reports the
+ # newly reported time since it was renabled
+ allow(GC::Profiler).to receive(:enabled?).and_return(true)
+ expect(gc_profiler_mock).to receive(:total_time).and_return(25)
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ # Exactly two emissions: the disabled phase reported nothing.
+ expect_gauges([["gc_time", 5], ["gc_time", 10]])
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+ # The gauge keeps its last value; assert the post-re-enable value
+ # (10), confirming the disable/re-enable cache logic emits correctly.
+ expect(find_gauge_point(metric_snapshots, "gc_time").value).to eq(10)
+ end
end
end
- it "tracks GC run count" do
- expect(GC).to receive(:count).and_return(10, 15)
- expect(GC).to receive(:stat).and_return(
- { :minor_gc_count => 10, :major_gc_count => 10 },
- :minor_gc_count => 16, :major_gc_count => 17
- )
- probe.call
- probe.call
- expect_gauge_value("gc_count", 5, :tags => { :metric => :gc_count })
- expect_gauge_value("gc_count", 6, :tags => { :metric => :minor_gc_count })
- expect_gauge_value("gc_count", 7, :tags => { :metric => :major_gc_count })
+ describe "the gc run count gauge" do
+ # The gauges report deltas between measurements, so call twice.
+ def perform(probe)
+ expect(GC).to receive(:count).and_return(10, 15)
+ expect(GC).to receive(:stat).and_return(
+ { :minor_gc_count => 10, :major_gc_count => 10 },
+ :minor_gc_count => 16, :major_gc_count => 17
+ )
+ probe.call
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ expect_gauge_value("gc_count", 5, :tags => { :metric => :gc_count })
+ expect_gauge_value("gc_count", 6, :tags => { :metric => :minor_gc_count })
+ expect_gauge_value("gc_count", 7, :tags => { :metric => :major_gc_count })
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+ snapshots = metric_snapshots
+ expect(find_gauge_point(snapshots, "gc_count", :metric => :gc_count).value).to eq(5)
+ expect(find_gauge_point(snapshots, "gc_count", :metric => :minor_gc_count).value).to eq(6)
+ expect(find_gauge_point(snapshots, "gc_count", :metric => :major_gc_count).value).to eq(7)
+ end
end
- it "tracks object allocation" do
- expect(GC).to receive(:stat).and_return(
- { :total_allocated_objects => 10 },
- :total_allocated_objects => 15
- )
- # Only tracks delta value so the needs to be called twice
- probe.call
- probe.call
- expect_gauge_value("allocated_objects", 5)
+ describe "the allocated objects gauge" do
+ # Only tracks the delta value, so it needs to be called twice.
+ def perform(probe)
+ expect(GC).to receive(:stat).and_return(
+ { :total_allocated_objects => 10 },
+ :total_allocated_objects => 15
+ )
+ probe.call
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ expect_gauge_value("allocated_objects", 5)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+ expect(find_gauge_point(metric_snapshots, "allocated_objects").value).to eq(5)
+ end
end
- it "tracks heap slots" do
- probe.call
- expect_gauge_value("heap_slots", :tags => { :metric => :heap_live })
- expect_gauge_value("heap_slots", :tags => { :metric => :heap_free })
+ describe "the heap slots gauges" do
+ def perform(probe)
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ expect_gauge_value("heap_slots", :tags => { :metric => :heap_live })
+ expect_gauge_value("heap_slots", :tags => { :metric => :heap_free })
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+ snapshots = metric_snapshots
+ expect(find_gauge_point(snapshots, "heap_slots", :metric => :heap_live).value)
+ .to be_a(Numeric)
+ expect(find_gauge_point(snapshots, "heap_slots", :metric => :heap_free).value)
+ .to be_a(Numeric)
+ end
end
context "with custom hostname" do
let(:hostname) { "my hostname" }
+ # Collector mode reads the hostname from the real Appsignal config; agent
+ # mode reads it from the AppsignalMock, which carries it directly.
+ let(:start_agent_args) { { :options => { :hostname => hostname } } }
- it "reports custom hostname tag value" do
- probe.call
- expect_gauge_value("heap_slots",
- :tags => { :metric => :heap_live, :hostname => hostname })
+ describe "reports custom hostname tag value" do
+ def perform(probe)
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform(probe)
+ expect_gauge_value("heap_slots",
+ :tags => { :metric => :heap_live, :hostname => hostname })
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform(collector_probe)
+ point = find_gauge_point(metric_snapshots, "heap_slots",
+ :metric => :heap_live, :hostname => hostname)
+ expect(point.attributes["hostname"]).to eq(hostname)
+ end
end
end
end
end
+ # A probe wired to the real Appsignal so `set_gauge` routes through the OTel
+ # metrics backend (collector mode) instead of the in-memory AppsignalMock.
+ def collector_probe
+ described_class.new(:appsignal => Appsignal, :gc_profiler => gc_profiler_mock)
+ end
+
+ # Find a single gauge data point in the pulled snapshots by metric name and
+ # (stringified) tags, asserting the snapshot exists, is a gauge, and carries a
+ # hostname. Tag values are compared as strings since OTel stringifies them.
+ def find_gauge_point(snapshots, name, tags = {})
+ snapshot = snapshots.find { |s| s.name == name }
+ expect(snapshot).not_to(be_nil, "expected a #{name} snapshot")
+ expect(snapshot.instrument_kind).to eq(:gauge)
+ point = snapshot.data_points.find do |p|
+ tags.all? { |key, value| p.attributes[key.to_s] == value.to_s }
+ end
+ expect(point).not_to(be_nil, "expected #{name} point with tags #{tags}")
+ expect(point.attributes).to include("hostname" => kind_of(String))
+ point
+ end
+
def expect_gauge_value(expected_key, expected_value = nil, tags: {})
expected_tags = { :hostname => Socket.gethostname }.merge(tags)
expect(appsignal_mock.gauges).to satisfy do |gauges|
diff --git a/spec/lib/appsignal/probes/sidekiq_spec.rb b/spec/lib/appsignal/probes/sidekiq_spec.rb
index 712fb31b3..a278cc701 100644
--- a/spec/lib/appsignal/probes/sidekiq_spec.rb
+++ b/spec/lib/appsignal/probes/sidekiq_spec.rb
@@ -5,9 +5,10 @@
let(:probe) { described_class.new }
let(:redis_hostname) { "localhost" }
let(:expected_default_tags) { { :hostname => "localhost" } }
+ # `start_agent` is supplied by the `:agent_mode`/`:collector_mode` contexts
+ # on each example, not here -- a hardcoded `start_agent` would boot the agent
+ # in agent mode and clobber collector mode's collector-endpoint setup.
before do
- start_agent
-
# The probe will `require "sidekiq/api"` on initialize, which
# as of 8.0.8 expects the `Sidekiq` module to provide a `loader`
# method that responds to `run_load_hooks`.
@@ -204,7 +205,8 @@ def with_sidekiq6!
end
end
- it "loads Sidekiq::API" do
+ it "loads Sidekiq::API", :agent_mode do
+ start_agent
with_sidekiq!
# Hide the Sidekiq constant if it was already loaded. It will be
# redefined by loading "sidekiq/api" in the probe.
@@ -215,7 +217,8 @@ def with_sidekiq6!
expect(defined?(Sidekiq::Stats)).to be_truthy
end
- it "logs config on initialize" do
+ it "logs config on initialize", :agent_mode do
+ start_agent
with_sidekiq!
log = capture_logs { probe }
expect(log).to contains_log(:debug, "Initializing Sidekiq probe\n")
@@ -224,7 +227,8 @@ def with_sidekiq6!
context "with Sidekiq 7" do
before { with_sidekiq7! }
- it "logs used hostname on call once" do
+ it "logs used hostname on call once", :agent_mode do
+ start_agent
log = capture_logs { probe.call }
expect(log).to contains_log(
:debug,
@@ -235,37 +239,52 @@ def with_sidekiq6!
expect(log).to_not contains_log(:debug, %(Sidekiq probe: ))
end
- it "collects custom metrics" do
- expect_gauge("worker_count", 24).twice
- expect_gauge("process_count", 25).twice
- expect_gauge("connection_count", 2).twice
- expect_gauge("memory_usage", 1024).twice
- expect_gauge("memory_usage_rss", 512).twice
- expect_gauge("job_count", 5, :status => :processed) # Gauge delta
- expect_gauge("job_count", 3, :status => :failed) # Gauge delta
- expect_gauge("job_count", 12, :status => :retry_queue).twice
- expect_gauge("job_count", 2, :status => :died) # Gauge delta
- expect_gauge("job_count", 14, :status => :scheduled).twice
- expect_gauge("job_count", 15, :status => :enqueued).twice
- expect_gauge("queue_length", 10, :queue => "default").twice
- expect_gauge("queue_latency", 12_000, :queue => "default").twice
- expect_gauge("queue_length", 1, :queue => "critical").twice
- expect_gauge("queue_latency", 2_000, :queue => "critical").twice
- # Call probe twice so we can calculate the delta for some gauge values
- probe.call
- probe.call
+ describe "collecting custom metrics" do
+ # Call the probe twice so the delta-based gauges report a value.
+ def perform
+ probe.call
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect_all_custom_gauges
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ expect_all_custom_gauge_snapshots
+ end
end
context "when redis info doesn't contain requested keys" do
before { Sidekiq7Mock.redis_info_data = {} }
- it "doesn't create metrics for nil values" do
- expect_gauge("connection_count").never
- expect_gauge("memory_usage").never
- expect_gauge("memory_usage_rss").never
- # Call probe twice so we can calculate the delta for some gauge values
- probe.call
- probe.call
+ describe "the redis info gauges" do
+ # Call probe twice so we can calculate the delta for some gauge values.
+ def perform
+ probe.call
+ probe.call
+ end
+
+ it "doesn't create metrics for nil values in agent mode", :agent_mode do
+ start_agent
+ expect_gauge("connection_count").never
+ expect_gauge("memory_usage").never
+ expect_gauge("memory_usage_rss").never
+ perform
+ end
+
+ it "doesn't create metrics for nil values in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ names = metric_snapshots.map(&:name)
+ expect(names).not_to include("sidekiq_connection_count")
+ expect(names).not_to include("sidekiq_memory_usage")
+ expect(names).not_to include("sidekiq_memory_usage_rss")
+ end
end
end
end
@@ -273,7 +292,8 @@ def with_sidekiq6!
context "with Sidekiq 6" do
before { with_sidekiq6! }
- it "logs used hostname on call once" do
+ it "logs used hostname on call once", :agent_mode do
+ start_agent
log = capture_logs { probe.call }
expect(log).to contains_log(
:debug,
@@ -284,25 +304,24 @@ def with_sidekiq6!
expect(log).to_not contains_log(:debug, %(Sidekiq probe: ))
end
- it "collects custom metrics" do
- expect_gauge("worker_count", 24).twice
- expect_gauge("process_count", 25).twice
- expect_gauge("connection_count", 2).twice
- expect_gauge("memory_usage", 1024).twice
- expect_gauge("memory_usage_rss", 512).twice
- expect_gauge("job_count", 5, :status => :processed) # Gauge delta
- expect_gauge("job_count", 3, :status => :failed) # Gauge delta
- expect_gauge("job_count", 12, :status => :retry_queue).twice
- expect_gauge("job_count", 2, :status => :died) # Gauge delta
- expect_gauge("job_count", 14, :status => :scheduled).twice
- expect_gauge("job_count", 15, :status => :enqueued).twice
- expect_gauge("queue_length", 10, :queue => "default").twice
- expect_gauge("queue_latency", 12_000, :queue => "default").twice
- expect_gauge("queue_length", 1, :queue => "critical").twice
- expect_gauge("queue_latency", 2_000, :queue => "critical").twice
- # Call probe twice so we can calculate the delta for some gauge values
- probe.call
- probe.call
+ describe "collecting custom metrics" do
+ # Call the probe twice so the delta-based gauges report a value.
+ def perform
+ probe.call
+ probe.call
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect_all_custom_gauges
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ expect_all_custom_gauge_snapshots
+ end
end
context "when Sidekiq `redis_info` is not defined" do
@@ -310,11 +329,23 @@ def with_sidekiq6!
allow(Sidekiq).to receive(:respond_to?).with(:redis_info).and_return(false)
end
- it "does not collect redis metrics" do
- expect_gauge("connection_count", 2).never
- expect_gauge("memory_usage", 1024).never
- expect_gauge("memory_usage_rss", 512).never
- probe.call
+ describe "the redis info gauges" do
+ it "does not collect redis metrics in agent mode", :agent_mode do
+ start_agent
+ expect_gauge("connection_count", 2).never
+ expect_gauge("memory_usage", 1024).never
+ expect_gauge("memory_usage_rss", 512).never
+ probe.call
+ end
+
+ it "does not collect redis metrics in collector mode", :collector_mode do
+ start_collector_agent
+ probe.call
+ names = metric_snapshots.map(&:name)
+ expect(names).not_to include("sidekiq_connection_count")
+ expect(names).not_to include("sidekiq_memory_usage")
+ expect(names).not_to include("sidekiq_memory_usage_rss")
+ end
end
end
end
@@ -323,7 +354,8 @@ def with_sidekiq6!
let(:redis_hostname) { "my_redis_server" }
let(:probe) { described_class.new(:hostname => redis_hostname) }
- it "uses the redis hostname for the hostname tag" do
+ it "uses the redis hostname for the hostname tag", :agent_mode do
+ start_agent
with_sidekiq!
allow(Appsignal).to receive(:set_gauge).and_call_original
@@ -340,6 +372,17 @@ def with_sidekiq6!
expect(Appsignal).to have_received(:set_gauge)
.with(anything, anything, :hostname => redis_hostname).at_least(:once)
end
+
+ it "tags the emitted gauges with the configured hostname", :collector_mode do
+ start_collector_agent
+ with_sidekiq!
+
+ probe.call
+
+ snapshot = metric_snapshot("sidekiq_worker_count")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.data_points.first.attributes).to eq("hostname" => redis_hostname)
+ end
end
def expect_gauge(key, value = anything, tags = {})
@@ -347,5 +390,67 @@ def expect_gauge(key, value = anything, tags = {})
.with("sidekiq_#{key}", value, expected_default_tags.merge(tags))
.and_call_original
end
+
+ # The full set of gauges the probe emits over two `#call`s, asserted in
+ # agent mode via `set_gauge` message expectations. Delta-based gauges
+ # (processed/failed/died job counts) only report on the second call, so
+ # they are expected once; every other gauge is expected on both calls.
+ def expect_all_custom_gauges
+ expect_gauge("worker_count", 24).twice
+ expect_gauge("process_count", 25).twice
+ expect_gauge("connection_count", 2).twice
+ expect_gauge("memory_usage", 1024).twice
+ expect_gauge("memory_usage_rss", 512).twice
+ expect_gauge("job_count", 5, :status => :processed) # Gauge delta
+ expect_gauge("job_count", 3, :status => :failed) # Gauge delta
+ expect_gauge("job_count", 12, :status => :retry_queue).twice
+ expect_gauge("job_count", 2, :status => :died) # Gauge delta
+ expect_gauge("job_count", 14, :status => :scheduled).twice
+ expect_gauge("job_count", 15, :status => :enqueued).twice
+ expect_gauge("queue_length", 10, :queue => "default").twice
+ expect_gauge("queue_latency", 12_000, :queue => "default").twice
+ expect_gauge("queue_length", 1, :queue => "critical").twice
+ expect_gauge("queue_latency", 2_000, :queue => "critical").twice
+ end
+
+ # The collector-mode counterpart of `expect_all_custom_gauges`: the agent
+ # has no in-memory readout, so here we read the same gauges back off the
+ # OpenTelemetry exporter and assert each value AND its attributes. A gauge
+ # holds its last recorded value, so the values match the agent-mode deltas.
+ # Each row is [metric short name, extra attributes, expected value]. A gauge
+ # holds its last recorded value, so the delta-based job counts
+ # (processed/failed/died) match the agent-mode deltas.
+ EXPECTED_CUSTOM_GAUGES = [
+ ["worker_count", {}, 24],
+ ["process_count", {}, 25],
+ ["connection_count", {}, 2],
+ ["memory_usage", {}, 1024],
+ ["memory_usage_rss", {}, 512],
+ ["job_count", { "status" => "processed" }, 5],
+ ["job_count", { "status" => "failed" }, 3],
+ ["job_count", { "status" => "retry_queue" }, 12],
+ ["job_count", { "status" => "died" }, 2],
+ ["job_count", { "status" => "scheduled" }, 14],
+ ["job_count", { "status" => "enqueued" }, 15],
+ ["queue_length", { "queue" => "default" }, 10],
+ ["queue_latency", { "queue" => "default" }, 12_000],
+ ["queue_length", { "queue" => "critical" }, 1],
+ ["queue_latency", { "queue" => "critical" }, 2_000]
+ ].freeze
+
+ def expect_all_custom_gauge_snapshots
+ # `metric_snapshots` resets the reader on each call, so pull once.
+ snapshots = metric_snapshots
+
+ EXPECTED_CUSTOM_GAUGES.each do |name, extra_attributes, value|
+ snapshot = snapshots.find { |s| s.name == "sidekiq_#{name}" }
+ expect(snapshot).not_to(be_nil, "expected a sidekiq_#{name} snapshot")
+ expect(snapshot.instrument_kind).to eq(:gauge)
+ expected_attributes = { "hostname" => "localhost" }.merge(extra_attributes)
+ point = snapshot.data_points.find { |p| p.attributes == expected_attributes }
+ expect(point).not_to(be_nil, "expected sidekiq_#{name} point with #{expected_attributes}")
+ expect(point.value).to eq(value)
+ end
+ end
end
end
diff --git a/spec/lib/appsignal/rack/abstract_middleware_spec.rb b/spec/lib/appsignal/rack/abstract_middleware_spec.rb
index e51bb02dc..e991d147e 100644
--- a/spec/lib/appsignal/rack/abstract_middleware_spec.rb
+++ b/spec/lib/appsignal/rack/abstract_middleware_spec.rb
@@ -4,6 +4,7 @@
Rack::MockRequest.env_for(
"/some/path",
"REQUEST_METHOD" => "GET",
+ "HTTP_ACCEPT" => "application/json",
:params => { "page" => 2, "query" => "lorem" },
"rack.session" => { "session" => "data", "user_id" => 123 }
)
@@ -12,8 +13,8 @@
let(:appsignal_env) { :default }
let(:options) { {} }
- before { start_agent(:env => appsignal_env) }
- around { |example| keep_transactions { example.run } }
+ # Pass the example's AppSignal env through to the mode contexts' `start_agent`.
+ let(:start_agent_args) { { :env => appsignal_env } }
def make_request
middleware.call(env)
@@ -27,56 +28,147 @@ def make_request_with_error(error_class, error_message)
context "when not active" do
let(:appsignal_env) { :inactive_env }
- it "does not instrument the request" do
+ it_in_both_modes "does not instrument the request" do
expect { make_request }.to_not(change { created_transactions.count })
end
- it "calls the next middleware in the stack" do
+ it_in_both_modes "calls the next middleware in the stack" do
make_request
expect(app).to be_called
end
end
context "when appsignal is active" do
- it "creates a transaction for the request" do
- expect { make_request }.to(change { created_transactions.count }.by(1))
+ describe "creates a transaction for the request" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ end
- expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ expect(root_span.kind).to eq(:server)
+ end
end
- it "wraps the response body in a BodyWrapper subclass" do
+ describe "incoming trace context" do
+ let(:trace_id_hex) { "0af7651916cd43dd8448eb211c80319c" }
+ let(:span_id_hex) { "b7ad6b7169203331" }
+
+ it "continues the upstream trace when a traceparent is present", :collector_mode do
+ env["HTTP_TRACEPARENT"] = "00-#{trace_id_hex}-#{span_id_hex}-01"
+ start_collector_agent
+ make_request
+
+ expect(root_span.hex_trace_id).to eq(trace_id_hex)
+ expect(root_span.parent_span_id.unpack1("H*")).to eq(span_id_hex)
+ end
+
+ it "starts a fresh root trace when no traceparent is present", :collector_mode do
+ start_collector_agent
+ make_request
+
+ expect(root_span.parent_span_id).to eq(::OpenTelemetry::Trace::INVALID_SPAN_ID)
+ end
+ end
+
+ it_in_both_modes "wraps the response body in a BodyWrapper subclass" do
_status, _headers, body = make_request
expect(body).to be_kind_of(Appsignal::Rack::BodyWrapper)
end
context "without an error" do
- before { make_request }
-
- it "calls the next middleware in the stack" do
+ it_in_both_modes "calls the next middleware in the stack" do
+ make_request
expect(app).to be_called
end
- it "does not record an error" do
- expect(last_transaction).to_not have_error
+ describe "does not record an error" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ end
end
context "without :instrument_event_name option set" do
let(:options) { {} }
- it "does not record an instrumentation event" do
- expect(last_transaction).to_not include_event
+ describe "does not record an instrumentation event" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not include_event
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(event_spans).to be_empty
+ end
end
end
context "with :instrument_event_name option set" do
let(:options) { { :instrument_event_name => "event_name.category" } }
- it "records an instrumentation event" do
- expect(last_transaction).to include_event(:name => "event_name.category")
+ describe "records an instrumentation event" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_event(:name => "event_name.category")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(event_spans.map(&:name)).to include("event_name.category")
+ span = event_spans.find { |s| s.name == "event_name.category" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ end
end
end
- it "completes the transaction" do
+ # `be_completed` reads `backend._completed?` and `Appsignal::Transaction
+ # .current` is the thread-local, so both assertions are backend-agnostic.
+ it_in_both_modes "completes the transaction" do
+ make_request
+
expect(last_transaction).to be_completed
expect(Appsignal::Transaction.current)
.to be_kind_of(Appsignal::Transaction::NilTransaction)
@@ -85,8 +177,24 @@ def make_request_with_error(error_class, error_message)
context "when instrument_event_name option is nil" do
let(:options) { { :instrument_event_name => nil } }
- it "does not record an instrumentation event" do
- expect(last_transaction).to_not include_events
+ describe "does not record an instrumentation event" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not include_events
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(event_spans).to be_empty
+ end
end
end
end
@@ -95,117 +203,306 @@ def make_request_with_error(error_class, error_message)
let(:error) { ExampleException.new("error message") }
let(:app) { lambda { |_env| raise ExampleException, "error message" } }
- it "create a transaction for the request" do
- expect { make_request_with_error(ExampleException, "error message") }
- .to(change { created_transactions.count }.by(1))
+ describe "create a transaction for the request" do
+ def perform
+ make_request_with_error(ExampleException, "error message")
+ end
- expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ end
end
describe "error" do
- before do
- make_request_with_error(ExampleException, "error message")
- end
+ describe "records the error" do
+ def perform
+ make_request_with_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
- it "records the error" do
- expect(last_transaction).to have_error("ExampleException", "error message")
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
- it "completes the transaction" do
+ it_in_both_modes "completes the transaction" do
+ make_request_with_error(ExampleException, "error message")
+
expect(last_transaction).to be_completed
expect(Appsignal::Transaction.current)
.to be_kind_of(Appsignal::Transaction::NilTransaction)
end
context "with :report_errors set to false" do
- let(:app) { lambda { |_env| raise ExampleException, "error message" } }
let(:options) { { :report_errors => false } }
- it "does not record the exception on the transaction" do
- expect(last_transaction).to_not have_error
+ describe "does not record the exception on the transaction" do
+ def perform
+ make_request_with_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ end
end
end
context "with :report_errors set to true" do
- let(:app) { lambda { |_env| raise ExampleException, "error message" } }
let(:options) { { :report_errors => true } }
- it "records the exception on the transaction" do
- expect(last_transaction).to have_error("ExampleException", "error message")
+ describe "records the exception on the transaction" do
+ def perform
+ make_request_with_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
context "with :report_errors set to a lambda that returns false" do
- let(:app) { lambda { |_env| raise ExampleException, "error message" } }
let(:options) { { :report_errors => lambda { |_env| false } } }
- it "does not record the exception on the transaction" do
- expect(last_transaction).to_not have_error
+ describe "does not record the exception on the transaction" do
+ def perform
+ make_request_with_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ end
end
end
context "with :report_errors set to a lambda that returns true" do
- let(:app) { lambda { |_env| raise ExampleException, "error message" } }
let(:options) { { :report_errors => lambda { |_env| true } } }
- it "records the exception on the transaction" do
- expect(last_transaction).to have_error("ExampleException", "error message")
+ describe "records the exception on the transaction" do
+ def perform
+ make_request_with_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
end
end
context "without action name metadata" do
- it "reports no action name" do
- make_request
+ describe "reports no action name" do
+ def perform
+ make_request
+ end
- expect(last_transaction).to_not have_action
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
end
# Partial duplicate tests from Appsignal::Rack::ApplyRackRequest that
# ensure the request metadata is set on via the AbstractMiddleware.
describe "request metadata" do
- it "sets request metadata" do
- env.merge!("PATH_INFO" => "/some/path", "REQUEST_METHOD" => "GET")
- make_request
+ describe "sets request metadata" do
+ def perform
+ env.merge!("PATH_INFO" => "/some/path", "REQUEST_METHOD" => "GET")
+ make_request
+ end
- expect(last_transaction).to include_metadata(
- "request_method" => "GET",
- "method" => "GET",
- "request_path" => "/some/path",
- "path" => "/some/path"
- )
- expect(last_transaction).to include_environment(
- "REQUEST_METHOD" => "GET",
- "PATH_INFO" => "/some/path"
- # and more, but we don't need to test Rack mock defaults
- )
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_metadata(
+ "request_method" => "GET",
+ "method" => "GET",
+ "request_path" => "/some/path",
+ "path" => "/some/path"
+ )
+ expect(last_transaction).to include_environment(
+ "REQUEST_METHOD" => "GET",
+ "PATH_INFO" => "/some/path"
+ # and more, but we don't need to test Rack mock defaults
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ # Metadata is emitted as `appsignal.tag.*` attributes.
+ expect(root_span.attributes["appsignal.tag.request_method"]).to eq("GET")
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("GET")
+ expect(root_span.attributes["appsignal.tag.request_path"]).to eq("/some/path")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/some/path")
+ # Only true HTTP headers map to the `http.request.header.*`
+ # convention; the non-header CGI vars (REQUEST_METHOD, PATH_INFO)
+ # are intentionally dropped.
+ expect(root_span.attributes["http.request.header.accept"]).to eq("application/json")
+ expect(root_span.attributes.keys).to_not include("http.request.header.request-method")
+ end
end
- it "sets request parameters" do
- make_request
+ describe "sets request parameters" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_params(
+ "page" => "2",
+ "query" => "lorem"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to include_params(
- "page" => "2",
- "query" => "lorem"
- )
+ params = JSON.parse(root_span.attributes["appsignal.request.payload"])
+ expect(params).to include("page" => "2", "query" => "lorem")
+ end
end
- it "sets session data" do
- make_request
+ describe "sets session data" do
+ def perform
+ make_request
+ end
- expect(last_transaction).to include_session_data("session" => "data", "user_id" => 123)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_session_data("session" => "data", "user_id" => 123)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ session = JSON.parse(root_span.attributes["appsignal.request.session_data"])
+ expect(session).to include("session" => "data", "user_id" => 123)
+ end
end
context "with queue start header" do
let(:queue_start_time) { fixed_time * 1_000 }
- it "sets the queue start" do
- env["HTTP_X_REQUEST_START"] = "t=#{queue_start_time.to_i}" # in milliseconds
- make_request
+ describe "sets the queue start" do
+ def perform
+ env["HTTP_X_REQUEST_START"] = "t=#{queue_start_time.to_i}" # in milliseconds
+ make_request
+ end
- expect(last_transaction).to have_queue_start(queue_start_time)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_queue_start(queue_start_time)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ queue_event = Array(root_span.events).find { |e| e.name == "appsignal.queue_start" }
+ expect(queue_event.attributes["appsignal.queue_start"])
+ .to eq(queue_start_time.to_i)
+ end
end
end
@@ -238,53 +535,73 @@ def session
{ :request_class => SomeFilteredRequest, :params_method => :filtered_params }
end
- it "uses the overridden request class and params method to fetch params" do
- make_request
+ describe "uses the overridden request class and params method to fetch params" do
+ def perform
+ make_request
+ end
- expect(last_transaction).to include_params("abc" => "123")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_params("abc" => "123")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ params = JSON.parse(root_span.attributes["appsignal.request.payload"])
+ expect(params).to include("abc" => "123")
+ end
end
- it "uses the overridden request class to fetch session data" do
- make_request
+ describe "uses the overridden request class to fetch session data" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_session_data("data" => "value")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to include_session_data("data" => "value")
+ session = JSON.parse(root_span.attributes["appsignal.request.session_data"])
+ expect(session).to include("data" => "value")
+ end
end
end
end
context "with parent instrumentation" do
let(:transaction) { http_request_transaction }
- before do
+
+ # The parent transaction's backend is mode-specific, so it must be built
+ # after the agent starts -- called from each example body, not a `before`.
+ def setup_parent_transaction
env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = transaction
set_current_transaction(transaction)
end
- it "uses the existing transaction" do
+ it_in_both_modes "uses the existing transaction" do
+ setup_parent_transaction
make_request
expect { make_request }.to_not(change { created_transactions.count })
end
- it "wraps the response body in a BodyWrapper subclass" do
- _status, _headers, body = make_request
- expect(body).to be_kind_of(Appsignal::Rack::BodyWrapper)
-
- body.to_ary
- response_events =
- last_transaction.to_h["events"].count do |event|
- event["name"] == "process_response_body.rack"
- end
- expect(response_events).to eq(1)
- end
-
- context "when the response body is already instrumented" do
- let(:body) { Appsignal::Rack::BodyWrapper.wrap(["hello!"], transaction) }
- let(:app) { DummyApp.new { [200, {}, body] } }
-
- it "doesn't wrap the body again" do
- env[Appsignal::Rack::APPSIGNAL_RESPONSE_INSTRUMENTED] = true
+ describe "wraps the response body in a BodyWrapper subclass" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ setup_parent_transaction
_status, _headers, body = make_request
- expect(body).to eq(body)
+ expect(body).to be_kind_of(Appsignal::Rack::BodyWrapper)
body.to_ary
response_events =
@@ -293,31 +610,118 @@ def session
end
expect(response_events).to eq(1)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ setup_parent_transaction
+ _status, _headers, body = make_request
+ expect(body).to be_kind_of(Appsignal::Rack::BodyWrapper)
+
+ body.to_ary
+ response_events =
+ event_spans.count do |span|
+ span.attributes["appsignal.category"] == "process_response_body.rack"
+ end
+ expect(response_events).to eq(1)
+ end
+ end
+
+ context "when the response body is already instrumented" do
+ let(:body) { Appsignal::Rack::BodyWrapper.wrap(["hello!"], transaction) }
+ let(:app) { DummyApp.new { [200, {}, body] } }
+
+ describe "doesn't wrap the body again" do
+ def perform
+ setup_parent_transaction
+ env[Appsignal::Rack::APPSIGNAL_RESPONSE_INSTRUMENTED] = true
+ _status, _headers, body = make_request
+ expect(body).to eq(body)
+ body.to_ary
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ response_events =
+ last_transaction.to_h["events"].count do |event|
+ event["name"] == "process_response_body.rack"
+ end
+ expect(response_events).to eq(1)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ response_events =
+ event_spans.count do |span|
+ span.attributes["appsignal.category"] == "process_response_body.rack"
+ end
+ expect(response_events).to eq(1)
+ end
+ end
end
context "with error" do
let(:app) { lambda { |_env| raise ExampleException, "error message" } }
- it "doesn't record the error on the transaction" do
- make_request_with_error(ExampleException, "error message")
+ describe "doesn't record the error on the transaction" do
+ def perform
+ setup_parent_transaction
+ make_request_with_error(ExampleException, "error message")
+ end
- expect(last_transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ # The middleware leaves the parent open; finish it so its span
+ # exports and we can confirm no exception event was recorded.
+ Appsignal::Transaction.complete_current!
+
+ expect(exception_events).to be_empty
+ end
end
end
- it "doesn't complete the existing transaction" do
+ it_in_both_modes "doesn't complete the existing transaction" do
+ setup_parent_transaction
make_request
expect(env[Appsignal::Rack::APPSIGNAL_TRANSACTION]).to_not be_completed
end
context "with custom set action name" do
- it "does not overwrite the action name" do
- env[Appsignal::Rack::APPSIGNAL_TRANSACTION].set_action("My custom action")
- env["appsignal.action"] = "POST /my-action"
- make_request
+ describe "does not overwrite the action name" do
+ def perform
+ setup_parent_transaction
+ env[Appsignal::Rack::APPSIGNAL_TRANSACTION].set_action("My custom action")
+ env["appsignal.action"] = "POST /my-action"
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("My custom action")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ Appsignal::Transaction.complete_current!
- expect(last_transaction).to have_action("My custom action")
+ expect(root_span.name).to eq("My custom action")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("My custom action")
+ end
end
end
@@ -325,10 +729,26 @@ def session
let(:app) { lambda { |_env| raise ExampleException, "error message" } }
let(:options) { { :report_errors => false } }
- it "does not record the error on the transaction" do
- make_request_with_error(ExampleException, "error message")
+ describe "does not record the error on the transaction" do
+ def perform
+ setup_parent_transaction
+ make_request_with_error(ExampleException, "error message")
+ end
- expect(last_transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(exception_events).to be_empty
+ end
end
end
@@ -336,10 +756,32 @@ def session
let(:app) { lambda { |_env| raise ExampleException, "error message" } }
let(:options) { { :report_errors => true } }
- it "records the error on the transaction" do
- make_request_with_error(ExampleException, "error message")
+ describe "records the error on the transaction" do
+ def perform
+ setup_parent_transaction
+ make_request_with_error(ExampleException, "error message")
+ end
- expect(last_transaction).to have_error("ExampleException", "error message")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ Appsignal::Transaction.complete_current!
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
@@ -347,10 +789,26 @@ def session
let(:app) { lambda { |_env| raise ExampleException, "error message" } }
let(:options) { { :report_errors => lambda { |_env| false } } }
- it "does not record the exception on the transaction" do
- make_request_with_error(ExampleException, "error message")
+ describe "does not record the exception on the transaction" do
+ def perform
+ setup_parent_transaction
+ make_request_with_error(ExampleException, "error message")
+ end
- expect(last_transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(exception_events).to be_empty
+ end
end
end
@@ -358,10 +816,32 @@ def session
let(:app) { lambda { |_env| raise ExampleException, "error message" } }
let(:options) { { :report_errors => lambda { |_env| true } } }
- it "records the error on the transaction" do
- make_request_with_error(ExampleException, "error message")
+ describe "records the error on the transaction" do
+ def perform
+ setup_parent_transaction
+ make_request_with_error(ExampleException, "error message")
+ end
- expect(last_transaction).to have_error("ExampleException", "error message")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ Appsignal::Transaction.complete_current!
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
end
diff --git a/spec/lib/appsignal/rack/body_wrapper_spec.rb b/spec/lib/appsignal/rack/body_wrapper_spec.rb
index 0a425cb3e..637c1d9dc 100644
--- a/spec/lib/appsignal/rack/body_wrapper_spec.rb
+++ b/spec/lib/appsignal/rack/body_wrapper_spec.rb
@@ -1,11 +1,47 @@
describe Appsignal::Rack::BodyWrapper do
- let(:transaction) { http_request_transaction }
- before do
- start_agent
- set_current_transaction(transaction)
+ # Create the transaction inside the example (lazily, on first reference) and
+ # set it as the current transaction. In collector mode it must be created
+ # after the mode context's `before` has enabled collector mode, so it gets
+ # the OpenTelemetry backend.
+ let(:transaction) do
+ http_request_transaction.tap { |t| set_current_transaction(t) }
end
- it "forwards method calls to the body if the method doesn't exist" do
+ # Collector-mode assertion helpers. The wrapper does not complete the
+ # transaction, so finish it here to export its spans, then assert on the
+ # recorded exception events / child event spans.
+ def expect_collector_error(type, message)
+ transaction.complete
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq(type)
+ expect(event.attributes["exception.message"]).to eq(message)
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
+
+ def expect_collector_no_error
+ transaction.complete
+ expect(exception_events).to be_empty
+ end
+
+ def expect_collector_event(name, title = nil)
+ transaction.complete
+ # The event name lives in appsignal.category; the span name carries the
+ # human-readable title (falling back to the event name when title-less).
+ span = event_spans.find { |s| s.attributes["appsignal.category"] == name }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.name).to eq(title) if title
+ end
+
+ def expect_collector_no_event(name)
+ transaction.complete
+ expect(event_spans.map { |s| s.attributes["appsignal.category"] }).to_not include(name)
+ end
+
+ it_in_both_modes "forwards method calls to the body if the method doesn't exist" do
fake_body = double(
:body => ["some body"],
:some_method => :some_value
@@ -19,7 +55,7 @@
expect(wrapped.some_method).to eq(:some_value)
end
- it "doesn't respond to methods the Rack::BodyProxy doesn't respond to" do
+ it_in_both_modes "doesn't respond to methods the Rack::BodyProxy doesn't respond to" do
body = Rack::BodyProxy.new(["body"])
wrapped = described_class.wrap(body, transaction)
@@ -31,7 +67,7 @@
end
describe "with a body only supporting each()" do
- it "wraps with appropriate class" do
+ it_in_both_modes "wraps with appropriate class" do
fake_body = double(:each => nil)
wrapped = described_class.wrap(fake_body, transaction)
@@ -41,183 +77,421 @@
expect(wrapped).to respond_to(:close)
end
- it "reads out the body in full using each" do
- fake_body = double
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
+ describe "reads out the body in full using each" do
+ def perform
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
- wrapped = described_class.wrap(fake_body, transaction)
- expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
+ end
- expect(transaction).to include_event(
- "name" => "process_response_body.rack",
- "title" => "Process Rack response body (#each)"
- )
- end
+ it "in agent mode", :agent_mode do
+ start_agent
- it "returns an Enumerator if each() gets called without a block" do
- fake_body = double
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
+ perform
- wrapped = described_class.wrap(fake_body, transaction)
- enum = wrapped.each
- expect(enum).to be_kind_of(Enumerator)
- expect { |b| enum.each(&b) }.to yield_successive_args("a", "b", "c")
+ expect(transaction).to include_event(
+ "name" => "process_response_body.rack",
+ "title" => "Process Rack response body (#each)"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
- expect(transaction).to_not include_event("name" => "process_response_body.rack")
+ expect_collector_event(
+ "process_response_body.rack",
+ "Process Rack response body (#each)"
+ )
+ end
end
- it "sets the exception raised inside each() on the transaction" do
- fake_body = double
- expect(fake_body).to receive(:each).once.and_raise(ExampleException, "error message")
+ describe "returns an Enumerator if each() gets called without a block" do
+ def perform
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(ExampleException, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ enum = wrapped.each
+ expect(enum).to be_kind_of(Enumerator)
+ expect { |b| enum.each(&b) }.to yield_successive_args("a", "b", "c")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
- expect(transaction).to have_error("ExampleException", "error message")
+ expect(transaction).to_not include_event("name" => "process_response_body.rack")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ transaction.complete
+
+ # Mirrors the agent `to_not include_event` here: that matcher only
+ # excludes a *default-shaped* event (empty title). Iterating the
+ # returned Enumerator still instruments `each`, so the recorded event
+ # carries the "#each" title -- there is just never a title-less one.
+ # A title-less event would fall back to naming the span after its
+ # category (the event name); the "#each" one never does.
+ titleless_event = event_spans.find do |span|
+ span.attributes["appsignal.category"] == "process_response_body.rack" &&
+ span.name == span.attributes["appsignal.category"]
+ end
+ expect(titleless_event).to be_nil
+ end
end
- it "doesn't report EPIPE error" do
- fake_body = double
- expect(fake_body).to receive(:each).once.and_raise(Errno::EPIPE)
+ describe "sets the exception raised inside each() on the transaction" do
+ def perform
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_raise(ExampleException, "error message")
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(Errno::EPIPE)
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
- expect(transaction).to_not have_error
+ expect_collector_error("ExampleException", "error message")
+ end
end
- it "doesn't report ECONNRESET error" do
- fake_body = double
- expect(fake_body).to receive(:each).once.and_raise(Errno::ECONNRESET)
+ describe "doesn't report EPIPE error" do
+ def perform
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_raise(Errno::EPIPE)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(Errno::ECONNRESET)
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(Errno::EPIPE)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
- expect(transaction).to_not have_error
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report EPIPE error when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::EPIPE)
- fake_body = double
- expect(fake_body).to receive(:each).once.and_raise(error)
+ describe "doesn't report ECONNRESET error" do
+ def perform
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_raise(Errno::ECONNRESET)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(Errno::ECONNRESET)
+ end
- expect(transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report EPIPE error when it's the nested error cause" do
- error = error_with_nested_cause(StandardError, "error message", Errno::EPIPE)
- fake_body = double
- expect(fake_body).to receive(:each).once.and_raise(error)
+ describe "does not report EPIPE error when it's the error cause" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::EPIPE)
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
- expect(transaction).to_not have_error
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report ECONNRESET error when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
- fake_body = double
- expect(fake_body).to receive(:each).once.and_raise(error)
+ describe "does not report EPIPE error when it's the nested error cause" do
+ def perform
+ error = error_with_nested_cause(StandardError, "error message", Errno::EPIPE)
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
- expect(transaction).to_not have_error
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report ECONNRESET error when it's the nested error cause" do
- error = error_with_nested_cause(StandardError, "error message", Errno::ECONNRESET)
- fake_body = double
- expect(fake_body).to receive(:each).once.and_raise(error)
+ describe "does not report ECONNRESET error when it's the error cause" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(StandardError, "error message")
+ end
- expect(transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "closes the body and tracks an instrumentation event when it gets closed" do
- fake_body = double(:close => nil)
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
+ describe "does not report ECONNRESET error when it's the nested error cause" do
+ def perform
+ error = error_with_nested_cause(StandardError, "error message", Errno::ECONNRESET)
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
- wrapped.close
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
- expect(transaction).to include_event("name" => "close_response_body.rack")
+ perform
+ expect_collector_no_error
+ end
end
- it "reports an error if an error occurs on close" do
- fake_body = double
- expect(fake_body).to receive(:close).and_raise(ExampleException, "error message")
+ describe "closes the body and tracks an instrumentation event when it gets closed" do
+ def perform
+ fake_body = double(:close => nil)
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
wrapped.close
- end.to raise_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to include_event("name" => "close_response_body.rack")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect_collector_event("close_response_body.rack")
+ end
+ end
+
+ describe "reports an error if an error occurs on close" do
+ def perform
+ fake_body = double
+ expect(fake_body).to receive(:close).and_raise(ExampleException, "error message")
+
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.close
+ end.to raise_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
- expect(transaction).to have_error("ExampleException", "error message")
+ expect_collector_error("ExampleException", "error message")
+ end
end
- it "doesn't report EPIPE error on close" do
- fake_body = double
- expect(fake_body).to receive(:close).and_raise(Errno::EPIPE)
+ describe "doesn't report EPIPE error on close" do
+ def perform
+ fake_body = double
+ expect(fake_body).to receive(:close).and_raise(Errno::EPIPE)
- wrapped = described_class.wrap(fake_body, transaction)
- expect { wrapped.close }.to raise_error(Errno::EPIPE)
- expect(transaction).to_not have_error
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect { wrapped.close }.to raise_error(Errno::EPIPE)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "doesn't report ECONNRESET error on close" do
- fake_body = double
- expect(fake_body).to receive(:close).and_raise(Errno::ECONNRESET)
+ describe "doesn't report ECONNRESET error on close" do
+ def perform
+ fake_body = double
+ expect(fake_body).to receive(:close).and_raise(Errno::ECONNRESET)
- wrapped = described_class.wrap(fake_body, transaction)
- expect { wrapped.close }.to raise_error(Errno::ECONNRESET)
- expect(transaction).to_not have_error
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect { wrapped.close }.to raise_error(Errno::ECONNRESET)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report EPIPE error when it's the error cause on close" do
- error = error_with_cause(StandardError, "error message", Errno::EPIPE)
- fake_body = double
- expect(fake_body).to receive(:close).and_raise(error)
+ describe "does not report EPIPE error when it's the error cause on close" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::EPIPE)
+ fake_body = double
+ expect(fake_body).to receive(:close).and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect { wrapped.close }.to raise_error(StandardError, "error message")
- expect(transaction).to_not have_error
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect { wrapped.close }.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report ECONNRESET error when it's the error cause on close" do
- error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
- fake_body = double
- expect(fake_body).to receive(:close).and_raise(error)
+ describe "does not report ECONNRESET error when it's the error cause on close" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
+ fake_body = double
+ expect(fake_body).to receive(:close).and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect { wrapped.close }.to raise_error(StandardError, "error message")
- expect(transaction).to_not have_error
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect { wrapped.close }.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
end
describe "with a body supporting both each() and call" do
- it "wraps with the wrapper that exposes each" do
+ it_in_both_modes "wraps with the wrapper that exposes each" do
fake_body = double(
:each => true,
:call => "original call"
@@ -236,7 +510,7 @@
describe "with a body supporting both to_ary and each" do
let(:fake_body) { double(:each => nil, :to_ary => []) }
- it "wraps with appropriate class" do
+ it_in_both_modes "wraps with appropriate class" do
wrapped = described_class.wrap(fake_body, transaction)
expect(wrapped).to respond_to(:each)
expect(wrapped).to respond_to(:to_ary)
@@ -245,138 +519,292 @@
expect(wrapped).to respond_to(:close)
end
- it "reads out the body in full using each" do
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
+ describe "reads out the body in full using each" do
+ def perform
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
- wrapped = described_class.wrap(fake_body, transaction)
- expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
+ end
- expect(transaction).to include_event(
- "name" => "process_response_body.rack",
- "title" => "Process Rack response body (#each)"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "process_response_body.rack",
+ "title" => "Process Rack response body (#each)"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect_collector_event(
+ "process_response_body.rack",
+ "Process Rack response body (#each)"
+ )
+ end
end
- it "sets the exception raised inside each() into the Appsignal transaction" do
- expect(fake_body).to receive(:each).once.and_raise(ExampleException, "error message")
+ describe "sets the exception raised inside each() into the Appsignal transaction" do
+ def perform
+ expect(fake_body).to receive(:each).once.and_raise(ExampleException, "error message")
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(ExampleException, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
- expect(transaction).to have_error("ExampleException", "error message")
+ expect(transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect_collector_error("ExampleException", "error message")
+ end
end
- it "doesn't report EPIPE error" do
- expect(fake_body).to receive(:each).once.and_raise(Errno::EPIPE)
+ describe "doesn't report EPIPE error" do
+ def perform
+ expect(fake_body).to receive(:each).once.and_raise(Errno::EPIPE)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(Errno::EPIPE)
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(Errno::EPIPE)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
- expect(transaction).to_not have_error
+ perform
+ expect_collector_no_error
+ end
end
- it "doesn't report ECONNRESET error" do
- expect(fake_body).to receive(:each).once.and_raise(Errno::ECONNRESET)
+ describe "doesn't report ECONNRESET error" do
+ def perform
+ expect(fake_body).to receive(:each).once.and_raise(Errno::ECONNRESET)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(Errno::ECONNRESET)
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(Errno::ECONNRESET)
+ end
- expect(transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report EPIPE error when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::EPIPE)
- fake_body = double
- expect(fake_body).to receive(:each).once.and_raise(error)
+ describe "does not report EPIPE error when it's the error cause (each)" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::EPIPE)
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(StandardError, "error message")
+ end
- expect(transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report ECONNRESET error when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
- fake_body = double
- expect(fake_body).to receive(:each).once.and_raise(error)
+ describe "does not report ECONNRESET error when it's the error cause (each)" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
+ fake_body = double
+ expect(fake_body).to receive(:each).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
- expect(transaction).to_not have_error
+ perform
+ expect_collector_no_error
+ end
end
- it "reads out the body in full using to_ary" do
- expect(fake_body).to receive(:to_ary).and_return(["one", "two", "three"])
+ describe "reads out the body in full using to_ary" do
+ def perform
+ expect(fake_body).to receive(:to_ary).and_return(["one", "two", "three"])
- wrapped = described_class.wrap(fake_body, transaction)
- expect(wrapped.to_ary).to eq(["one", "two", "three"])
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect(wrapped.to_ary).to eq(["one", "two", "three"])
+ end
- expect(transaction).to include_event(
- "name" => "process_response_body.rack",
- "title" => "Process Rack response body (#to_ary)"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "process_response_body.rack",
+ "title" => "Process Rack response body (#to_ary)"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect_collector_event(
+ "process_response_body.rack",
+ "Process Rack response body (#to_ary)"
+ )
+ end
end
- it "sends the exception raised inside to_ary() into the Appsignal and closes transaction" do
- fake_body = double
- allow(fake_body).to receive(:each)
- expect(fake_body).to receive(:to_ary).once.and_raise(ExampleException, "error message")
- expect(fake_body).to_not receive(:close) # Per spec we expect the body has closed itself
+ describe "sends the exception raised inside to_ary() to AppSignal and closes" do
+ def perform
+ fake_body = double
+ allow(fake_body).to receive(:each)
+ expect(fake_body).to receive(:to_ary).once.and_raise(ExampleException, "error message")
+ expect(fake_body).to_not receive(:close) # Per spec we expect the body has closed itself
+
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.to_ary
+ end.to raise_error(ExampleException, "error message")
+ end
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.to_ary
- end.to raise_error(ExampleException, "error message")
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
- expect(transaction).to have_error("ExampleException", "error message")
+ expect_collector_error("ExampleException", "error message")
+ end
end
- it "does not report EPIPE error when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::EPIPE)
- fake_body = double
- allow(fake_body).to receive(:each)
- expect(fake_body).to receive(:to_ary).once.and_raise(error)
- expect(fake_body).to_not receive(:close) # Per spec we expect the body has closed itself
+ describe "does not report EPIPE error when it's the error cause (to_ary)" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::EPIPE)
+ fake_body = double
+ allow(fake_body).to receive(:each)
+ expect(fake_body).to receive(:to_ary).once.and_raise(error)
+ expect(fake_body).to_not receive(:close) # Per spec we expect the body has closed itself
+
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.to_ary
+ end.to raise_error(StandardError, "error message")
+ end
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.to_ary
- end.to raise_error(StandardError, "error message")
+ it "in agent mode", :agent_mode do
+ start_agent
- expect(transaction).to_not have_error
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report ECONNRESET error when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
- fake_body = double
- allow(fake_body).to receive(:each)
- expect(fake_body).to receive(:to_ary).once.and_raise(error)
- expect(fake_body).to_not receive(:close) # Per spec we expect the body has closed itself
+ describe "does not report ECONNRESET error when it's the error cause (to_ary)" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
+ fake_body = double
+ allow(fake_body).to receive(:each)
+ expect(fake_body).to receive(:to_ary).once.and_raise(error)
+ expect(fake_body).to_not receive(:close) # Per spec we expect the body has closed itself
+
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.to_ary
+ end.to raise_error(StandardError, "error message")
+ end
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.to_ary
- end.to raise_error(StandardError, "error message")
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
- expect(transaction).to_not have_error
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
end
describe "with a body supporting both to_path and each" do
let(:fake_body) { double(:each => nil, :to_path => nil) }
- it "wraps with appropriate class" do
+ it_in_both_modes "wraps with appropriate class" do
wrapped = described_class.wrap(fake_body, transaction)
expect(wrapped).to respond_to(:each)
expect(wrapped).to_not respond_to(:to_ary)
@@ -385,127 +813,281 @@
expect(wrapped).to respond_to(:close)
end
- it "reads out the body in full using each()" do
- expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
+ describe "reads out the body in full using each()" do
+ def perform
+ expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
- wrapped = described_class.wrap(fake_body, transaction)
- expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
+ end
- expect(transaction).to include_event(
- "name" => "process_response_body.rack",
- "title" => "Process Rack response body (#each)"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "process_response_body.rack",
+ "title" => "Process Rack response body (#each)"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect_collector_event(
+ "process_response_body.rack",
+ "Process Rack response body (#each)"
+ )
+ end
end
- it "sets the exception raised inside each() into the Appsignal transaction" do
- expect(fake_body).to receive(:each).once.and_raise(ExampleException, "error message")
+ describe "sets the exception raised inside each() into the Appsignal transaction" do
+ def perform
+ expect(fake_body).to receive(:each).once.and_raise(ExampleException, "error message")
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(ExampleException, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to have_error("ExampleException", "error message")
+ end
- expect(transaction).to have_error("ExampleException", "error message")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect_collector_error("ExampleException", "error message")
+ end
end
- it "sets the exception raised inside to_path() into the Appsignal transaction" do
- allow(fake_body).to receive(:to_path).once.and_raise(ExampleException, "error message")
+ describe "sets the exception raised inside to_path() into the Appsignal transaction" do
+ def perform
+ allow(fake_body).to receive(:to_path).once.and_raise(ExampleException, "error message")
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.to_path
- end.to raise_error(ExampleException, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.to_path
+ end.to raise_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
- expect(transaction).to have_error("ExampleException", "error message")
+ perform
+
+ expect_collector_error("ExampleException", "error message")
+ end
end
- it "doesn't report EPIPE error" do
- expect(fake_body).to receive(:to_path).once.and_raise(Errno::EPIPE)
+ describe "doesn't report EPIPE error" do
+ def perform
+ expect(fake_body).to receive(:to_path).once.and_raise(Errno::EPIPE)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.to_path
- end.to raise_error(Errno::EPIPE)
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.to_path
+ end.to raise_error(Errno::EPIPE)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
- expect(transaction).to_not have_error
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "doesn't report ECONNRESET error" do
- expect(fake_body).to receive(:to_path).once.and_raise(Errno::ECONNRESET)
+ describe "doesn't report ECONNRESET error" do
+ def perform
+ expect(fake_body).to receive(:to_path).once.and_raise(Errno::ECONNRESET)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.to_path
- end.to raise_error(Errno::ECONNRESET)
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.to_path
+ end.to raise_error(Errno::ECONNRESET)
+ end
- expect(transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report EPIPE error from #each when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::EPIPE)
- expect(fake_body).to receive(:each).once.and_raise(error)
+ describe "does not report EPIPE error from #each when it's the error cause" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::EPIPE)
+ expect(fake_body).to receive(:each).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
- expect(transaction).to_not have_error
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report ECONNRESET error from #each when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
- expect(fake_body).to receive(:each).once.and_raise(error)
+ describe "does not report ECONNRESET error from #each when it's the error cause" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
+ expect(fake_body).to receive(:each).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- expect { |b| wrapped.each(&b) }.to yield_control
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ expect { |b| wrapped.each(&b) }.to yield_control
+ end.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
- expect(transaction).to_not have_error
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report EPIPE error from #to_path when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::EPIPE)
- allow(fake_body).to receive(:to_path).once.and_raise(error)
+ describe "does not report EPIPE error from #to_path when it's the error cause" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::EPIPE)
+ allow(fake_body).to receive(:to_path).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.to_path
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.to_path
+ end.to raise_error(StandardError, "error message")
+ end
- expect(transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report ECONNRESET error from #to_path when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
- allow(fake_body).to receive(:to_path).once.and_raise(error)
+ describe "does not report ECONNRESET error from #to_path when it's the error cause" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
+ allow(fake_body).to receive(:to_path).once.and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.to_path
- end.to raise_error(StandardError, "error message")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.to_path
+ end.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
- expect(transaction).to_not have_error
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "exposes to_path to the sender" do
- allow(fake_body).to receive(:to_path).and_return("/tmp/file.bin")
+ describe "exposes to_path to the sender" do
+ def perform
+ allow(fake_body).to receive(:to_path).and_return("/tmp/file.bin")
- wrapped = described_class.wrap(fake_body, transaction)
- expect(wrapped.to_path).to eq("/tmp/file.bin")
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect(wrapped.to_path).to eq("/tmp/file.bin")
+ end
- expect(transaction).to include_event(
- "name" => "process_response_body.rack",
- "title" => "Process Rack response body (#to_path)"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "process_response_body.rack",
+ "title" => "Process Rack response body (#to_path)"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect_collector_event(
+ "process_response_body.rack",
+ "Process Rack response body (#to_path)"
+ )
+ end
end
end
describe "with a body only supporting call()" do
let(:fake_body) { double(:call => nil) }
- it "wraps with appropriate class" do
+ it_in_both_modes "wraps with appropriate class" do
wrapped = described_class.wrap(fake_body, transaction)
expect(wrapped).to_not respond_to(:each)
expect(wrapped).to_not respond_to(:to_ary)
@@ -514,92 +1096,183 @@
expect(wrapped).to respond_to(:close)
end
- it "passes the stream into the call() of the body" do
- fake_rack_stream = double("stream")
- expect(fake_body).to receive(:call).with(fake_rack_stream)
+ describe "passes the stream into the call() of the body" do
+ def perform
+ fake_rack_stream = double("stream")
+ expect(fake_body).to receive(:call).with(fake_rack_stream)
- wrapped = described_class.wrap(fake_body, transaction)
- wrapped.call(fake_rack_stream)
+ wrapped = described_class.wrap(fake_body, transaction)
+ wrapped.call(fake_rack_stream)
+ end
- expect(transaction).to include_event(
- "name" => "process_response_body.rack",
- "title" => "Process Rack response body (#call)"
- )
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to include_event(
+ "name" => "process_response_body.rack",
+ "title" => "Process Rack response body (#call)"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ expect_collector_event(
+ "process_response_body.rack",
+ "Process Rack response body (#call)"
+ )
+ end
end
- it "sets the exception raised inside call() into the Appsignal transaction" do
- fake_rack_stream = double
- allow(fake_body).to receive(:call)
- .with(fake_rack_stream)
- .and_raise(ExampleException, "error message")
+ describe "sets the exception raised inside call() into the Appsignal transaction" do
+ def perform
+ fake_rack_stream = double
+ allow(fake_body).to receive(:call)
+ .with(fake_rack_stream)
+ .and_raise(ExampleException, "error message")
- wrapped = described_class.wrap(fake_body, transaction)
+ wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.call(fake_rack_stream)
- end.to raise_error(ExampleException, "error message")
+ expect do
+ wrapped.call(fake_rack_stream)
+ end.to raise_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+
+ expect(transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
- expect(transaction).to have_error("ExampleException", "error message")
+ expect_collector_error("ExampleException", "error message")
+ end
end
- it "doesn't report EPIPE error" do
- fake_rack_stream = double
- expect(fake_body).to receive(:call)
- .with(fake_rack_stream)
- .and_raise(Errno::EPIPE)
+ describe "doesn't report EPIPE error" do
+ def perform
+ fake_rack_stream = double
+ expect(fake_body).to receive(:call)
+ .with(fake_rack_stream)
+ .and_raise(Errno::EPIPE)
+
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.call(fake_rack_stream)
+ end.to raise_error(Errno::EPIPE)
+ end
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.call(fake_rack_stream)
- end.to raise_error(Errno::EPIPE)
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
- expect(transaction).to_not have_error
+ perform
+ expect_collector_no_error
+ end
end
- it "doesn't report ECONNRESET error" do
- fake_rack_stream = double
- expect(fake_body).to receive(:call)
- .with(fake_rack_stream)
- .and_raise(Errno::ECONNRESET)
+ describe "doesn't report ECONNRESET error" do
+ def perform
+ fake_rack_stream = double
+ expect(fake_body).to receive(:call)
+ .with(fake_rack_stream)
+ .and_raise(Errno::ECONNRESET)
+
+ wrapped = described_class.wrap(fake_body, transaction)
+ expect do
+ wrapped.call(fake_rack_stream)
+ end.to raise_error(Errno::ECONNRESET)
+ end
- wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.call(fake_rack_stream)
- end.to raise_error(Errno::ECONNRESET)
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
- expect(transaction).to_not have_error
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report EPIPE error from #call when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::EPIPE)
- fake_rack_stream = double
- allow(fake_body).to receive(:call)
- .with(fake_rack_stream)
- .and_raise(error)
+ describe "does not report EPIPE error from #call when it's the error cause" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::EPIPE)
+ fake_rack_stream = double
+ allow(fake_body).to receive(:call)
+ .with(fake_rack_stream)
+ .and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
+ wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.call(fake_rack_stream)
- end.to raise_error(StandardError, "error message")
+ expect do
+ wrapped.call(fake_rack_stream)
+ end.to raise_error(StandardError, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
- expect(transaction).to_not have_error
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
- it "does not report ECONNRESET error from #call when it's the error cause" do
- error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
- fake_rack_stream = double
- allow(fake_body).to receive(:call)
- .with(fake_rack_stream)
- .and_raise(error)
+ describe "does not report ECONNRESET error from #call when it's the error cause" do
+ def perform
+ error = error_with_cause(StandardError, "error message", Errno::ECONNRESET)
+ fake_rack_stream = double
+ allow(fake_body).to receive(:call)
+ .with(fake_rack_stream)
+ .and_raise(error)
- wrapped = described_class.wrap(fake_body, transaction)
+ wrapped = described_class.wrap(fake_body, transaction)
- expect do
- wrapped.call(fake_rack_stream)
- end.to raise_error(StandardError, "error message")
+ expect do
+ wrapped.call(fake_rack_stream)
+ end.to raise_error(StandardError, "error message")
+ end
- expect(transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ perform
+ expect(transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+ expect_collector_no_error
+ end
end
end
diff --git a/spec/lib/appsignal/rack/event_handler_spec.rb b/spec/lib/appsignal/rack/event_handler_spec.rb
index 65308e8eb..bdb86f6b5 100644
--- a/spec/lib/appsignal/rack/event_handler_spec.rb
+++ b/spec/lib/appsignal/rack/event_handler_spec.rb
@@ -5,6 +5,7 @@
"HTTP_X_REQUEST_START" => "t=#{queue_start_time.to_i}", # in milliseconds
"REQUEST_METHOD" => "POST",
"PATH_INFO" => "/path",
+ "HTTP_ACCEPT" => "application/json",
"QUERY_STRING" => "query_param1=value1&query_param2=value2",
"rack.session" => { "session1" => "value1", "session2" => "value2" },
"rack.input" => StringIO.new("post_param1=value1&post_param2=value2")
@@ -24,11 +25,14 @@
end
let(:rack_app) { lambda { |_env| [200, {}, ["Hello world!"]] } }
let(:appsignal_env) { :default }
- before do
- start_agent(:env => appsignal_env)
+ # Pass the example's AppSignal env through to the mode contexts' `start_agent`.
+ let(:start_agent_args) { { :env => appsignal_env } }
+
+ # `start_agent` resets the internal logger, so the test logger has to be
+ # installed from the example body, after the mode context starts the agent.
+ def use_test_logger
Appsignal.internal_logger = test_logger(log_stream)
end
- around { |example| keep_transactions { example.run } }
def on_start
event_handler_instance.on_start(request, response)
@@ -43,7 +47,8 @@ def on_error(error)
# `Rack::Events` middleware.
let(:event_handler_instance) { described_class.new }
- it "emits a warning about using it with Rack::Events" do
+ it_in_both_modes "emits a warning about using it with Rack::Events" do
+ use_test_logger
events = ::Rack::Events.new(rack_app, [event_handler_instance])
logs = capture_logs { events.call({}) }
@@ -55,7 +60,8 @@ def on_error(error)
end
context "When used via ::Appsignal::Rack::EventMiddleware" do
- it "does not emit a warning about using it with Rack::Events" do
+ it_in_both_modes "does not emit a warning about using it with Rack::Events" do
+ use_test_logger
expect(described_class).to receive(:new).and_call_original
event_middleware = Appsignal::Rack::EventMiddleware.new(rack_app)
@@ -69,61 +75,133 @@ def on_error(error)
end
describe "#on_start" do
- it "creates a new transaction" do
- expect { on_start }.to change { created_transactions.length }.by(1)
+ describe "creates a new transaction" do
+ def perform
+ on_start
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ transaction = last_transaction
+ expect(transaction).to have_id
+ expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
- transaction = last_transaction
- expect(transaction).to have_id
- expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ expect(Appsignal::Transaction.current).to eq(transaction)
+ end
- expect(Appsignal::Transaction.current).to eq(transaction)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ expect { perform }.to change { created_transactions.length }.by(1)
+
+ transaction = last_transaction
+ expect(Appsignal::Transaction.current).to eq(transaction)
+ # Finish the still-open root span so we can read the namespace it was
+ # opened with.
+ Appsignal::Transaction.complete_current!
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ expect(root_span.kind).to eq(:server)
+ end
+ end
+
+ describe "incoming trace context" do
+ let(:trace_id_hex) { "0af7651916cd43dd8448eb211c80319c" }
+ let(:span_id_hex) { "b7ad6b7169203331" }
+
+ it "continues the upstream trace when a traceparent is present", :collector_mode do
+ env["HTTP_TRACEPARENT"] = "00-#{trace_id_hex}-#{span_id_hex}-01"
+ start_collector_agent
+ on_start
+ Appsignal::Transaction.complete_current!
+
+ expect(root_span.hex_trace_id).to eq(trace_id_hex)
+ expect(root_span.parent_span_id.unpack1("H*")).to eq(span_id_hex)
+ end
+
+ it "starts a fresh root trace when no traceparent is present", :collector_mode do
+ start_collector_agent
+ on_start
+ Appsignal::Transaction.complete_current!
+
+ expect(root_span.parent_span_id).to eq(::OpenTelemetry::Trace::INVALID_SPAN_ID)
+ end
end
context "when not active" do
let(:appsignal_env) { :inactive_env }
- it "does not create a new transaction" do
+ it_in_both_modes "does not create a new transaction" do
+ use_test_logger
expect { on_start }.to_not(change { created_transactions.length })
end
end
context "when the handler is nested in another EventHandler" do
- it "does not create a new transaction in the nested EventHandler" do
+ it_in_both_modes "does not create a new transaction in the nested EventHandler" do
+ use_test_logger
on_start
expect { described_class.new.on_start(request, response) }
.to_not(change { created_transactions.length })
end
end
- it "registers transaction on the request environment" do
+ it_in_both_modes "registers transaction on the request environment" do
+ use_test_logger
on_start
expect(request.env[Appsignal::Rack::APPSIGNAL_TRANSACTION])
.to eq(last_transaction)
end
- it "registers an rack.after_reply callback that completes the transaction" do
- request.env[Appsignal::Rack::RACK_AFTER_REPLY] = []
- expect do
- on_start
- end.to change { request.env[Appsignal::Rack::RACK_AFTER_REPLY].length }.by(1)
+ describe "registers an rack.after_reply callback that completes the transaction" do
+ def perform
+ request.env[Appsignal::Rack::RACK_AFTER_REPLY] = []
+ expect do
+ on_start
+ end.to change { request.env[Appsignal::Rack::RACK_AFTER_REPLY].length }.by(1)
- expect(Appsignal::Transaction.current).to eq(last_transaction)
+ expect(Appsignal::Transaction.current).to eq(last_transaction)
- callback = request.env[Appsignal::Rack::RACK_AFTER_REPLY].first
- callback.call
+ callback = request.env[Appsignal::Rack::RACK_AFTER_REPLY].first
+ callback.call
+
+ expect(Appsignal::Transaction.current).to be_kind_of(Appsignal::Transaction::NilTransaction)
+ end
- expect(Appsignal::Transaction.current).to be_kind_of(Appsignal::Transaction::NilTransaction)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
- expect(last_transaction.ext.queue_start).to eq(queue_start_time)
- expect(last_transaction).to include_event(
- "name" => "process_request.rack",
- "title" => "callback: after_reply"
- )
+ expect(last_transaction.backend.queue_start).to eq(queue_start_time)
+ expect(last_transaction).to include_event(
+ "name" => "process_request.rack",
+ "title" => "callback: after_reply"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ queue_event = Array(root_span.events).find { |e| e.name == "appsignal.queue_start" }
+ expect(queue_event.attributes["appsignal.queue_start"]).to eq(queue_start_time.to_i)
+ event = event_spans.find do |span|
+ span.attributes["appsignal.category"] == "process_request.rack"
+ end
+ expect(event).not_to be_nil
+ expect(event.parent_span_id).to eq(root_span.span_id)
+ expect(event.name).to eq("callback: after_reply")
+ end
end
context "with error inside rack.after_reply handler" do
- before do
+ def trigger_after_reply_error
on_start
# A random spot we can access to raise an error for this test
expect(request.env[Appsignal::Rack::APPSIGNAL_TRANSACTION])
@@ -133,11 +211,17 @@ def on_error(error)
callback.call
end
- it "completes the transaction" do
+ it_in_both_modes "completes the transaction" do
+ use_test_logger
+ trigger_after_reply_error
+
expect(last_transaction).to be_completed
end
- it "logs an error" do
+ it_in_both_modes "logs an error" do
+ use_test_logger
+ trigger_after_reply_error
+
expect(logs).to contains_log(
:error,
"Error occurred in Appsignal::Rack::EventHandler's after_reply: " \
@@ -146,7 +230,8 @@ def on_error(error)
end
end
- it "logs errors from rack.after_reply callbacks" do
+ it_in_both_modes "logs errors from rack.after_reply callbacks" do
+ use_test_logger
on_start
expect(request.env[Appsignal::Rack::APPSIGNAL_TRANSACTION])
@@ -161,7 +246,8 @@ def on_error(error)
)
end
- it "logs an error in case of an error" do
+ it_in_both_modes "logs an error in case of an error" do
+ use_test_logger
expect(Appsignal::Transaction)
.to receive(:create).and_raise(ExampleStandardError, "oh no")
@@ -175,34 +261,99 @@ def on_error(error)
end
describe "#on_error" do
- it "reports the error" do
- on_start
- on_error(ExampleStandardError.new("the error"))
+ describe "reports the error" do
+ def perform
+ on_start
+ on_error(ExampleStandardError.new("the error"))
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
- expect(last_transaction).to have_error("ExampleStandardError", "the error")
+ expect(last_transaction).to have_error("ExampleStandardError", "the error")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+ # The error is recorded on the still-open span; finish it so the
+ # exception event exports.
+ Appsignal::Transaction.complete_current!
+
+ # The error is set while the `process_request.rack` event span is the
+ # current span (on_start opens it; it is not finished before on_error),
+ # so the exception event rides on that event span, not the root span.
+ error_span = span_exporter.finished_spans.find do |span|
+ Array(span.events).any? { |e| e.name == "exception" }
+ end
+ expect(error_span).not_to be_nil
+ event = error_span.events.find { |e| e.name == "exception" }
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("the error")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(error_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
context "when not active" do
let(:appsignal_env) { :inactive_env }
- it "does not report the transaction" do
- on_start
- on_error(ExampleStandardError.new("the error"))
+ describe "does not report the transaction" do
+ def perform
+ on_start
+ on_error(ExampleStandardError.new("the error"))
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
- expect(last_transaction).to_not have_error
+ expect(exception_events).to be_empty
+ end
end
end
context "when the handler is nested in another EventHandler" do
- it "does not report the error on the transaction" do
- on_start
- described_class.new.on_error(request, response, ExampleStandardError.new("the error"))
+ describe "does not report the error on the transaction" do
+ def perform
+ on_start
+ described_class.new.on_error(request, response, ExampleStandardError.new("the error"))
+ end
- expect(last_transaction).to_not have_error
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(exception_events).to be_empty
+ end
end
end
- it "logs an error in case of an internal error" do
+ it_in_both_modes "logs an error in case of an internal error" do
+ use_test_logger
on_start
expect(request.env[Appsignal::Rack::APPSIGNAL_TRANSACTION])
@@ -224,84 +375,76 @@ def on_finish(given_request = request, given_response = response)
event_handler_instance.on_finish(given_request, given_response)
end
- it "doesn't do anything without a transaction" do
- on_start
-
- request.env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = nil
-
- on_finish
-
- expect(last_transaction).to_not have_action
- expect(last_transaction).to_not include_events
- expect(last_transaction).to include("sample_data" => {})
- expect(last_transaction).to_not be_completed
- end
-
- context "when not active" do
- let(:appsignal_env) { :inactive_env }
-
- it "doesn't do anything" do
- request.env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = http_request_transaction
+ describe "doesn't do anything without a transaction" do
+ def perform
+ on_start
+ request.env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = nil
on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
expect(last_transaction).to_not have_action
expect(last_transaction).to_not include_events
expect(last_transaction).to include("sample_data" => {})
expect(last_transaction).to_not be_completed
end
- end
-
- it "sets params on the transaction" do
- on_start
- on_finish
-
- expect(last_transaction).to include_params(
- "query_param1" => "value1",
- "query_param2" => "value2",
- "post_param1" => "value1",
- "post_param2" => "value2"
- )
- end
- it "sets headers on the transaction" do
- on_start
- on_finish
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
- expect(last_transaction).to include_environment(
- "REQUEST_METHOD" => "POST",
- "PATH_INFO" => "/path"
- )
+ expect(last_transaction).to_not be_completed
+ expect(root_span).to be_nil
+ expect(event_spans).to be_empty
+ end
end
- it "sets session data on the transaction" do
- on_start
- on_finish
+ context "when not active" do
+ let(:appsignal_env) { :inactive_env }
- expect(last_transaction).to include_session_data(
- "session1" => "value1",
- "session2" => "value2"
- )
- end
+ describe "doesn't do anything" do
+ def perform
+ request.env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = http_request_transaction
+ on_finish
+ end
- it "sets the queue start time on the transaction" do
- on_start
- on_finish
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
- expect(last_transaction).to have_queue_start(queue_start_time)
- end
+ expect(last_transaction).to_not have_action
+ expect(last_transaction).to_not include_events
+ expect(last_transaction).to include("sample_data" => {})
+ expect(last_transaction).to_not be_completed
+ end
- it "completes the transaction" do
- on_start
- on_finish
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
- expect(last_transaction).to_not have_action
- expect(last_transaction).to be_completed
+ expect(last_transaction).to_not be_completed
+ expect(event_spans).to be_empty
+ end
+ end
end
- context "without a response" do
- it "sets params on the transaction" do
+ describe "sets params on the transaction" do
+ def perform
on_start
on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
expect(last_transaction).to include_params(
"query_param1" => "value1",
@@ -311,9 +454,31 @@ def on_finish(given_request = request, given_response = response)
)
end
- it "sets headers on the transaction" do
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ params = JSON.parse(root_span.attributes["appsignal.request.payload"])
+ expect(params).to include(
+ "query_param1" => "value1",
+ "query_param2" => "value2",
+ "post_param1" => "value1",
+ "post_param2" => "value2"
+ )
+ end
+ end
+
+ describe "sets headers on the transaction" do
+ def perform
on_start
on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
expect(last_transaction).to include_environment(
"REQUEST_METHOD" => "POST",
@@ -321,9 +486,28 @@ def on_finish(given_request = request, given_response = response)
)
end
- it "sets session data on the transaction" do
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ # Only true HTTP headers map to `http.request.header.*`; the CGI vars
+ # REQUEST_METHOD/PATH_INFO are intentionally dropped.
+ expect(root_span.attributes["http.request.header.accept"]).to eq("application/json")
+ expect(root_span.attributes.keys).to_not include("http.request.header.request-method")
+ end
+ end
+
+ describe "sets session data on the transaction" do
+ def perform
on_start
on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
expect(last_transaction).to include_session_data(
"session1" => "value1",
@@ -331,71 +515,334 @@ def on_finish(given_request = request, given_response = response)
)
end
- it "sets the queue start time on the transaction" do
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ session = JSON.parse(root_span.attributes["appsignal.request.session_data"])
+ expect(session).to include(
+ "session1" => "value1",
+ "session2" => "value2"
+ )
+ end
+ end
+
+ describe "sets the queue start time on the transaction" do
+ def perform
on_start
on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
expect(last_transaction).to have_queue_start(queue_start_time)
end
- it "completes the transaction" do
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ # `set_queue_start` is an intentional no-op in collector mode.
+ expect(last_transaction).to_not have_queue_start
+ end
+ end
+
+ describe "completes the transaction" do
+ def perform
on_start
- on_finish(request, nil)
+ on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
- # The action is not set on purpose, as we can't set a normalized route
- # It requires the app to set an action name
expect(last_transaction).to_not have_action
expect(last_transaction).to be_completed
end
- it "does not set a response_status tag" do
- on_start
- on_finish(request, nil)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
- expect(last_transaction).to_not include_tags("response_status" => anything)
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ expect(last_transaction).to be_completed
end
+ end
- it "does not report a response_status counter metric" do
- expect(Appsignal).to_not receive(:increment_counter)
- .with(:response_status, anything, anything)
+ context "without a response" do
+ describe "sets params on the transaction" do
+ def perform
+ on_start
+ on_finish
+ end
- on_start
- on_finish(request, nil)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to include_params(
+ "query_param1" => "value1",
+ "query_param2" => "value2",
+ "post_param1" => "value1",
+ "post_param2" => "value2"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ params = JSON.parse(root_span.attributes["appsignal.request.payload"])
+ expect(params).to include(
+ "query_param1" => "value1",
+ "query_param2" => "value2",
+ "post_param1" => "value1",
+ "post_param2" => "value2"
+ )
+ end
end
- context "with an error previously recorded by on_error" do
- it "sets response status 500 as a tag" do
+ describe "sets headers on the transaction" do
+ def perform
+ on_start
+ on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to include_environment(
+ "REQUEST_METHOD" => "POST",
+ "PATH_INFO" => "/path"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("application/json")
+ expect(root_span.attributes.keys).to_not include("http.request.header.request-method")
+ end
+ end
+
+ describe "sets session data on the transaction" do
+ def perform
+ on_start
+ on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to include_session_data(
+ "session1" => "value1",
+ "session2" => "value2"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ session = JSON.parse(root_span.attributes["appsignal.request.session_data"])
+ expect(session).to include(
+ "session1" => "value1",
+ "session2" => "value2"
+ )
+ end
+ end
+
+ describe "sets the queue start time on the transaction" do
+ def perform
+ on_start
+ on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to have_queue_start(queue_start_time)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ expect(last_transaction).to_not have_queue_start
+ end
+ end
+
+ describe "completes the transaction" do
+ # The action is not set on purpose, as we can't set a normalized route.
+ # It requires the app to set an action name.
+ def perform
on_start
- on_error(ExampleStandardError.new("the error"))
on_finish(request, nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
- expect(last_transaction).to include_tags("response_status" => 500)
+ expect(last_transaction).to_not have_action
+ expect(last_transaction).to be_completed
end
- it "increments the response status counter for response status 500" do
- expect(Appsignal).to receive(:increment_counter)
- .with(:response_status, 1, :status => 500, :namespace => :web)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ expect(last_transaction).to be_completed
+ end
+ end
+
+ describe "does not set a response_status tag" do
+ def perform
on_start
- on_error(ExampleStandardError.new("the error"))
on_finish(request, nil)
end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to_not include_tags("response_status" => anything)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ expect(root_span.attributes.keys).to_not include("appsignal.tag.response_status")
+ end
+ end
+
+ describe "does not report a response_status counter metric" do
+ def perform
+ on_start
+ on_finish(request, nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ expect(Appsignal).to_not receive(:increment_counter)
+ .with(:response_status, anything, anything)
+
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ expect(metric_snapshot("response_status")).to be_nil
+ end
+ end
+
+ context "with an error previously recorded by on_error" do
+ describe "sets response status 500 as a tag" do
+ def perform
+ on_start
+ on_error(ExampleStandardError.new("the error"))
+ on_finish(request, nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to include_tags("response_status" => 500)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ expect(root_span.attributes["appsignal.tag.response_status"]).to eq(500)
+ end
+ end
+
+ describe "increments the response status counter for response status 500" do
+ def perform
+ on_start
+ on_error(ExampleStandardError.new("the error"))
+ on_finish(request, nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ expect(Appsignal).to receive(:increment_counter)
+ .with(:response_status, 1, :status => 500, :namespace => :web)
+
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ snapshot = metric_snapshot("response_status")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.data_points.first.value).to eq(1.0)
+ expect(snapshot.data_points.first.attributes).to eq(
+ "status" => 500,
+ "namespace" => "web"
+ )
+ end
+ end
end
end
context "with error inside on_finish handler" do
- before do
+ def trigger_on_finish_error
on_start
# A random spot we can access to raise an error for this test
expect(Appsignal).to receive(:increment_counter).and_raise(ExampleStandardError, "oh no")
on_finish
end
- it "completes the transaction" do
+ it_in_both_modes "completes the transaction" do
+ use_test_logger
+ trigger_on_finish_error
+
expect(last_transaction).to be_completed
end
- it "logs an error" do
+ it_in_both_modes "logs an error" do
+ use_test_logger
+ trigger_on_finish_error
+
expect(logs).to contains_log(
:error,
"Error occurred in Appsignal::Rack::EventHandler#on_finish: ExampleStandardError: oh no"
@@ -404,73 +851,176 @@ def on_finish(given_request = request, given_response = response)
end
context "when the handler is nested in another EventHandler" do
- it "does not complete the transaction" do
- on_start
- described_class.new.on_finish(request, response)
+ describe "does not complete the transaction" do
+ def perform
+ on_start
+ described_class.new.on_finish(request, response)
+ end
- expect(last_transaction).to_not have_action
- expect(last_transaction).to_not include_metadata
- expect(last_transaction).to_not include_events
- expect(last_transaction.to_h).to include("sample_data" => {})
- expect(last_transaction).to_not be_completed
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to_not have_action
+ expect(last_transaction).to_not include_metadata
+ expect(last_transaction).to_not include_events
+ expect(last_transaction.to_h).to include("sample_data" => {})
+ expect(last_transaction).to_not be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ expect(last_transaction).to_not be_completed
+ expect(root_span).to be_nil
+ expect(event_spans).to be_empty
+ end
end
end
- it "doesn't set the action name if already set" do
- on_start
- last_transaction.set_action("My action")
- on_finish
+ describe "doesn't set the action name if already set" do
+ def perform
+ on_start
+ last_transaction.set_action("My action")
+ on_finish
+ end
- expect(last_transaction).to have_action("My action")
- end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
- it "finishes the process_request.rack event" do
- on_start
- on_finish
+ expect(last_transaction).to have_action("My action")
+ end
- expect(last_transaction).to include_event(
- "name" => "process_request.rack",
- "title" => "callback: on_finish"
- )
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ expect(root_span.name).to eq("My action")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("My action")
+ end
end
- context "with response" do
- it "sets the response status as a tag" do
+ describe "finishes the process_request.rack event" do
+ def perform
on_start
on_finish
+ end
- expect(last_transaction).to include_tags("response_status" => 200)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to include_event(
+ "name" => "process_request.rack",
+ "title" => "callback: on_finish"
+ )
end
- it "increments the response status counter for response status" do
- expect(Appsignal).to receive(:increment_counter)
- .with(:response_status, 1, :status => 200, :namespace => :web)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
- on_start
- on_finish
+ event = event_spans.find do |span|
+ span.attributes["appsignal.category"] == "process_request.rack"
+ end
+ expect(event).not_to be_nil
+ expect(event.parent_span_id).to eq(root_span.span_id)
+ expect(event.name).to eq("callback: on_finish")
end
+ end
- context "with an error previously recorded by on_error" do
- it "sets response status from the response as a tag" do
+ context "with response" do
+ describe "sets the response status as a tag" do
+ def perform
on_start
- on_error(ExampleStandardError.new("the error"))
on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
expect(last_transaction).to include_tags("response_status" => 200)
end
- it "increments the response status counter based on the response" do
- expect(Appsignal).to receive(:increment_counter)
- .with(:response_status, 1, :status => 200, :namespace => :web)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
- on_start
- on_error(ExampleStandardError.new("the error"))
- on_finish
+ expect(root_span.attributes["appsignal.tag.response_status"]).to eq(200)
+ end
+ end
+
+ context "with an error previously recorded by on_error" do
+ describe "sets response status from the response as a tag" do
+ def perform
+ on_start
+ on_error(ExampleStandardError.new("the error"))
+ on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ perform
+
+ expect(last_transaction).to include_tags("response_status" => 200)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ expect(root_span.attributes["appsignal.tag.response_status"]).to eq(200)
+ end
+ end
+
+ describe "increments the response status counter based on the response" do
+ def perform
+ on_start
+ on_error(ExampleStandardError.new("the error"))
+ on_finish
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ use_test_logger
+ expect(Appsignal).to receive(:increment_counter)
+ .with(:response_status, 1, :status => 200, :namespace => :web)
+
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ use_test_logger
+ perform
+
+ snapshot = metric_snapshot("response_status")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.data_points.first.value).to eq(1.0)
+ expect(snapshot.data_points.first.attributes).to eq(
+ "status" => 200,
+ "namespace" => "web"
+ )
+ end
end
end
end
- it "logs an error in case of an error" do
+ it_in_both_modes "logs an error in case of an error" do
+ use_test_logger
# A random spot we can access to raise an error for this test
expect(Appsignal).to receive(:increment_counter).and_raise(ExampleStandardError, "oh no")
@@ -484,3 +1034,88 @@ def on_finish(given_request = request, given_response = response)
end
end
end
+
+# Separate top-level describe so it doesn't inherit the parameterized
+# `before { start_agent(:env => appsignal_env) }` above (which would clobber
+# collector mode); `start_agent` comes from the mode contexts. The agent has no
+# in-memory metric readout, so agent mode keeps the `increment_counter` mock
+# while collector mode asserts the counter reaches the OpenTelemetry backend.
+describe Appsignal::Rack::EventHandler, "response status counter" do
+ let(:env) do
+ {
+ "REQUEST_METHOD" => "GET",
+ "PATH_INFO" => "/path",
+ "rack.input" => StringIO.new("")
+ }
+ end
+ let(:request) { Rack::Request.new(env) }
+ let(:response) { Rack::Events::BufferedResponse.new(200, {}, ["body"]) }
+ let(:event_handler_instance) do
+ described_class.new.tap do |handler|
+ handler.using_appsignal_event_middleware = true
+ end
+ end
+
+ describe "for a successful request" do
+ def perform
+ event_handler_instance.on_start(request, response)
+ event_handler_instance.on_finish(request, response)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ expect(Appsignal).to receive(:increment_counter)
+ .with(:response_status, 1, :status => 200, :namespace => :web)
+
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ snapshot = metric_snapshot("response_status")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.data_points.first.value).to eq(1.0)
+ expect(snapshot.data_points.first.attributes).to eq(
+ "status" => 200,
+ "namespace" => "web"
+ )
+ end
+ end
+
+ describe "for a request that errors" do
+ # No response, and an error recorded by `on_error`, so the status comes
+ # from the error (500) rather than the response.
+ def perform
+ event_handler_instance.on_start(request, response)
+ event_handler_instance.on_error(request, response, ExampleStandardError.new("the error"))
+ event_handler_instance.on_finish(request, nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+
+ expect(Appsignal).to receive(:increment_counter)
+ .with(:response_status, 1, :status => 500, :namespace => :web)
+
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+
+ perform
+
+ snapshot = metric_snapshot("response_status")
+ expect(snapshot).not_to be_nil
+ expect(snapshot.data_points.first.value).to eq(1.0)
+ expect(snapshot.data_points.first.attributes).to eq(
+ "status" => 500,
+ "namespace" => "web"
+ )
+ end
+ end
+end
diff --git a/spec/lib/appsignal/rack/grape_middleware_spec.rb b/spec/lib/appsignal/rack/grape_middleware_spec.rb
index a43a36c62..007ef2b96 100644
--- a/spec/lib/appsignal/rack/grape_middleware_spec.rb
+++ b/spec/lib/appsignal/rack/grape_middleware_spec.rb
@@ -14,14 +14,7 @@
let(:env) do
Rack::MockRequest.env_for("/ping", :method => "POST")
end
- let(:transaction) { http_request_transaction }
- before do
- stub_const("GrapeExample::Api", app)
- start_agent
- end
- around do |example|
- keep_transactions { example.run }
- end
+ before { stub_const("GrapeExample::Api", app) }
def make_request(env)
app.call(env)
@@ -44,10 +37,30 @@ def make_request_with_exception(env, exception_class, exception_message)
end
end
- it "sets the error" do
- make_request_with_exception(env, ExampleException, "error message")
+ describe "sets the error" do
+ def perform
+ make_request_with_exception(env, ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to have_error("ExampleException", "error message")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
context "with env['grape.skip_appsignal_error'] = true" do
@@ -62,10 +75,24 @@ def make_request_with_exception(env, exception_class, exception_message)
end
end
- it "does not add the error" do
- make_request_with_exception(env, ExampleException, "error message")
+ describe "does not add the error" do
+ def perform
+ make_request_with_exception(env, ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to_not have_error
+ expect(exception_events).to be_empty
+ end
end
end
end
@@ -83,11 +110,30 @@ def make_request_with_exception(env, exception_class, exception_message)
Rack::MockRequest.env_for("/hello", :method => "GET")
end
- it "sets non-unique route path" do
- make_request(env)
+ describe "sets non-unique route path" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_action("GET::GrapeExample::Api#/hello")
+ expect(last_transaction).to include_metadata("path" => "/hello", "method" => "GET")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to have_action("GET::GrapeExample::Api#/hello")
- expect(last_transaction).to include_metadata("path" => "/hello", "method" => "GET")
+ expect(root_span.name).to eq("GET::GrapeExample::Api#/hello")
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("GET::GrapeExample::Api#/hello")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/hello")
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("GET")
+ end
end
end
@@ -109,15 +155,62 @@ def make_request_with_exception(env, exception_class, exception_message)
Rack::MockRequest.env_for("/users/123", :method => "GET")
end
- it "sets non-unique route_param path" do
- make_request(env)
+ describe "sets non-unique route_param path" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_action("GET::GrapeExample::Api#/users/:id/")
+ expect(last_transaction).to include_metadata("path" => "/users/:id/", "method" => "GET")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to have_action("GET::GrapeExample::Api#/users/:id/")
- expect(last_transaction).to include_metadata("path" => "/users/:id/", "method" => "GET")
+ expect(root_span.name).to eq("GET::GrapeExample::Api#/users/:id/")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("GET::GrapeExample::Api#/users/:id/")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/users/:id/")
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("GET")
+ end
end
end
context "with namespaced path" do
+ shared_examples "sets the namespaced path" do |action|
+ describe "sets namespaced path" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_action(action)
+ expect(last_transaction).to include_metadata(
+ "path" => "/v1/beta/ping",
+ "method" => "POST"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.name).to eq(action)
+ expect(root_span.attributes["appsignal.action_name"]).to eq(action)
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/v1/beta/ping")
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("POST")
+ end
+ end
+ end
+
context "with symbols" do
let(:app) do
Class.new(::Grape::API) do
@@ -136,13 +229,7 @@ def make_request_with_exception(env, exception_class, exception_message)
Rack::MockRequest.env_for("/v1/beta/ping", :method => "POST")
end
- it "sets namespaced path" do
- make_request(env)
-
- expect(last_transaction).to have_action("POST::GrapeExample::Api#/v1/beta/ping")
- expect(last_transaction).to include_metadata("path" => "/v1/beta/ping",
- "method" => "POST")
- end
+ include_examples "sets the namespaced path", "POST::GrapeExample::Api#/v1/beta/ping"
end
context "with strings" do
@@ -164,15 +251,7 @@ def make_request_with_exception(env, exception_class, exception_message)
Rack::MockRequest.env_for("/v1/beta/ping", :method => "POST")
end
- it "sets namespaced path" do
- make_request(env)
-
- expect(last_transaction).to have_action("POST::GrapeExample::Api#/v1/beta/ping")
- expect(last_transaction).to include_metadata(
- "path" => "/v1/beta/ping",
- "method" => "POST"
- )
- end
+ include_examples "sets the namespaced path", "POST::GrapeExample::Api#/v1/beta/ping"
end
context "with / prefix" do
@@ -193,13 +272,7 @@ def make_request_with_exception(env, exception_class, exception_message)
Rack::MockRequest.env_for("/v1/beta/ping", :method => "POST")
end
- it "sets namespaced path" do
- make_request(env)
-
- expect(last_transaction).to have_action("POST::GrapeExample::Api#/v1/beta/ping")
- expect(last_transaction).to include_metadata("path" => "/v1/beta/ping",
- "method" => "POST")
- end
+ include_examples "sets the namespaced path", "POST::GrapeExample::Api#/v1/beta/ping"
end
end
end
diff --git a/spec/lib/appsignal/rack/hanami_middleware_spec.rb b/spec/lib/appsignal/rack/hanami_middleware_spec.rb
index eccd441fa..ea7e90598 100644
--- a/spec/lib/appsignal/rack/hanami_middleware_spec.rb
+++ b/spec/lib/appsignal/rack/hanami_middleware_spec.rb
@@ -14,9 +14,6 @@
end
let(:middleware) { Appsignal::Rack::HanamiMiddleware.new(app, {}) }
- before { start_agent }
- around { |example| keep_transactions { example.run } }
-
def make_request(env)
if DependencyHelper.hanami2_2_present?
instance =
@@ -31,34 +28,96 @@ def self.name
end
context "without params" do
- it "sets no request parameters on the transaction" do
- make_request(env)
+ describe "sets no request parameters on the transaction" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to_not include_params
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to_not include_params
+ expect(root_span.attributes.keys).to_not include("appsignal.request.payload")
+ end
end
end
context "with params" do
let(:router_params) { { "param1" => "value1", "param2" => "value2" } }
- it "sets request parameters on the transaction" do
- make_request(env)
+ describe "sets request parameters on the transaction" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to include_params("param1" => "value1", "param2" => "value2")
+ end
- expect(last_transaction).to include_params("param1" => "value1", "param2" => "value2")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.kind).to eq(:server)
+ params = JSON.parse(root_span.attributes["appsignal.request.payload"])
+ expect(params).to include("param1" => "value1", "param2" => "value2")
+ end
end
end
- it "reports a process_action.hanami event" do
- make_request(env)
+ describe "reports a process_action.hanami event" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to include_event("name" => "process_action.hanami")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to include_event("name" => "process_action.hanami")
+ span = event_spans.find { |s| s.name == "process_action.hanami" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ end
end
if DependencyHelper.hanami2_2_present?
- it "sets action name on the transaction" do
- make_request(env)
+ describe "sets action name on the transaction" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_action("HanamiApp::Actions::Books::Index")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to have_action("HanamiApp::Actions::Books::Index")
+ expect(root_span.name).to eq("HanamiApp::Actions::Books::Index")
+ expect(root_span.attributes["appsignal.action_name"])
+ .to eq("HanamiApp::Actions::Books::Index")
+ end
end
end
end
diff --git a/spec/lib/appsignal/rack/instrumentation_middleware_spec.rb b/spec/lib/appsignal/rack/instrumentation_middleware_spec.rb
index 6dffe3235..c53166b27 100644
--- a/spec/lib/appsignal/rack/instrumentation_middleware_spec.rb
+++ b/spec/lib/appsignal/rack/instrumentation_middleware_spec.rb
@@ -3,36 +3,80 @@
let(:env) { Rack::MockRequest.env_for("/some/path") }
let(:middleware) { described_class.new(app, {}) }
- before { start_agent }
- around { |example| keep_transactions { example.run } }
-
def make_request(env)
middleware.call(env)
end
context "without an exception" do
- it "reports a process_request_middleware.rack event" do
- make_request(env)
+ describe "reports a process_request_middleware.rack event" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to include_event("name" => "process_request_middleware.rack")
+ end
- expect(last_transaction).to include_event("name" => "process_request_middleware.rack")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(event_spans.map(&:name)).to include("process_request_middleware.rack")
+ expect(root_span.kind).to eq(:server)
+ span = event_spans.find { |s| s.name == "process_request_middleware.rack" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ end
end
end
context "with custom action name" do
let(:app) { DummyApp.new { |_env| Appsignal.set_action("MyAction") } }
- it "reports the custom action name" do
- make_request(env)
+ describe "reports the custom action name" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
- expect(last_transaction).to have_action("MyAction")
+ expect(last_transaction).to have_action("MyAction")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.name).to eq("MyAction")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MyAction")
+ end
end
end
context "without action name metadata" do
- it "reports no action name" do
- make_request(env)
+ describe "reports no action name" do
+ def perform
+ make_request(env)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to_not have_action
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
end
end
diff --git a/spec/lib/appsignal/rack/rails_instrumentation_spec.rb b/spec/lib/appsignal/rack/rails_instrumentation_spec.rb
index 5b28b5be0..55850bee7 100644
--- a/spec/lib/appsignal/rack/rails_instrumentation_spec.rb
+++ b/spec/lib/appsignal/rack/rails_instrumentation_spec.rb
@@ -26,9 +26,10 @@ class MockController; end
)
end
let(:middleware) { Appsignal::Rack::RailsInstrumentation.new(app, {}) }
- around { |example| keep_transactions { example.run } }
- before do
- start_agent
+
+ # The middleware wraps an existing (parent) transaction, so it must be built
+ # after the agent starts; register it from the example body, not a `before`.
+ def setup_transaction
env[Appsignal::Rack::APPSIGNAL_TRANSACTION] = transaction
end
@@ -42,14 +43,49 @@ def make_request_with_error(error_class, error_message)
end
context "with a request that doesn't raise an error" do
- before { make_request }
+ describe "calls the next middleware in the stack" do
+ def perform
+ setup_transaction
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(app).to be_called
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ # The middleware leaves the parent open; finish it to export the span.
+ transaction.complete
- it "calls the next middleware in the stack" do
- expect(app).to be_called
+ expect(app).to be_called
+ end
end
- it "does not instrument an event" do
- expect(last_transaction).to_not include_events
+ describe "does not instrument an event" do
+ def perform
+ setup_transaction
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to_not include_events
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(event_spans).to be_empty
+ end
end
end
@@ -57,69 +93,206 @@ def make_request_with_error(error_class, error_message)
let(:app) do
DummyApp.new { |_env| raise ExampleException, "error message" }
end
- before do
- make_request_with_error(ExampleException, "error message")
- end
- it "calls the next middleware in the stack" do
- expect(app).to be_called
+ describe "calls the next middleware in the stack" do
+ def perform
+ setup_transaction
+ make_request_with_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(app).to be_called
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(app).to be_called
+ end
end
- it "reports the error on the transaction" do
- expect(last_transaction).to have_error("ExampleException", "error message")
+ describe "reports the error on the transaction" do
+ def perform
+ setup_transaction
+ make_request_with_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
- it "sets the controller action as the action name" do
- make_request
+ describe "sets the controller action as the action name" do
+ def perform
+ setup_transaction
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ expect(last_transaction).to have_action("MockController#index")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
- expect(last_transaction).to have_action("MockController#index")
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ expect(root_span.name).to eq("MockController#index")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MockController#index")
+ end
end
- it "sets request metadata on the transaction" do
- make_request
+ describe "sets request metadata on the transaction" do
+ def perform
+ setup_transaction
+ make_request
+ end
- expect(last_transaction).to include_metadata(
- "method" => "GET",
- "path" => "/blog"
- )
- expect(last_transaction).to include_tags("request_id" => "request_id123")
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to include_metadata(
+ "method" => "GET",
+ "path" => "/blog"
+ )
+ expect(last_transaction).to include_tags("request_id" => "request_id123")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ # Metadata and tags are both emitted as `appsignal.tag.*` attributes.
+ expect(root_span.attributes["appsignal.tag.method"]).to eq("GET")
+ expect(root_span.attributes["appsignal.tag.path"]).to eq("/blog")
+ expect(root_span.attributes["appsignal.tag.request_id"]).to eq("request_id123")
+ end
end
- it "reports Rails filter parameters" do
- make_request
+ describe "reports Rails filter parameters" do
+ def perform
+ setup_transaction
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
- expect(last_transaction).to include_params(
- "controller" => "blog_posts",
- "action" => "show",
- "id" => "1",
- "my_custom_param" => "[FILTERED]",
- "password" => "[FILTERED]"
- )
+ expect(last_transaction).to include_params(
+ "controller" => "blog_posts",
+ "action" => "show",
+ "id" => "1",
+ "my_custom_param" => "[FILTERED]",
+ "password" => "[FILTERED]"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ params = JSON.parse(root_span.attributes["appsignal.request.payload"])
+ expect(params).to include(
+ "controller" => "blog_posts",
+ "action" => "show",
+ "id" => "1",
+ "my_custom_param" => "[FILTERED]",
+ "password" => "[FILTERED]"
+ )
+ end
end
context "with an invalid HTTP request method" do
- it "does not store the invalid HTTP request method" do
- env[:request_method] = "FOO"
- env["REQUEST_METHOD"] = "FOO"
- logs = capture_logs { make_request }
-
- expect(last_transaction).to_not include_metadata("method" => anything)
- expect(logs).to contains_log(
- :error,
- "Exception while fetching the HTTP request method: "
- )
+ describe "does not store the invalid HTTP request method" do
+ def perform
+ setup_transaction
+ env[:request_method] = "FOO"
+ env["REQUEST_METHOD"] = "FOO"
+ capture_logs { make_request }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ logs = perform
+
+ expect(last_transaction).to_not include_metadata("method" => anything)
+ expect(logs).to contains_log(
+ :error,
+ "Exception while fetching the HTTP request method: "
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ logs = perform
+ transaction.complete
+
+ expect(root_span.attributes.keys).to_not include("appsignal.tag.method")
+ expect(logs).to contains_log(
+ :error,
+ "Exception while fetching the HTTP request method: "
+ )
+ end
end
end
context "with a request path that's not a route" do
- it "doesn't set an action name" do
- env[:path] = "/unknown-route"
- env["action_controller.instance"] = nil
- make_request
+ describe "doesn't set an action name" do
+ def perform
+ setup_transaction
+ env[:path] = "/unknown-route"
+ env["action_controller.instance"] = nil
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- expect(last_transaction).to_not have_action
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
end
end
diff --git a/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb b/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb
index 165317ee5..343ea40ce 100644
--- a/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb
+++ b/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb
@@ -21,24 +21,17 @@ def make_request_with_error(error)
end
let(:middleware) { Appsignal::Rack::SinatraInstrumentation.new(app) }
- before { start_agent }
- around do |example|
- keep_transactions { example.run }
- end
-
describe "#call" do
before { allow(middleware).to receive(:raw_payload).and_return({}) }
- it "doesn't instrument requests" do
+ it_in_both_modes "doesn't instrument requests" do
expect { make_request }.to_not(change { created_transactions.count })
end
end
describe ".settings" do
- subject { middleware.settings }
-
- it "returns the app's settings" do
- expect(subject).to eq(app.settings)
+ it_in_both_modes "returns the app's settings" do
+ expect(middleware.settings).to eq(app.settings)
end
end
end
@@ -55,14 +48,14 @@ def make_request_with_error(error)
let(:options) { {} }
let(:middleware) { Appsignal::Rack::SinatraBaseInstrumentation.new(app, options) }
- before { start_agent(:env => appsignal_env) }
- around { |example| keep_transactions { example.run } }
+ # Pass the example's Appsignal env through to the mode contexts' `start_agent`.
+ let(:start_agent_args) { { :env => appsignal_env } }
describe "#initialize" do
context "with no settings method in the Sinatra app" do
let(:app) { double(:call => true) }
- it "does not raise errors" do
+ it_in_both_modes "does not raise errors" do
expect(middleware.raise_errors_on).to be(false)
end
end
@@ -70,7 +63,7 @@ def make_request_with_error(error)
context "with no raise_errors setting in the Sinatra app" do
let(:app) { double(:call => true, :settings => double) }
- it "does not raise errors" do
+ it_in_both_modes "does not raise errors" do
expect(middleware.raise_errors_on).to be(false)
end
end
@@ -78,7 +71,7 @@ def make_request_with_error(error)
context "with raise_errors turned off in the Sinatra app" do
let(:app) { double(:call => true, :settings => double(:raise_errors => false)) }
- it "raises errors" do
+ it_in_both_modes "raises errors" do
expect(middleware.raise_errors_on).to be(false)
end
end
@@ -86,7 +79,7 @@ def make_request_with_error(error)
context "with raise_errors turned on in the Sinatra app" do
let(:app) { double(:call => true, :settings => double(:raise_errors => true)) }
- it "raises errors" do
+ it_in_both_modes "raises errors" do
expect(middleware.raise_errors_on).to be(true)
end
end
@@ -98,11 +91,11 @@ def make_request_with_error(error)
context "when appsignal is not active" do
let(:appsignal_env) { :inactive_env }
- it "does not instrument requests" do
+ it_in_both_modes "does not instrument requests" do
expect { make_request }.to_not(change { created_transactions.count })
end
- it "calls the next middleware in the stack" do
+ it_in_both_modes "calls the next middleware in the stack" do
make_request
expect(app).to have_received(:call).with(env)
@@ -111,16 +104,48 @@ def make_request_with_error(error)
context "when appsignal is active" do
context "without an error" do
- it "creates a transaction for the request" do
- expect { make_request }.to(change { created_transactions.count }.by(1))
+ describe "creates a transaction for the request" do
+ def perform
+ make_request
+ end
- expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ expect(root_span.kind).to eq(:server)
+ end
end
- it "reports a process_action.sinatra event" do
- make_request
+ describe "reports a process_action.sinatra event" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to include_event("name" => "process_action.sinatra")
+ end
- expect(last_transaction).to include_event("name" => "process_action.sinatra")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ span = event_spans.find { |s| s.name == "process_action.sinatra" }
+ expect(span).not_to be_nil
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ end
end
end
@@ -128,29 +153,78 @@ def make_request_with_error(error)
let(:error) { ExampleException.new("error message") }
before { env["sinatra.error"] = error }
- it "creates a transaction for the request" do
- expect { make_request }.to(change { created_transactions.count }.by(1))
+ describe "creates a transaction for the request" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ end
- expect(last_transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ end
end
context "when raise_errors is off" do
let(:settings) { double(:raise_errors => false) }
- it "records the error" do
- make_request
+ describe "records the error" do
+ def perform
+ make_request
+ end
- expect(last_transaction).to have_error("ExampleException", "error message")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
end
context "when raise_errors is on" do
let(:settings) { double(:raise_errors => true) }
- it "does not record the error" do
- make_request
+ describe "does not record the error" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to_not have_error
+ expect(exception_events).to be_empty
+ end
end
end
@@ -162,19 +236,48 @@ def make_request_with_error(error)
)
end
- it "does not record the error" do
- make_request
+ describe "does not record the error" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
- expect(last_transaction).to_not have_error
+ expect(last_transaction).to_not have_error
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(exception_events).to be_empty
+ end
end
end
end
describe "action name" do
- it "sets the action to the request method and path" do
- make_request
+ describe "sets the action to the request method and path" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to have_action("GET /path")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to have_action("GET /path")
+ expect(root_span.name).to eq("GET /path")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("GET /path")
+ end
end
context "without 'sinatra.route' env" do
@@ -182,20 +285,49 @@ def make_request_with_error(error)
Rack::MockRequest.env_for("/path", "REQUEST_METHOD" => "GET")
end
- it "doesn't set an action name" do
- make_request
+ describe "doesn't set an action name" do
+ def perform
+ make_request
+ end
- expect(last_transaction).to_not have_action
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
end
context "with mounted modular application" do
before { env["SCRIPT_NAME"] = "/api" }
- it "sets the action name with an application prefix path" do
- make_request
+ describe "sets the action name with an application prefix path" do
+ def perform
+ make_request
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
- expect(last_transaction).to have_action("GET /api/path")
+ expect(last_transaction).to have_action("GET /api/path")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.name).to eq("GET /api/path")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("GET /api/path")
+ end
end
context "without 'sinatra.route' env" do
@@ -203,10 +335,24 @@ def make_request_with_error(error)
Rack::MockRequest.env_for("/path", "REQUEST_METHOD" => "GET")
end
- it "doesn't set an action name" do
- make_request
+ describe "doesn't set an action name" do
+ def perform
+ make_request
+ end
- expect(last_transaction).to_not have_action
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(last_transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
end
end
diff --git a/spec/lib/appsignal/transaction/extension_backend_spec.rb b/spec/lib/appsignal/transaction/extension_backend_spec.rb
new file mode 100644
index 000000000..9e517dee6
--- /dev/null
+++ b/spec/lib/appsignal/transaction/extension_backend_spec.rb
@@ -0,0 +1,226 @@
+# frozen_string_literal: true
+
+describe Appsignal::Transaction::ExtensionBackend do
+ before { start_agent }
+
+ let(:backend) { described_class.new("abc-123", Appsignal::Transaction::HTTP_REQUEST) }
+
+ describe "#initialize" do
+ it "wraps a real extension transaction when the extension is loaded" do
+ handle = backend.instance_variable_get(:@handle)
+ expect(handle).to be_kind_of(Appsignal::Extension::Transaction)
+ end
+
+ context "when an existing handle is passed in" do
+ it "wraps that handle directly without starting a new transaction" do
+ existing_handle = Appsignal::Extension.start_transaction("other-id", "background_job", 0)
+
+ backend_with_handle = described_class.new(
+ "ignored-id",
+ "ignored-namespace",
+ :handle => existing_handle
+ )
+
+ expect(backend_with_handle.instance_variable_get(:@handle)).to be(existing_handle)
+ end
+ end
+
+ context "when the extension cannot be loaded", :extension_installation_failure do
+ around { |example| Appsignal::Testing.without_testing { example.run } }
+
+ it "falls back to a MockTransaction" do
+ backend = described_class.new("abc-123", Appsignal::Transaction::HTTP_REQUEST)
+ expect(backend.instance_variable_get(:@handle))
+ .to be_kind_of(Appsignal::Extension::MockTransaction)
+ end
+ end
+ end
+
+ describe "#duplicate" do
+ it "returns a new ExtensionBackend wrapping a duplicated extension transaction" do
+ duplicate = backend.duplicate("new-id")
+
+ expect(duplicate).to be_kind_of(described_class)
+ expect(duplicate).not_to be(backend)
+ expect(duplicate.instance_variable_get(:@handle))
+ .not_to be(backend.instance_variable_get(:@handle))
+ end
+ end
+
+ describe "method delegation" do
+ let(:handle) { backend.instance_variable_get(:@handle) }
+
+ it "forwards #start_event to the handle" do
+ expect(handle).to receive(:start_event).with(0)
+ backend.start_event
+ end
+
+ it "forwards #finish_event to the handle" do
+ expect(handle).to receive(:finish_event).with("name", "title", "body", 1, 0)
+ backend.finish_event("name", "title", "body", 1)
+ end
+
+ it "forwards #record_event to the handle" do
+ expect(handle).to receive(:record_event).with("name", "title", "body", 1, 1000, 0)
+ backend.record_event("name", "title", "body", 1, 1000)
+ end
+
+ it "forwards #set_action to the handle" do
+ expect(handle).to receive(:set_action).with("MyAction")
+ backend.set_action("MyAction")
+ end
+
+ it "forwards #set_namespace to the handle" do
+ expect(handle).to receive(:set_namespace).with("background_job")
+ backend.set_namespace("background_job")
+ end
+
+ it "forwards #set_queue_start to the handle" do
+ expect(handle).to receive(:set_queue_start).with(123_456)
+ backend.set_queue_start(123_456)
+ end
+
+ it "forwards #set_metadata to the handle" do
+ expect(handle).to receive(:set_metadata).with("key", "value")
+ backend.set_metadata("key", "value")
+ end
+
+ it "serializes the sample data to Data and forwards #set_sample_data to the handle" do
+ raw = { "a" => 1 }
+ data = Appsignal::Utils::Data.generate(raw)
+ expect(Appsignal::Utils::Data).to receive(:generate).with(raw).and_return(data)
+ expect(handle).to receive(:set_sample_data).with("params", data)
+ backend.set_sample_data("params", raw)
+ end
+
+ it "serializes the backtrace Array to Data and forwards #set_error to the handle" do
+ allow(backend).to receive(:set_sample_data)
+ data = Appsignal::Utils::Data.generate(["line 1"])
+ expect(Appsignal::Utils::Data).to receive(:generate).with(["line 1"]).and_return(data)
+ expect(handle).to receive(:set_error).with("RuntimeError", "boom", data)
+ backend.set_error("RuntimeError", "boom", ["line 1"], [], false)
+ end
+
+ it "forwards an empty Data array when the backtrace is nil" do
+ allow(backend).to receive(:set_sample_data)
+ data = Appsignal::Extension.data_array_new
+ expect(Appsignal::Extension).to receive(:data_array_new).and_return(data)
+ expect(handle).to receive(:set_error).with("RuntimeError", "boom", data)
+ backend.set_error("RuntimeError", "boom", nil, [], false)
+ end
+
+ it "flushes the causes as error_causes sample data" do
+ allow(handle).to receive(:set_error)
+ causes = [{
+ :name => "ArgumentError",
+ :message => "bad arg",
+ :backtrace => ["/app/lib/foo.rb:10:in `bar'"]
+ }]
+ expect(backend).to receive(:set_sample_data).with("error_causes", an_instance_of(Array))
+ backend.set_error("RuntimeError", "boom", ["line 1"], causes, false)
+ end
+
+ it "forwards #finish to the handle and returns its value" do
+ expect(handle).to receive(:finish).with(0).and_return(true)
+ expect(backend.finish).to eq(true)
+ end
+
+ it "forwards #complete to the handle" do
+ expect(handle).to receive(:complete)
+ backend.complete
+ end
+
+ it "drops the transaction on #discard without completing the handle" do
+ expect(handle).to_not receive(:complete)
+ backend.discard
+ end
+
+ it "forwards #to_json to the handle" do
+ expect(handle).to receive(:to_json).and_return("{}")
+ expect(backend.to_json).to eq("{}")
+ end
+
+ it "forwards #queue_start to the handle" do
+ backend.set_queue_start(99)
+ expect(backend.queue_start).to eq(99)
+ end
+
+ it "forwards #_completed? to the handle" do
+ expect(backend._completed?).to eq(false)
+ backend.complete
+ expect(backend._completed?).to eq(true)
+ end
+ end
+
+ describe "breadcrumbs" do
+ let(:handle) { backend.instance_variable_get(:@handle) }
+
+ it "caps the buffer at the breadcrumb limit, keeping the most recent" do
+ 25.times { |i| backend.add_breadcrumb(:index => i) }
+
+ buffer = backend.instance_variable_get(:@breadcrumbs)
+ expect(buffer.length).to eq(Appsignal::Transaction::BREADCRUMB_LIMIT)
+ expect(buffer.first).to eq(:index => 5)
+ expect(buffer.last).to eq(:index => 24)
+ end
+
+ it "flushes the buffered breadcrumbs as sample data on complete" do
+ backend.add_breadcrumb(:action => "click")
+ data = Appsignal::Utils::Data.generate([{ :action => "click" }])
+ expect(Appsignal::Utils::Data).to receive(:generate)
+ .with([{ :action => "click" }]).and_return(data)
+ expect(handle).to receive(:set_sample_data).with("breadcrumbs", data)
+ expect(handle).to receive(:complete)
+
+ backend.complete
+ end
+
+ it "does not flush sample data when there are no breadcrumbs" do
+ expect(handle).to_not receive(:set_sample_data)
+ expect(handle).to receive(:complete)
+
+ backend.complete
+ end
+
+ it "copies the buffer into a duplicate" do
+ backend.add_breadcrumb(:action => "click")
+ duplicate = backend.duplicate("new-id")
+
+ expect(duplicate.instance_variable_get(:@breadcrumbs)).to eq([{ :action => "click" }])
+ end
+ end
+
+ describe "error_causes projection" do
+ it "projects causes to the first-line shape" do
+ causes = [{
+ :name => "ArgumentError",
+ :message => "bad arg",
+ :backtrace => ["/app/lib/foo.rb:10:in `bar'"]
+ }]
+ projected = backend.send(:error_causes_sample_data, causes, false)
+
+ expect(projected.first).to include(:name => "ArgumentError", :message => "bad arg")
+ expect(projected.first[:first_line]["original"]).to eq("/app/lib/foo.rb:10:in `bar'")
+ expect(projected.first[:first_line]["line"]).to eq(10)
+ end
+
+ it "marks the last cause as not the root cause when the chain was truncated" do
+ causes = [{ :name => "E", :message => "m", :backtrace => ["/app/x.rb:1:in `y'"] }]
+
+ expect(backend.send(:error_causes_sample_data, causes, true).last[:is_root_cause])
+ .to eq(false)
+ end
+
+ it "leaves the first line nil for a cause with no backtrace" do
+ causes = [{ :name => "E", :message => "m", :backtrace => nil }]
+
+ expect(backend.send(:error_causes_sample_data, causes, false).first[:first_line]).to be_nil
+ end
+ end
+
+ describe "#records_errors_eagerly?" do
+ it "returns false (extra errors are reported as duplicate transactions)" do
+ expect(backend.records_errors_eagerly?).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/appsignal/transaction/opentelemetry_backend_spec.rb b/spec/lib/appsignal/transaction/opentelemetry_backend_spec.rb
new file mode 100644
index 000000000..081744c87
--- /dev/null
+++ b/spec/lib/appsignal/transaction/opentelemetry_backend_spec.rb
@@ -0,0 +1,1014 @@
+# frozen_string_literal: true
+
+require "opentelemetry/sdk" if DependencyHelper.opentelemetry_present?
+
+describe Appsignal::Transaction::OpenTelemetryBackend,
+ :if => DependencyHelper.opentelemetry_present? do
+ let(:span_exporter) { ::OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new }
+ let(:tracer_provider) do
+ provider = ::OpenTelemetry::SDK::Trace::TracerProvider.new
+ provider.add_span_processor(
+ ::OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(span_exporter)
+ )
+ provider
+ end
+
+ before do
+ ::OpenTelemetry.tracer_provider = tracer_provider
+ @backends_created = []
+
+ # OTel reports context-balance violations (e.g. DetachError) through its
+ # error handler, which by default only logs. Capture them so the after hook
+ # can fail the example on an unexpected one -- an accidental imbalance should
+ # be a red test, not a silent log. An example that deliberately provokes one
+ # sets @expect_otel_errors.
+ @otel_errors = []
+ @original_otel_error_handler = ::OpenTelemetry.error_handler
+ ::OpenTelemetry.error_handler =
+ lambda { |exception: nil, message: nil| @otel_errors << [exception, message] }
+ end
+
+ # Each `create_backend` call constructs a real backend, which attaches an
+ # OTel context on initialize. We track them all here and complete any that
+ # the test didn't complete itself, so leftover spans / context attachments
+ # don't pollute the next test. Complete in reverse (LIFO) order: the contexts
+ # are stacked in creation order, so the last one created must detach first.
+ after do
+ @backends_created.reverse_each { |backend| backend.complete unless backend._completed? }
+ ::OpenTelemetry.error_handler = @original_otel_error_handler
+ expect(@otel_errors).to be_empty unless @expect_otel_errors
+ end
+
+ def create_backend(namespace = "http_request")
+ described_class.new("abc-123", namespace).tap { |b| @backends_created << b }
+ end
+
+ def foreign_tracer
+ ::OpenTelemetry.tracer_provider.tracer("foreign-instrumentation")
+ end
+
+ # Start a foreign span, make it the current OTel context for the block, and
+ # detach it afterwards (LIFO). Models another instrumentation's span sitting on
+ # top of AppSignal's context.
+ def with_foreign_current_span(name = "foreign")
+ foreign = foreign_tracer.start_span(name)
+ token = ::OpenTelemetry::Context.attach(::OpenTelemetry::Trace.context_with_span(foreign))
+ yield foreign
+ ensure
+ ::OpenTelemetry::Context.detach(token)
+ foreign.finish
+ end
+
+ def finished_span(span)
+ span_exporter.finished_spans.find { |s| s.span_id == span.context.span_id }
+ end
+
+ def event_names(finished)
+ Array(finished&.events).map(&:name)
+ end
+
+ describe "#initialize" do
+ it "constructs without raising" do
+ expect { create_backend }.not_to raise_error
+ end
+
+ it "names the span 'appsignal.transaction '" do
+ create_backend("http_request").complete
+ expect(span_exporter.finished_spans.first.name).to eq("appsignal.transaction http_request")
+ end
+
+ {
+ "http_request" => :server,
+ "background_job" => :consumer,
+ "action_cable" => :server,
+ "some_custom_ns" => :server
+ }.each do |namespace, expected_kind|
+ it "maps namespace #{namespace.inspect} to SpanKind #{expected_kind.inspect}" do
+ create_backend(namespace).complete
+ expect(span_exporter.finished_spans.first.kind).to eq(expected_kind)
+ end
+ end
+
+ context "with an incoming opentelemetry_context" do
+ let(:trace_id_hex) { "0af7651916cd43dd8448eb211c80319c" }
+ let(:span_id_hex) { "b7ad6b7169203331" }
+ let(:remote_context) do
+ # Build the remote parent context directly instead of parsing a
+ # `traceparent` through `OpenTelemetry.propagation`. The backend's job
+ # is to parent under a context it is handed; extracting one from a
+ # carrier is the Rack middleware's job, covered by its own specs.
+ # Building it here also keeps this a self-contained unit test: parsing
+ # would depend on the global propagator, which is only configured as a
+ # side effect of booting the SDK in some other example.
+ span_context = ::OpenTelemetry::Trace::SpanContext.new(
+ :trace_id => [trace_id_hex].pack("H*"),
+ :span_id => [span_id_hex].pack("H*"),
+ :trace_flags => ::OpenTelemetry::Trace::TraceFlags.from_byte(0x01),
+ :remote => true
+ )
+ ::OpenTelemetry::Trace.context_with_span(
+ ::OpenTelemetry::Trace.non_recording_span(span_context)
+ )
+ end
+
+ def create_backend_with_context(namespace, context)
+ described_class.new("abc-123", namespace, :opentelemetry_context => context)
+ .tap { |b| @backends_created << b }
+ end
+
+ it "parents a server transaction under the remote span (continues the trace)" do
+ backend = create_backend_with_context("http_request", remote_context)
+ backend.complete
+ root = finished_span(backend.instance_variable_get(:@span))
+
+ expect(root.hex_trace_id).to eq(trace_id_hex)
+ expect(root.parent_span_id.unpack1("H*")).to eq(span_id_hex)
+ expect(root.kind).to eq(:server)
+ end
+
+ it "starts a fresh root trace when no context is given" do
+ backend = create_backend("http_request")
+ backend.complete
+ root = finished_span(backend.instance_variable_get(:@span))
+
+ expect(root.hex_trace_id).not_to eq(trace_id_hex)
+ expect(root.parent_span_id).to eq(::OpenTelemetry::Trace::INVALID_SPAN_ID)
+ end
+
+ it "links a consumer transaction back to the remote span (starts a new trace)" do
+ backend = create_backend_with_context("background_job", remote_context)
+ backend.complete
+ root = finished_span(backend.instance_variable_get(:@span))
+
+ # A job is its own unit of work: new trace, no parent.
+ expect(root.hex_trace_id).not_to eq(trace_id_hex)
+ expect(root.parent_span_id).to eq(::OpenTelemetry::Trace::INVALID_SPAN_ID)
+ expect(root.kind).to eq(:consumer)
+
+ # ... but linked back to the enqueuing span.
+ expect(root.links.size).to eq(1)
+ link_context = root.links.first.span_context
+ expect(link_context.hex_trace_id).to eq(trace_id_hex)
+ expect(link_context.hex_span_id).to eq(span_id_hex)
+ end
+
+ it "does not link a consumer transaction when there is no context" do
+ backend = create_backend("background_job")
+ backend.complete
+ root = finished_span(backend.instance_variable_get(:@span))
+
+ expect(root.kind).to eq(:consumer)
+ expect(root.links).to be_nil
+ end
+ end
+
+ it "attaches the new span as the OpenTelemetry current span" do
+ backend = create_backend
+ expect(::OpenTelemetry::Trace.current_span)
+ .to eq(backend.instance_variable_get(:@span))
+ end
+
+ it "ignores the ambient OpenTelemetry context and starts a new trace" do
+ outer_tracer = ::OpenTelemetry.tracer_provider.tracer("outer")
+ outer = outer_tracer.start_span("outer")
+ outer_token =
+ ::OpenTelemetry::Context.attach(::OpenTelemetry::Trace.context_with_span(outer))
+ begin
+ backend = create_backend
+ backend_span = backend.instance_variable_get(:@span)
+ expect(backend_span.parent_span_id).to eq(::OpenTelemetry::Trace::INVALID_SPAN_ID)
+ expect(backend_span.context.trace_id).not_to eq(outer.context.trace_id)
+ # Complete (detach the root context) before detaching the outer token,
+ # so the detaches happen in LIFO order.
+ backend.complete
+ ensure
+ ::OpenTelemetry::Context.detach(outer_token)
+ outer.finish
+ end
+ end
+
+ it "restores the previously active OpenTelemetry context on #complete" do
+ outer_tracer = ::OpenTelemetry.tracer_provider.tracer("outer")
+ outer = outer_tracer.start_span("outer")
+ outer_token =
+ ::OpenTelemetry::Context.attach(::OpenTelemetry::Trace.context_with_span(outer))
+ begin
+ backend = create_backend
+ expect(::OpenTelemetry::Trace.current_span)
+ .to eq(backend.instance_variable_get(:@span))
+
+ backend.complete
+
+ expect(::OpenTelemetry::Trace.current_span).to eq(outer)
+ ensure
+ ::OpenTelemetry::Context.detach(outer_token)
+ outer.finish
+ end
+ end
+ end
+
+ describe "write method smoke tests" do
+ it "accepts #start_event without raising" do
+ expect { create_backend.start_event }.not_to raise_error
+ end
+
+ it "accepts #finish_event without raising" do
+ expect { create_backend.finish_event("name", "title", "body", 1) }.not_to raise_error
+ end
+
+ it "accepts #record_event without raising" do
+ expect { create_backend.record_event("name", "title", "body", 1, 1000) }.not_to raise_error
+ end
+
+ it "accepts #set_metadata without raising" do
+ expect { create_backend.set_metadata("key", "value") }.not_to raise_error
+ end
+
+ it "accepts #set_sample_data without raising" do
+ expect { create_backend.set_sample_data("params", "anything") }.not_to raise_error
+ end
+ end
+
+ describe "#start_event with opentelemetry_kind" do
+ def event_span_for(category)
+ span_exporter.finished_spans.find { |s| s.attributes["appsignal.category"] == category }
+ end
+
+ it "creates the event span with the given span kind" do
+ backend = create_backend
+ backend.start_event(:opentelemetry_kind => :client)
+ backend.finish_event("request.net_http", "GET", "", Appsignal::EventFormatter::DEFAULT)
+ backend.complete
+
+ expect(event_span_for("request.net_http").kind).to eq(:client)
+ end
+
+ it "defaults to an internal span when no kind is given" do
+ backend = create_backend
+ backend.start_event
+ backend.finish_event("sql.query", "title", "", Appsignal::EventFormatter::DEFAULT)
+ backend.complete
+
+ expect(event_span_for("sql.query").kind).to eq(:internal)
+ end
+ end
+
+ describe "#set_queue_start" do
+ let(:metrics) { Appsignal::Metrics::OpenTelemetryBackend }
+
+ it "adds an appsignal.queue_start event on the root span at the queue time" do
+ allow(metrics).to receive(:add_distribution_value)
+ backend = create_backend
+ backend.set_queue_start(1_700_000_000_000)
+ backend.complete
+
+ event = span_exporter.finished_spans.first.events
+ .find { |e| e.name == "appsignal.queue_start" }
+ expect(event).not_to be_nil
+ expect(event.attributes["appsignal.queue_start"]).to eq(1_700_000_000_000)
+ end
+
+ it "emits the queue duration metric in two series on completion" do
+ backend = create_backend("background_job")
+ start_time = backend.instance_variable_get(:@start_time)
+ queue_start = ((start_time.to_f * 1000) - 5_000).round
+
+ expect(metrics).to receive(:add_distribution_value).with(
+ "transaction_queue_duration", be_within(1_000).of(5_000), :namespace => "background"
+ )
+ expect(metrics).to receive(:add_distribution_value).with(
+ "transaction_queue_duration", be_within(1_000).of(5_000),
+ :namespace => "background", :hostname => an_instance_of(String)
+ )
+
+ backend.set_queue_start(queue_start)
+ backend.complete
+ end
+
+ it "ignores values below the epoch-ms floor" do
+ expect(metrics).to_not receive(:add_distribution_value)
+ backend = create_backend
+ backend.set_queue_start(10)
+ backend.complete
+
+ expect(Array(span_exporter.finished_spans.first.events).map(&:name))
+ .to_not include("appsignal.queue_start")
+ end
+ end
+
+ describe "#set_action" do
+ it "renames the root span to the action" do
+ backend = create_backend
+ backend.set_action("PagesController#show")
+ backend.complete
+
+ expect(span_exporter.finished_spans.first.name).to eq("PagesController#show")
+ end
+
+ it "sets the appsignal.action_name attribute on the root span" do
+ backend = create_backend
+ backend.set_action("PagesController#show")
+ backend.complete
+
+ expect(span_exporter.finished_spans.first.attributes["appsignal.action_name"])
+ .to eq("PagesController#show")
+ end
+ end
+
+ describe "appsignal.namespace attribute" do
+ # The backend converts the internal namespaces to the values the collector
+ # expects; everything else passes through.
+ {
+ "http_request" => "web",
+ "background_job" => "background",
+ "action_cable" => "action_cable",
+ "custom" => "custom"
+ }.each do |namespace, expected|
+ it "maps the constructor namespace #{namespace.inspect} to #{expected.inspect}" do
+ create_backend(namespace).complete
+
+ expect(span_exporter.finished_spans.first.attributes["appsignal.namespace"])
+ .to eq(expected)
+ end
+ end
+
+ describe "#set_namespace" do
+ it "overwrites the appsignal.namespace attribute" do
+ backend = create_backend("http_request")
+ backend.set_namespace("custom")
+ backend.complete
+
+ expect(span_exporter.finished_spans.first.attributes["appsignal.namespace"])
+ .to eq("custom")
+ end
+
+ it "converts the overriding namespace to its canonical value" do
+ backend = create_backend("custom")
+ backend.set_namespace("background_job")
+ backend.complete
+
+ expect(span_exporter.finished_spans.first.attributes["appsignal.namespace"])
+ .to eq("background")
+ end
+
+ it "does not change the span kind (fixed at creation)" do
+ backend = create_backend("http_request")
+ backend.set_namespace("background_job")
+ backend.complete
+
+ expect(span_exporter.finished_spans.first.kind).to eq(:server)
+ end
+ end
+ end
+
+ describe "#set_error" do
+ def exception_event(backend)
+ backend.complete
+ backend_span_id = backend.instance_variable_get(:@span).context.span_id
+ root = span_exporter.finished_spans.find { |s| s.span_id == backend_span_id }
+ root.events.find { |e| e.name == "exception" }
+ end
+
+ it "records an exception span-event on the root span" do
+ backend = create_backend
+ backend.set_error("RuntimeError", "boom", ["line 1", "line 2"], [], false)
+
+ event = exception_event(backend)
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("RuntimeError")
+ expect(event.attributes["exception.message"]).to eq("boom")
+ expect(event.attributes["exception.stacktrace"]).to eq("line 1\nline 2")
+ end
+
+ it "sets the span status to error" do
+ backend = create_backend
+ backend.set_error("RuntimeError", "boom", ["line 1"], [], false)
+ backend.complete
+
+ backend_span_id = backend.instance_variable_get(:@span).context.span_id
+ root = span_exporter.finished_spans.find { |s| s.span_id == backend_span_id }
+ expect(root.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
+
+ it "omits exception.stacktrace content when there is no backtrace" do
+ backend = create_backend
+ backend.set_error("RuntimeError", "boom", nil, [], false)
+
+ expect(exception_event(backend).attributes["exception.stacktrace"]).to eq("")
+ end
+
+ it "emits causes as an appsignal.error_causes JSON attribute matching ErrorSubCause" do
+ backend = create_backend
+ causes = [
+ { :name => "ArgumentError", :message => "bad arg", :backtrace => ["cause 1", "cause 2"] },
+ { :name => "KeyError", :message => "missing", :backtrace => ["cause 3"] }
+ ]
+ backend.set_error("RuntimeError", "boom", ["line 1"], causes, false)
+
+ parsed = JSON.parse(exception_event(backend).attributes["appsignal.error_causes"])
+ expect(parsed).to eq(
+ [
+ { "name" => "ArgumentError", "message" => "bad arg", "lines" => ["cause 1", "cause 2"] },
+ { "name" => "KeyError", "message" => "missing", "lines" => ["cause 3"] }
+ ]
+ )
+ end
+
+ it "defaults a cause's lines to an empty Array when it has no backtrace" do
+ backend = create_backend
+ backend.set_error(
+ "RuntimeError", "boom", ["line 1"],
+ [{ :name => "ArgumentError", :message => "bad arg", :backtrace => nil }],
+ false
+ )
+
+ parsed = JSON.parse(exception_event(backend).attributes["appsignal.error_causes"])
+ expect(parsed).to eq([{ "name" => "ArgumentError", "message" => "bad arg", "lines" => [] }])
+ end
+
+ it "does not set appsignal.error_causes when there are no causes" do
+ backend = create_backend
+ backend.set_error("RuntimeError", "boom", ["line 1"], [], false)
+
+ expect(exception_event(backend).attributes).not_to have_key("appsignal.error_causes")
+ end
+
+ it "flags the error for the collector and lets it compute the digest" do
+ backend = create_backend
+ backend.set_error("RuntimeError", "boom", ["line 1"], [], false)
+
+ attributes = exception_event(backend).attributes
+ # The gem flags the exception so the collector reports it even on a
+ # non-root span; the collector computes the digest itself.
+ expect(attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(attributes).not_to have_key("appsignal.error_digest")
+ end
+
+ it "records the exception on the span that is current when called" do
+ backend = create_backend
+ backend.start_event
+ backend.set_error("RuntimeError", "boom", ["line 1"], [], false)
+ backend.finish_event("sql.query", "title", "body", Appsignal::EventFormatter::DEFAULT)
+ backend.complete
+
+ event_span = span_exporter.finished_spans
+ .find { |s| s.attributes["appsignal.category"] == "sql.query" }
+ backend_span_id = backend.instance_variable_get(:@span).context.span_id
+ root = span_exporter.finished_spans.find { |s| s.span_id == backend_span_id }
+
+ expect(event_span.events.map(&:name)).to include("exception")
+ expect(Array(root.events).map(&:name)).not_to include("exception")
+ end
+
+ it "records one exception event per call (multiple errors on one span)" do
+ backend = create_backend
+ backend.set_error("RuntimeError", "first", ["line 1"], [], false)
+ backend.set_error("ArgumentError", "second", ["line 2"], [], false)
+ backend.complete
+
+ backend_span_id = backend.instance_variable_get(:@span).context.span_id
+ root = span_exporter.finished_spans.find { |s| s.span_id == backend_span_id }
+ events = root.events.select { |e| e.name == "exception" }
+ expect(events.map { |e| e.attributes["exception.type"] })
+ .to eq(["RuntimeError", "ArgumentError"])
+ expect(events.map { |e| e.attributes["exception.message"] }).to eq(["first", "second"])
+ end
+ end
+
+ describe "#records_errors_eagerly?" do
+ it "returns true (multiple exception events on one span)" do
+ expect(create_backend.records_errors_eagerly?).to eq(true)
+ end
+ end
+
+ describe "#finish" do
+ it "returns true so Transaction#complete runs the sample_data path" do
+ expect(create_backend.finish).to eq(true)
+ end
+ end
+
+ describe "#complete" do
+ it "finishes the OTel span" do
+ backend = create_backend
+ span = backend.instance_variable_get(:@span)
+ backend.complete
+
+ # `finished_spans` returns immutable `SpanData` structs, not the
+ # mutable `Span` objects we hold a reference to — compare by span_id.
+ expect(span_exporter.finished_spans.map(&:span_id)).to include(span.context.span_id)
+ end
+
+ it "detaches the OTel context (current_span back to INVALID)" do
+ backend = create_backend
+ expect(::OpenTelemetry::Trace.current_span).not_to eq(::OpenTelemetry::Trace::Span::INVALID)
+
+ backend.complete
+
+ expect(::OpenTelemetry::Trace.current_span).to eq(::OpenTelemetry::Trace::Span::INVALID)
+ end
+
+ it "toggles _completed? from false to true" do
+ backend = create_backend
+ expect(backend._completed?).to eq(false)
+
+ backend.complete
+
+ expect(backend._completed?).to eq(true)
+ end
+ end
+
+ describe "#discard" do
+ it "sets appsignal.ignore_subtrace = true on the root span" do
+ backend = create_backend
+ span = backend.instance_variable_get(:@span)
+ backend.discard
+
+ finished = span_exporter.finished_spans.find { |s| s.span_id == span.context.span_id }
+ expect(finished.attributes["appsignal.ignore_subtrace"]).to be(true)
+ end
+
+ it "finishes the OTel span" do
+ backend = create_backend
+ span = backend.instance_variable_get(:@span)
+ backend.discard
+
+ expect(span_exporter.finished_spans.map(&:span_id)).to include(span.context.span_id)
+ end
+
+ it "detaches the OTel context (current_span back to INVALID)" do
+ backend = create_backend
+ expect(::OpenTelemetry::Trace.current_span).not_to eq(::OpenTelemetry::Trace::Span::INVALID)
+
+ backend.discard
+
+ expect(::OpenTelemetry::Trace.current_span).to eq(::OpenTelemetry::Trace::Span::INVALID)
+ end
+
+ it "toggles _completed? from false to true" do
+ backend = create_backend
+ expect(backend._completed?).to eq(false)
+
+ backend.discard
+
+ expect(backend._completed?).to eq(true)
+ end
+
+ it "is idempotent" do
+ backend = create_backend
+ backend.discard
+
+ expect { backend.discard }.not_to raise_error
+ end
+ end
+
+ describe "#duplicate" do
+ # Collector mode records every error eagerly on one trace, so the Transaction
+ # never duplicates the backend. Duplication is agent-only.
+ it "raises NotImplementedError" do
+ expect { create_backend.duplicate("new-id") }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe "#to_json" do
+ it 'returns "{}" so Transaction#to_h yields an empty Hash' do
+ backend = create_backend
+ expect(backend.to_json).to eq("{}")
+ expect(JSON.parse(backend.to_json)).to eq({})
+ end
+ end
+
+ describe "#queue_start" do
+ it "returns nil (set_queue_start is a no-op for now)" do
+ backend = create_backend
+ backend.set_queue_start(123_456)
+ expect(backend.queue_start).to be_nil
+ end
+ end
+
+ # Smoke test: a Transaction backed by an OpenTelemetryBackend exercises
+ # every public API path without raising, and emits exactly one OTel root
+ # span on completion. The lifecycle behavior (kind, name, context attach)
+ # is covered above; this test mostly guards that no-op methods don't
+ # accidentally start crashing when called from the Transaction.
+ describe "Transaction backed by this backend (collector-mode shape)" do
+ before { start_agent }
+
+ def new_transaction_with_otel_backend(namespace = Appsignal::Transaction::HTTP_REQUEST)
+ backend = described_class.new("abc-123", namespace)
+ @backends_created << backend
+ Appsignal::Transaction.new(namespace, :backend => backend)
+ end
+
+ it "does not raise across the create -> events -> set_action -> complete flow" do
+ expect do
+ transaction = new_transaction_with_otel_backend
+ transaction.set_action("MyController#index")
+ transaction.set_namespace(Appsignal::Transaction::BACKGROUND_JOB)
+ transaction.set_queue_start(1_000_000)
+ transaction.set_metadata("foo", "bar")
+ transaction.start_event
+ transaction.finish_event("sql.query", "title", "SELECT 1", 1)
+ transaction.add_tags(:tag => "value")
+ transaction.add_error(RuntimeError.new("boom"))
+ transaction.complete
+ transaction.to_h
+ end.not_to raise_error
+ end
+
+ it "produces an empty Hash from #to_h (to_json returns {})" do
+ transaction = new_transaction_with_otel_backend
+ transaction.start_event
+ transaction.finish_event("event", "title", "body", 1)
+ transaction.complete
+
+ expect(transaction.to_h).to eq({})
+ end
+
+ it "emits a root span plus a child event span on completion" do
+ transaction = new_transaction_with_otel_backend
+ transaction.start_event
+ transaction.finish_event("event", "title", "body", Appsignal::EventFormatter::DEFAULT)
+ transaction.complete
+
+ expect(span_exporter.finished_spans.size).to eq(2)
+ kinds = span_exporter.finished_spans.map(&:kind)
+ expect(kinds).to include(:server)
+ expect(kinds).to include(:internal)
+ end
+ end
+
+ describe "event stack" do
+ describe "#start_event" do
+ it "opens a child span and attaches it as the current OTel context" do
+ backend = create_backend
+ root_span = backend.instance_variable_get(:@span)
+
+ backend.start_event
+
+ current = ::OpenTelemetry::Trace.current_span
+ expect(current).not_to eq(root_span)
+ expect(current.context.trace_id).to eq(root_span.context.trace_id)
+
+ stack = backend.instance_variable_get(:@event_stack)
+ expect(stack.size).to eq(1)
+ expect(stack.first.first).to eq(current)
+ end
+ end
+
+ describe "#finish_event" do
+ it "pops the stack, names the span after the title, finishes it, and detaches the context" do
+ backend = create_backend
+ root_span = backend.instance_variable_get(:@span)
+
+ backend.start_event
+ backend.finish_event("custom.event", "Title", "Body",
+ Appsignal::EventFormatter::DEFAULT)
+
+ expect(backend.instance_variable_get(:@event_stack)).to be_empty
+ expect(::OpenTelemetry::Trace.current_span).to eq(root_span)
+
+ event_span = span_exporter.finished_spans
+ .find { |s| s.attributes["appsignal.category"] == "custom.event" }
+ expect(event_span).not_to be_nil
+ # The human-readable title becomes the span name; the event name
+ # rides along in appsignal.category.
+ expect(event_span.name).to eq("Title")
+ expect(event_span.attributes["appsignal.category"]).to eq("custom.event")
+ expect(event_span.attributes["appsignal.body"]).to eq("Body")
+ expect(event_span.attributes).not_to have_key("appsignal.title")
+ end
+
+ it "does nothing if the event stack is empty (unpaired finish_event)" do
+ backend = create_backend
+ expect do
+ backend.finish_event("custom.event", "T", "B",
+ Appsignal::EventFormatter::DEFAULT)
+ end.not_to raise_error
+ end
+ end
+
+ describe "#record_event" do
+ it "creates a child span with the event name and a backdated start_timestamp" do
+ backend = create_backend
+ duration_ns = 1_000_000_000 # 1 second
+ backend.record_event("custom.event", "T", "B",
+ Appsignal::EventFormatter::DEFAULT, duration_ns)
+
+ span = span_exporter.finished_spans
+ .find { |s| s.attributes["appsignal.category"] == "custom.event" }
+ expect(span).not_to be_nil
+ expect(span.name).to eq("T")
+ observed = span.end_timestamp - span.start_timestamp
+ # Allow a small slack for clock jitter and the time elapsed
+ # between computing start_time and calling finish.
+ expect(observed).to be_within(50_000_000).of(duration_ns)
+ end
+
+ it "does NOT push onto the event stack" do
+ backend = create_backend
+ backend.record_event("custom.event", nil, nil,
+ Appsignal::EventFormatter::DEFAULT, 1_000)
+ expect(backend.instance_variable_get(:@event_stack)).to be_empty
+ end
+ end
+
+ describe "nested events" do
+ it "produces a properly nested span tree" do
+ backend = create_backend
+ root_span = backend.instance_variable_get(:@span)
+
+ backend.start_event
+ outer_span = backend.instance_variable_get(:@event_stack).last.first
+ backend.start_event
+ inner_span = backend.instance_variable_get(:@event_stack).last.first
+
+ backend.finish_event("inner.event", nil, nil,
+ Appsignal::EventFormatter::DEFAULT)
+ backend.finish_event("outer.event", nil, nil,
+ Appsignal::EventFormatter::DEFAULT)
+
+ inner = span_exporter.finished_spans.find { |s| s.name == "inner.event" }
+ outer = span_exporter.finished_spans.find { |s| s.name == "outer.event" }
+
+ expect(inner.span_id).to eq(inner_span.context.span_id)
+ expect(outer.span_id).to eq(outer_span.context.span_id)
+ expect(inner.parent_span_id).to eq(outer_span.context.span_id)
+ expect(outer.parent_span_id).to eq(root_span.context.span_id)
+ end
+ end
+
+ describe "attribute mapping" do
+ it "writes db.query.text + db.system.name for SQL bodies (not appsignal.body)" do
+ backend = create_backend
+ backend.start_event
+ backend.finish_event("sql.query", "Q", "SELECT 1",
+ Appsignal::EventFormatter::SQL_BODY_FORMAT)
+
+ attrs = span_exporter.finished_spans
+ .find { |s| s.attributes["appsignal.category"] == "sql.query" }.attributes
+ expect(attrs["db.query.text"]).to eq("SELECT 1")
+ expect(attrs["db.system.name"]).to eq("other_sql")
+ expect(attrs).not_to have_key("appsignal.body")
+ end
+
+ it "writes appsignal.body for default bodies (no db.* attributes)" do
+ backend = create_backend
+ backend.start_event
+ backend.finish_event("custom", "T", "Body",
+ Appsignal::EventFormatter::DEFAULT)
+
+ attrs = span_exporter.finished_spans
+ .find { |s| s.attributes["appsignal.category"] == "custom" }.attributes
+ expect(attrs["appsignal.body"]).to eq("Body")
+ expect(attrs).not_to have_key("db.query.text")
+ expect(attrs).not_to have_key("db.system.name")
+ end
+
+ it "omits the body attribute entirely when body is empty or nil" do
+ backend = create_backend
+ backend.start_event
+ backend.finish_event("no.body", "T", nil,
+ Appsignal::EventFormatter::DEFAULT)
+ backend.start_event
+ backend.finish_event("empty.body", "T", "",
+ Appsignal::EventFormatter::DEFAULT)
+
+ no_body = span_exporter.finished_spans
+ .find { |s| s.attributes["appsignal.category"] == "no.body" }
+ empty_body = span_exporter.finished_spans
+ .find { |s| s.attributes["appsignal.category"] == "empty.body" }
+ expect(no_body.attributes).not_to have_key("appsignal.body")
+ expect(no_body.attributes).not_to have_key("db.query.text")
+ expect(empty_body.attributes).not_to have_key("appsignal.body")
+ expect(empty_body.attributes).not_to have_key("db.query.text")
+ end
+
+ it "falls back to the event name as the span name when title is empty or nil" do
+ backend = create_backend
+ backend.start_event
+ backend.finish_event("no.title", nil, "Body",
+ Appsignal::EventFormatter::DEFAULT)
+ backend.start_event
+ backend.finish_event("empty.title", "", "Body",
+ Appsignal::EventFormatter::DEFAULT)
+
+ no_title = span_exporter.finished_spans
+ .find { |s| s.attributes["appsignal.category"] == "no.title" }
+ empty_title = span_exporter.finished_spans
+ .find { |s| s.attributes["appsignal.category"] == "empty.title" }
+ # With no usable title, the span name is the event name itself.
+ expect(no_title.name).to eq("no.title")
+ expect(empty_title.name).to eq("empty.title")
+ expect(no_title.attributes).not_to have_key("appsignal.title")
+ expect(empty_title.attributes).not_to have_key("appsignal.title")
+ end
+ end
+
+ describe "#complete with unfinished event spans" do
+ it "drains the event stack without raising, finishing each span with the placeholder name" do
+ backend = create_backend
+ backend.start_event
+ backend.start_event
+
+ expect { backend.complete }.not_to raise_error
+ expect(backend.instance_variable_get(:@event_stack)).to be_empty
+
+ # Both drained spans keep the placeholder name; root span keeps its own.
+ names = span_exporter.finished_spans.map(&:name)
+ expect(names.count("appsignal.event")).to eq(2)
+ end
+ end
+ end
+
+ describe "#add_breadcrumb" do
+ def breadcrumb(overrides = {})
+ {
+ :time => 1_700_000_000,
+ :category => "network",
+ :action => "GET /",
+ :message => "ok",
+ :metadata => { "code" => "200" }
+ }.merge(overrides)
+ end
+
+ it "emits an appsignal.breadcrumb event with the breadcrumb's fields and time" do
+ backend = create_backend
+ backend.add_breadcrumb(breadcrumb)
+ backend.complete
+
+ event = finished_span(backend.instance_variable_get(:@span)).events
+ .find { |e| e.name == "appsignal.breadcrumb" }
+ expect(event.attributes["category"]).to eq("network")
+ expect(event.attributes["action"]).to eq("GET /")
+ expect(event.attributes["message"]).to eq("ok")
+ expect(JSON.parse(event.attributes["metadata"])).to eq("code" => "200")
+ expect(event.timestamp).to eq((Time.at(1_700_000_000).to_r * 1_000_000_000).to_i)
+ end
+
+ it "lands on the open event span when one is open" do
+ backend = create_backend
+ backend.start_event
+ event_span = backend.instance_variable_get(:@event_stack).last.first
+ backend.add_breadcrumb(breadcrumb)
+ backend.finish_event("custom", "T", "B", Appsignal::EventFormatter::DEFAULT)
+ backend.complete
+
+ expect(event_names(finished_span(event_span))).to include("appsignal.breadcrumb")
+ end
+
+ it "caps at BREADCRUMB_LIMIT, keeping the first ones added" do
+ backend = create_backend
+ limit = Appsignal::Transaction::BREADCRUMB_LIMIT
+ (limit + 5).times { |i| backend.add_breadcrumb(breadcrumb(:action => "act-#{i}")) }
+ backend.complete
+
+ crumbs = finished_span(backend.instance_variable_get(:@span)).events
+ .select { |e| e.name == "appsignal.breadcrumb" }
+ expect(crumbs.size).to eq(limit)
+ expect(crumbs.first.attributes["action"]).to eq("act-0")
+ expect(crumbs.last.attributes["action"]).to eq("act-#{limit - 1}")
+ end
+ end
+
+ # AppSignal writes to the OpenTelemetry SDK but does not read its global
+ # current span to decide where its own data goes: errors and breadcrumbs land
+ # on AppSignal's own span (the open event span, or the root), never on a
+ # foreign span that happens to be current. Parenting is the one thing that
+ # does follow the global context, so foreign and AppSignal spans nest under
+ # each other.
+ describe "interop with foreign OpenTelemetry spans" do
+ describe "AppSignal data lands on AppSignal's own spans" do
+ it "records an error on the root span, not a foreign current span" do
+ backend = create_backend
+ foreign = nil
+ with_foreign_current_span do |f|
+ foreign = f
+ backend.set_error("RuntimeError", "boom", ["line 1"], [], false)
+ end
+ backend.complete
+
+ expect(event_names(finished_span(backend.instance_variable_get(:@span))))
+ .to include("exception")
+ expect(event_names(finished_span(foreign))).not_to include("exception")
+ end
+
+ it "records an error on the open event span, not a foreign current span" do
+ backend = create_backend
+ backend.start_event
+ event_span = backend.instance_variable_get(:@event_stack).last.first
+ foreign = nil
+ with_foreign_current_span do |f|
+ foreign = f
+ backend.set_error("RuntimeError", "boom", ["line 1"], [], false)
+ end
+ backend.finish_event("sql.query", "title", "body", Appsignal::EventFormatter::DEFAULT)
+ backend.complete
+
+ expect(event_names(finished_span(event_span))).to include("exception")
+ expect(event_names(finished_span(foreign))).not_to include("exception")
+ expect(event_names(finished_span(backend.instance_variable_get(:@span))))
+ .not_to include("exception")
+ end
+
+ it "records a breadcrumb on the root span, not a foreign current span" do
+ backend = create_backend
+ foreign = nil
+ with_foreign_current_span do |f|
+ foreign = f
+ backend.add_breadcrumb(
+ :time => 1_700_000_000, :category => "c", :action => "a",
+ :message => "m", :metadata => {}
+ )
+ end
+ backend.complete
+
+ expect(event_names(finished_span(backend.instance_variable_get(:@span))))
+ .to include("appsignal.breadcrumb")
+ expect(event_names(finished_span(foreign))).not_to include("appsignal.breadcrumb")
+ end
+ end
+
+ describe "tree shape (parenting follows the global context)" do
+ it "parents a foreign span under the open AppSignal event span" do
+ backend = create_backend
+ backend.start_event
+ event_span = backend.instance_variable_get(:@event_stack).last.first
+
+ foreign = foreign_tracer.start_span("foreign")
+ foreign.finish
+
+ backend.finish_event("e", "t", "b", Appsignal::EventFormatter::DEFAULT)
+ backend.complete
+
+ expect(finished_span(foreign).parent_span_id).to eq(event_span.context.span_id)
+ end
+
+ it "parents a foreign span under the root span when no event is open" do
+ backend = create_backend
+ root = backend.instance_variable_get(:@span)
+
+ foreign = foreign_tracer.start_span("foreign")
+ foreign.finish
+ backend.complete
+
+ expect(finished_span(foreign).parent_span_id).to eq(root.context.span_id)
+ end
+
+ it "parents an AppSignal event span under a foreign current span" do
+ backend = create_backend
+ event_span = nil
+ foreign_id = nil
+ with_foreign_current_span do |foreign|
+ foreign_id = foreign.context.span_id
+ backend.start_event
+ event_span = backend.instance_variable_get(:@event_stack).last.first
+ backend.finish_event("e", "t", "b", Appsignal::EventFormatter::DEFAULT)
+ end
+ backend.complete
+
+ expect(finished_span(event_span).parent_span_id).to eq(foreign_id)
+ end
+ end
+
+ describe "context lifecycle" do
+ it "unwinds cleanly when a foreign span attaches and detaches inside an event" do
+ backend = create_backend
+ root = backend.instance_variable_get(:@span)
+ backend.start_event
+
+ with_foreign_current_span { nil }
+
+ backend.finish_event("e", "t", "b", Appsignal::EventFormatter::DEFAULT)
+ expect(::OpenTelemetry::Trace.current_span).to eq(root)
+ expect(backend.instance_variable_get(:@event_stack)).to be_empty
+
+ backend.complete
+ expect(::OpenTelemetry::Trace.current_span).to eq(::OpenTelemetry::Trace::Span::INVALID)
+ # The after hook asserts no OTel context error was recorded.
+ end
+
+ it "does not defend against a co-resident context leak (characterization)" do
+ # If another instrumentation attaches a context and never detaches it,
+ # AppSignal's own detach pops that leaked frame instead of its own and
+ # OTel signals a DetachError. AppSignal does not try to recover. This
+ # records the current behaviour; it is not a guarantee.
+ @expect_otel_errors = true
+
+ backend = create_backend
+ backend.start_event
+ leaked = foreign_tracer.start_span("leaky")
+ ::OpenTelemetry::Context.attach(::OpenTelemetry::Trace.context_with_span(leaked))
+
+ backend.finish_event("e", "t", "b", Appsignal::EventFormatter::DEFAULT)
+
+ expect(@otel_errors.map(&:first))
+ .to include(an_instance_of(::OpenTelemetry::Context::DetachError))
+
+ backend.complete
+ leaked.finish
+ # Clear the deliberately leaked frame so it can't pollute later examples.
+ ::OpenTelemetry::Context.clear
+ end
+ end
+ end
+end
diff --git a/spec/lib/appsignal/transaction_spec.rb b/spec/lib/appsignal/transaction_spec.rb
index d3ab930dd..31dc2efbc 100644
--- a/spec/lib/appsignal/transaction_spec.rb
+++ b/spec/lib/appsignal/transaction_spec.rb
@@ -3,10 +3,21 @@
let(:time) { Time.at(fixed_time) }
let(:root_path) { nil }
- before do
- start_agent(:options => options, :root_path => root_path)
+ before do |example|
+ # Only auto-start the agent for non-mode examples. Mode-tagged examples
+ # (`:agent_mode`/`:collector_mode`) start the agent themselves in their body
+ # (agent mode via `start_agent(**start_agent_args)`, collector mode via
+ # `start_collector_agent`) -- the dual-mode start principle -- so starting it
+ # here too would clobber the collector setup / leave the test in agent mode.
+ unless example.metadata[:agent_mode] || example.metadata[:collector_mode]
+ start_agent(:options => options, :root_path => root_path)
+ end
Timecop.freeze(time)
end
+
+ # Mode-tagged examples start the agent in their body; expose the same
+ # `:options`/`:root_path` the automatic start above would have used.
+ let(:start_agent_args) { { :options => options, :root_path => root_path } }
after { Timecop.return }
around do |example|
keep_transactions do
@@ -36,11 +47,11 @@
end
end
- context "when an explicit extension transaction is passed in the initialiser" do
- let(:ext) { "some_ext" }
+ context "when an explicit backend is passed in the initialiser" do
+ let(:backend) { "some_backend" }
- it "assigns the extension transaction to the transaction" do
- expect(described_class.new("web", :ext => ext).ext).to be(ext)
+ it "assigns the backend to the transaction" do
+ expect(described_class.new("web", :backend => backend).backend).to be(backend)
end
end
@@ -99,6 +110,100 @@
expect(current_transaction.transaction_id).to eq("transaction_id_2")
end
end
+
+ describe "transaction state after create" do
+ it_in_both_modes do
+ transaction = create_transaction
+ expect(transaction.namespace).to eq(Appsignal::Transaction::HTTP_REQUEST)
+ expect(transaction.transaction_id).to be_a(String)
+ expect(transaction.transaction_id).not_to be_empty
+ end
+ end
+
+ describe "OpenTelemetry root span" do
+ it "starts a root span with SpanKind::SERVER for HTTP_REQUEST", :collector_mode do
+ start_collector_agent
+ create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ Appsignal::Transaction.complete_current!
+
+ expect(span_exporter.finished_spans.size).to eq(1)
+ span = span_exporter.finished_spans.first
+ expect(span.kind).to eq(:server)
+ expect(span.name).to eq("appsignal.transaction http_request")
+ end
+
+ it "uses SpanKind::CONSUMER for BACKGROUND_JOB", :collector_mode do
+ start_collector_agent
+ create_transaction(Appsignal::Transaction::BACKGROUND_JOB)
+ Appsignal::Transaction.complete_current!
+
+ expect(span_exporter.finished_spans.first.kind).to eq(:consumer)
+ end
+
+ it "uses SpanKind::SERVER for ACTION_CABLE", :collector_mode do
+ start_collector_agent
+ create_transaction(Appsignal::Transaction::ACTION_CABLE)
+ Appsignal::Transaction.complete_current!
+
+ expect(span_exporter.finished_spans.first.kind).to eq(:server)
+ end
+
+ it "uses SpanKind::SERVER for an unknown custom namespace", :collector_mode do
+ start_collector_agent
+ create_transaction("my_custom_namespace")
+ Appsignal::Transaction.complete_current!
+
+ span = span_exporter.finished_spans.first
+ expect(span.kind).to eq(:server)
+ expect(span.name).to eq("appsignal.transaction my_custom_namespace")
+ end
+ end
+
+ describe "OpenTelemetry current context" do
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(::OpenTelemetry::Trace.current_span).to eq(::OpenTelemetry::Trace::Span::INVALID)
+
+ create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+
+ expect(::OpenTelemetry::Trace.current_span).not_to eq(::OpenTelemetry::Trace::Span::INVALID)
+ expect(::OpenTelemetry::Trace.current_span.context.trace_id).not_to be_nil
+
+ Appsignal::Transaction.complete_current!
+
+ expect(::OpenTelemetry::Trace.current_span).to eq(::OpenTelemetry::Trace::Span::INVALID)
+ end
+ end
+
+ describe "OpenTelemetry interop with a foreign current span" do
+ it "keeps errors and breadcrumbs on AppSignal's span", :collector_mode do
+ start_collector_agent
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+
+ # A foreign instrumentation's span becomes current inside an AppSignal
+ # event. AppSignal's error and breadcrumb should still land on its own
+ # event span, not on the foreign span.
+ transaction.start_event
+ foreign = ::OpenTelemetry.tracer_provider.tracer("foreign").start_span("foreign-call")
+ foreign_token =
+ ::OpenTelemetry::Context.attach(::OpenTelemetry::Trace.context_with_span(foreign))
+
+ transaction.add_error(ExampleStandardError.new("boom"))
+ transaction.add_breadcrumb("network", "GET /", "ok", { "code" => "200" })
+
+ ::OpenTelemetry::Context.detach(foreign_token)
+ foreign.finish
+ transaction.finish_event("sql.query", "Query", "SELECT 1",
+ Appsignal::EventFormatter::DEFAULT)
+ Appsignal::Transaction.complete_current!
+
+ event_span = event_spans.find { |s| s.attributes["appsignal.category"] == "sql.query" }
+ foreign_span = span_exporter.finished_spans.find { |s| s.name == "foreign-call" }
+
+ expect(event_span.events.map(&:name)).to include("exception", "appsignal.breadcrumb")
+ expect(Array(foreign_span.events).map(&:name)).to be_empty
+ end
+ end
end
describe ".current" do
@@ -183,6 +288,16 @@
end.to_not(change { Thread.current[:appsignal_transaction] })
end
end
+
+ describe "current transaction after complete_current!" do
+ it_in_both_modes do
+ create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ Appsignal::Transaction.complete_current!
+
+ expect(Appsignal::Transaction.current).to be_a(Appsignal::Transaction::NilTransaction)
+ expect(Appsignal::Transaction.current?).to be(false)
+ end
+ end
end
describe "#complete" do
@@ -204,14 +319,10 @@
end
context "when a transaction is marked as discarded" do
- it "does not complete the transaction" do
+ it "marks the transaction as discarded" do
expect do
transaction.discard!
end.to change { transaction.discarded? }.from(false).to(true)
-
- transaction.complete
-
- expect(transaction).to_not be_completed
end
it "logs a debug message" do
@@ -223,17 +334,67 @@
"Skipping transaction 'mock_transaction_id' because it was manually discarded."
end
+ describe "completing a discarded transaction" do
+ def perform
+ transaction.discard!
+ transaction.complete
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ # Nothing is reported: the transaction is dropped, not completed.
+ expect(transaction).to_not be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ # The root span is still finished and exported, but flagged so the
+ # collector ignores the whole subtrace.
+ expect(root_span.attributes["appsignal.ignore_subtrace"]).to be(true)
+ # The discarded transaction's context is detached -- it does not leak
+ # as the thread's current OTel span.
+ expect(::OpenTelemetry::Trace.current_span)
+ .to eq(::OpenTelemetry::Trace::Span::INVALID)
+ end
+ end
+
context "when a discarded transaction is restored" do
- before { transaction.discard! }
+ it "unmarks the transaction as discarded" do
+ transaction.discard!
- it "completes the transaction" do
expect do
transaction.restore!
end.to change { transaction.discarded? }.from(true).to(false)
+ end
- transaction.complete
+ describe "completing a restored transaction" do
+ def perform
+ transaction.discard!
+ transaction.restore!
+ transaction.complete
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(transaction).to be_completed
+ # The transaction is reported as normal: the root span is exported
+ # without the ignore flag, so the collector keeps the subtrace.
+ expect(transaction).to be_completed
+ expect(root_span).not_to be_nil
+ expect(root_span.attributes).not_to have_key("appsignal.ignore_subtrace")
+ end
end
end
end
@@ -364,7 +525,7 @@
it "the duplicate transaction has a different extension transaction than the original" do
original_transaction, duplicate_transaction = created_transactions
- expect(original_transaction.ext).to_not eq(duplicate_transaction.ext)
+ expect(original_transaction.backend).to_not eq(duplicate_transaction.backend)
end
it "marks transaction as duplicate on the duplicate transaction" do
@@ -537,6 +698,26 @@
)
end
end
+
+ describe "completed? after #complete" do
+ it_in_both_modes do
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ transaction.complete
+
+ expect(transaction.completed?).to be(true)
+ end
+ end
+
+ describe "OpenTelemetry span emission" do
+ it "emits no span until complete is called", :collector_mode do
+ start_collector_agent
+ create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ expect(span_exporter.finished_spans).to be_empty
+
+ Appsignal::Transaction.complete_current!
+ expect(span_exporter.finished_spans.size).to eq(1)
+ end
+ end
end
context "pausing" do
@@ -599,7 +780,7 @@
let(:transaction) { new_transaction }
it "loads the AppSignal extension" do
- expect(transaction.ext).to_not be_nil
+ expect(transaction.backend).to_not be_nil
end
context "when extension is not loaded", :extension_installation_failure do
@@ -608,7 +789,7 @@
end
it "does not error on missing extension method calls" do
- expect(transaction.ext).to be_kind_of(Appsignal::Extension::MockTransaction)
+ expect(transaction.backend).to be_kind_of(Appsignal::Transaction::ExtensionBackend)
transaction.start_event
transaction.finish_event(
"name",
@@ -723,520 +904,1247 @@
expect(transaction.method(:add_params)).to eq(transaction.method(:set_params))
end
- it "adds the params to the transaction" do
- params = { "key" => "value" }
- transaction.add_params(params)
-
- transaction._sample
- expect(transaction).to include_params(params)
- end
+ describe "adding the params to the transaction" do
+ def perform
+ transaction.add_params("key" => "value")
+ end
- it "merges the params on the transaction" do
- transaction.add_params("abc" => "value")
- transaction.add_params("def" => "value")
- transaction.add_params { { "xyz" => "value" } }
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- transaction._sample
- expect(transaction).to include_params(
- "abc" => "value",
- "def" => "value",
- "xyz" => "value"
- )
- end
+ expect(transaction).to include_params("key" => "value")
+ end
- it "adds the params to the transaction with a block" do
- params = { "key" => "value" }
- transaction.add_params { params }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- transaction._sample
- expect(transaction).to include_params(params)
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("key" => "value")
+ end
end
- it "adds the params block value when both an argument and block are given" do
- arg_params = { "argument" => "value" }
- block_params = { "block" => "value" }
- transaction.add_params(arg_params) { block_params }
-
- transaction._sample
- expect(transaction).to include_params(block_params)
- end
+ describe "merging the params on the transaction" do
+ def perform
+ transaction.add_params("abc" => "value")
+ transaction.add_params("def" => "value")
+ transaction.add_params { { "xyz" => "value" } }
+ end
- it "logs an error if an error occurred storing the params" do
- transaction.add_params { raise "uh oh" }
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- logs = capture_logs { transaction._sample }
- expect(logs).to contains_log(
- :error,
- "Exception while fetching params: RuntimeError: uh oh"
- )
- end
+ expect(transaction).to include_params(
+ "abc" => "value",
+ "def" => "value",
+ "xyz" => "value"
+ )
+ end
- it "does not update the params on the transaction if the given value is nil" do
- params = { "key" => "value" }
- transaction.add_params(params)
- transaction.add_params(nil)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- transaction._sample
- expect(transaction).to include_params(params)
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"])).to eq(
+ "abc" => "value",
+ "def" => "value",
+ "xyz" => "value"
+ )
+ end
end
- context "with AppSignal filtering" do
- let(:options) { { :filter_parameters => %w[foo] } }
-
- it "returns sanitized custom params" do
- transaction.add_params("foo" => "value", "baz" => "bat")
+ describe "adding the params to the transaction with a block" do
+ def perform
+ transaction.add_params { { "key" => "value" } }
+ end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
- expect(transaction).to include_params("foo" => "[FILTERED]", "baz" => "bat")
+
+ expect(transaction).to include_params("key" => "value")
end
- end
- end
- describe "#add_params_if_nil" do
- let(:transaction) { new_transaction }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- it "has a #set_params_if_nil alias" do
- expect(transaction.method(:add_params_if_nil)).to eq(transaction.method(:set_params_if_nil))
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("key" => "value")
+ end
end
- context "when the params are not set" do
- it "adds the params to the transaction" do
- params = { "key" => "value" }
- transaction.add_params_if_nil(params)
+ describe "adding the params block value when both an argument and block are given" do
+ def perform
+ transaction.add_params("argument" => "value") { { "block" => "value" } }
+ end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
- expect(transaction).to include_params(params)
+
+ expect(transaction).to include_params("block" => "value")
end
- it "adds the params to the transaction with a block" do
- params = { "key" => "value" }
- transaction.add_params_if_nil { params }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- transaction._sample
- expect(transaction).to include_params(params)
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("block" => "value")
+ end
+ end
+
+ describe "when an error occurs storing the params" do
+ def perform
+ transaction.add_params { raise "uh oh" }
end
- it "adds the params block value when both an argument and block are given" do
- arg_params = { "argument" => "value" }
- block_params = { "block" => "value" }
- transaction.add_params_if_nil(arg_params) { block_params }
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
- transaction._sample
- expect(transaction).to include_params(block_params)
+ logs = capture_logs { transaction._sample }
+ expect(logs).to contains_log(
+ :error,
+ "Exception while fetching params: RuntimeError: uh oh"
+ )
end
- it "does not update the params on the transaction if the given value is nil" do
- params = { "key" => "value" }
- transaction.add_params(params)
- transaction.add_params_if_nil(nil)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- transaction._sample
- expect(transaction).to include_params(params)
+ logs = capture_logs { transaction.complete }
+ expect(logs).to contains_log(
+ :error,
+ "Exception while fetching params: RuntimeError: uh oh"
+ )
+ expect(root_span.attributes).to_not have_key("appsignal.request.payload")
end
end
- context "when the params are set" do
- it "does not update the params on the transaction" do
- preset_params = { "other" => "params" }
- params = { "key" => "value" }
- transaction.add_params(preset_params)
- transaction.add_params_if_nil(params)
+ describe "when the given params value is nil" do
+ def perform
+ transaction.add_params("key" => "value")
+ transaction.add_params(nil)
+ end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
- expect(transaction).to include_params(preset_params)
+
+ expect(transaction).to include_params("key" => "value")
end
- it "does not update the params with a block on the transaction" do
- preset_params = { "other" => "params" }
- params = { "key" => "value" }
- transaction.add_params(preset_params)
- transaction.add_params_if_nil { params }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- transaction._sample
- expect(transaction).to include_params(preset_params)
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("key" => "value")
end
end
- context "when the params were set as an empty value" do
- it "does not set params on the transaction" do
- transaction.add_params("key1" => "value")
- transaction.set_empty_params!
- transaction.add_params_if_nil("key2" => "value")
+ context "with AppSignal filtering" do
+ let(:options) { { :filter_parameters => %w[foo] } }
- transaction._sample
- expect(transaction).to_not include_params
+ describe "sanitizing the params" do
+ def perform
+ transaction.add_params("foo" => "value", "baz" => "bat")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_params("foo" => "[FILTERED]", "baz" => "bat")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("foo" => "[FILTERED]", "baz" => "bat")
+ end
end
end
end
- describe "#add_session_data" do
+ describe "#add_params_if_nil" do
let(:transaction) { new_transaction }
- it "has a #set_session_data alias" do
- expect(transaction.method(:add_session_data)).to eq(transaction.method(:set_session_data))
+ it "has a #set_params_if_nil alias" do
+ expect(transaction.method(:add_params_if_nil)).to eq(transaction.method(:set_params_if_nil))
end
- it "adds the session data to the transaction" do
- data = { "key" => "value" }
- transaction.add_session_data(data)
+ context "when the params are not set" do
+ describe "adding the params to the transaction" do
+ def perform
+ transaction.add_params_if_nil("key" => "value")
+ end
- transaction._sample
- expect(transaction).to include_session_data(data)
- end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- it "merges the session data on the transaction" do
- transaction.add_session_data("abc" => "value")
- transaction.add_session_data("def" => "value")
- transaction.add_session_data { { "xyz" => "value" } }
+ expect(transaction).to include_params("key" => "value")
+ end
- transaction._sample
- expect(transaction).to include_session_data(
- "abc" => "value",
- "def" => "value",
- "xyz" => "value"
- )
- end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- it "adds the session data to the transaction with a block" do
- data = { "key" => "value" }
- transaction.add_session_data { data }
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("key" => "value")
+ end
+ end
- transaction._sample
- expect(transaction).to include_session_data(data)
- end
+ describe "adding the params to the transaction with a block" do
+ def perform
+ transaction.add_params_if_nil { { "key" => "value" } }
+ end
- it "adds the session data block value when both an argument and block are given" do
- arg_data = { "argument" => "value" }
- block_data = { "block" => "value" }
- transaction.add_session_data(arg_data) { block_data }
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- transaction._sample
- expect(transaction).to include_session_data(block_data)
- end
+ expect(transaction).to include_params("key" => "value")
+ end
- it "adds certain Ruby objects as Strings" do
- transaction.add_session_data("time" => Time.utc(2024, 9, 12, 13, 14, 15))
- transaction.add_session_data("date" => Date.new(2024, 9, 11))
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- transaction._sample
- expect(transaction).to include_session_data(
- "time" => "#",
- "date" => "#"
- )
- end
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("key" => "value")
+ end
+ end
- it "logs an error if an error occurred storing the session data" do
- transaction.add_session_data { raise "uh oh" }
+ describe "adding the params block value when both an argument and block are given" do
+ def perform
+ transaction.add_params_if_nil("argument" => "value") { { "block" => "value" } }
+ end
- logs = capture_logs { transaction._sample }
- expect(logs).to contains_log(
- :error,
- "Exception while fetching session data: RuntimeError: uh oh"
- )
- end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- it "does not update the session data on the transaction if the given value is nil" do
- data = { "key" => "value" }
- transaction.add_session_data(data)
- transaction.add_session_data(nil)
+ expect(transaction).to include_params("block" => "value")
+ end
- transaction._sample
- expect(transaction).to include_session_data(data)
- end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- context "with filter_session_data" do
- let(:options) { { :filter_session_data => ["filtered_key"] } }
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("block" => "value")
+ end
+ end
+
+ describe "when the given value is nil" do
+ def perform
+ transaction.add_params("key" => "value")
+ transaction.add_params_if_nil(nil)
+ end
- it "does not include filtered out session data" do
- transaction.add_session_data("data" => "value1", "filtered_key" => "filtered_value")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- transaction._sample
- expect(transaction).to include_session_data("data" => "value1")
+ expect(transaction).to include_params("key" => "value")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("key" => "value")
+ end
end
end
- end
- describe "#add_session_data_if_nil" do
- let(:transaction) { new_transaction }
+ context "when the params are set" do
+ describe "not updating the params on the transaction" do
+ def perform
+ transaction.add_params("other" => "params")
+ transaction.add_params_if_nil("key" => "value")
+ end
- context "when the session data is not set" do
- it "sets the session data on the transaction" do
- data = { "key" => "value" }
- transaction.add_session_data_if_nil(data)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- transaction._sample
- expect(transaction).to include_session_data(data)
- end
+ expect(transaction).to include_params("other" => "params")
+ end
- it "updates the session data on the transaction with a block" do
- data = { "key" => "value" }
- transaction.add_session_data_if_nil { data }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- transaction._sample
- expect(transaction).to include_session_data(data)
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("other" => "params")
+ end
end
- it "updates with the session data block when both an argument and block are given" do
- arg_data = { "argument" => "value" }
- block_data = { "block" => "value" }
- transaction.add_session_data_if_nil(arg_data) { block_data }
+ describe "not updating the params with a block on the transaction" do
+ def perform
+ transaction.add_params("other" => "params")
+ transaction.add_params_if_nil { { "key" => "value" } }
+ end
- transaction._sample
- expect(transaction).to include_session_data(block_data)
- end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- it "does not update the session data on the transaction if the given value is nil" do
- data = { "key" => "value" }
- transaction.add_session_data(data)
- transaction.add_session_data_if_nil(nil)
+ expect(transaction).to include_params("other" => "params")
+ end
- transaction._sample
- expect(transaction).to include_session_data(data)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("other" => "params")
+ end
end
end
- context "when the session data are set" do
- it "does not update the session data on the transaction" do
- preset_data = { "other" => "data" }
- data = { "key" => "value" }
- transaction.add_session_data(preset_data)
- transaction.add_session_data_if_nil(data)
+ context "when the params were set as an empty value" do
+ describe "not setting params on the transaction" do
+ def perform
+ transaction.add_params("key1" => "value")
+ transaction.set_empty_params!
+ transaction.add_params_if_nil("key2" => "value")
+ end
- transaction._sample
- expect(transaction).to include_session_data(preset_data)
- end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- it "does not update the session data with a block on the transaction" do
- preset_data = { "other" => "data" }
- data = { "key" => "value" }
- transaction.add_session_data(preset_data)
- transaction.add_session_data_if_nil { data }
+ expect(transaction).to_not include_params
+ end
- transaction._sample
- expect(transaction).to include_session_data(preset_data)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes).to_not have_key("appsignal.request.payload")
+ end
end
end
end
- describe "#add_headers" do
+ describe "#add_session_data" do
let(:transaction) { new_transaction }
- it "has a #set_headers alias" do
- expect(transaction.method(:add_headers)).to eq(transaction.method(:set_headers))
+ it "has a #set_session_data alias" do
+ expect(transaction.method(:add_session_data)).to eq(transaction.method(:set_session_data))
end
- it "adds the headers to the transaction" do
- headers = { "PATH_INFO" => "value" }
- transaction.add_headers(headers)
-
- transaction._sample
- expect(transaction).to include_environment(headers)
- end
+ describe "adding the session data to the transaction" do
+ def perform
+ transaction.add_session_data("key" => "value")
+ end
- it "merges the headers on the transaction" do
- transaction.add_headers("PATH_INFO" => "value")
- transaction.add_headers("REQUEST_METHOD" => "value")
- transaction.add_headers { { "HTTP_ACCEPT" => "value" } }
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- transaction._sample
- expect(transaction).to include_environment(
- "PATH_INFO" => "value",
- "REQUEST_METHOD" => "value",
- "HTTP_ACCEPT" => "value"
- )
- end
+ expect(transaction).to include_session_data("key" => "value")
+ end
- it "adds the headers to the transaction with a block" do
- headers = { "PATH_INFO" => "value" }
- transaction.add_headers { headers }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- transaction._sample
- expect(transaction).to include_environment(headers)
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("key" => "value")
+ end
end
- it "adds the headers block value when both an argument and block are given" do
- arg_data = { "PATH_INFO" => "/arg-path" }
- block_data = { "PATH_INFO" => "/block-path" }
- transaction.add_headers(arg_data) { block_data }
-
- transaction._sample
- expect(transaction).to include_environment(block_data)
- end
+ describe "merging the session data on the transaction" do
+ def perform
+ transaction.add_session_data("abc" => "value")
+ transaction.add_session_data("def" => "value")
+ transaction.add_session_data { { "xyz" => "value" } }
+ end
- it "logs an error if an error occurred storing the headers" do
- transaction.add_headers { raise "uh oh" }
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- logs = capture_logs { transaction._sample }
- expect(logs).to contains_log(
- :error,
- "Exception while fetching headers: RuntimeError: uh oh"
- )
- end
+ expect(transaction).to include_session_data(
+ "abc" => "value",
+ "def" => "value",
+ "xyz" => "value"
+ )
+ end
- it "does not update the headers on the transaction if the given value is nil" do
- headers = { "PATH_INFO" => "value" }
- transaction.add_headers(headers)
- transaction.add_headers(nil)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- transaction._sample
- expect(transaction).to include_environment(headers)
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"])).to eq(
+ "abc" => "value",
+ "def" => "value",
+ "xyz" => "value"
+ )
+ end
end
- context "with request_headers options" do
- let(:options) { { :request_headers => ["MY_HEADER"] } }
+ describe "adding the session data to the transaction with a block" do
+ def perform
+ transaction.add_session_data { { "key" => "value" } }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data("key" => "value")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("key" => "value")
+ end
+ end
+
+ describe "adding the session data block when an argument and block are given" do
+ def perform
+ transaction.add_session_data("argument" => "value") { { "block" => "value" } }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data("block" => "value")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("block" => "value")
+ end
+ end
+
+ describe "adding certain Ruby objects as Strings" do
+ def perform
+ transaction.add_session_data("time" => Time.utc(2024, 9, 12, 13, 14, 15))
+ transaction.add_session_data("date" => Date.new(2024, 9, 11))
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data(
+ "time" => "#",
+ "date" => "#"
+ )
+ end
- it "does not include filtered out headers" do
- transaction.add_headers("MY_HEADER" => "value1", "filtered_key" => "filtered_value")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"])).to eq(
+ "time" => "#",
+ "date" => "#"
+ )
+ end
+ end
+
+ describe "when an error occurs storing the session data" do
+ def perform
+ transaction.add_session_data { raise "uh oh" }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ logs = capture_logs { transaction._sample }
+ expect(logs).to contains_log(
+ :error,
+ "Exception while fetching session data: RuntimeError: uh oh"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ logs = capture_logs { transaction.complete }
+ expect(logs).to contains_log(
+ :error,
+ "Exception while fetching session data: RuntimeError: uh oh"
+ )
+ expect(root_span.attributes).to_not have_key("appsignal.request.session_data")
+ end
+ end
+
+ describe "when the given session data value is nil" do
+ def perform
+ transaction.add_session_data("key" => "value")
+ transaction.add_session_data(nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
- expect(transaction).to include_environment("MY_HEADER" => "value1")
+
+ expect(transaction).to include_session_data("key" => "value")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("key" => "value")
+ end
+ end
+
+ context "with filter_session_data" do
+ let(:options) { { :filter_session_data => ["filtered_key"] } }
+
+ describe "filtering out session data" do
+ def perform
+ transaction.add_session_data("data" => "value1", "filtered_key" => "filtered_value")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data("data" => "value1")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ # Filtering redacts the value (mode-independent, applied before the
+ # backend) rather than dropping the key.
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("data" => "value1", "filtered_key" => "[FILTERED]")
+ end
end
end
end
- describe "#add_headers_if_nil" do
+ describe "#add_session_data_if_nil" do
let(:transaction) { new_transaction }
- it "has a #set_headers_if_nil alias" do
- expect(transaction.method(:add_headers_if_nil)).to eq(transaction.method(:set_headers_if_nil))
+ context "when the session data is not set" do
+ describe "setting the session data on the transaction" do
+ def perform
+ transaction.add_session_data_if_nil("key" => "value")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data("key" => "value")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("key" => "value")
+ end
+ end
+
+ describe "updating the session data on the transaction with a block" do
+ def perform
+ transaction.add_session_data_if_nil { { "key" => "value" } }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data("key" => "value")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("key" => "value")
+ end
+ end
+
+ describe "updating with the session data block when an argument and block are given" do
+ def perform
+ transaction.add_session_data_if_nil("argument" => "value") { { "block" => "value" } }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data("block" => "value")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("block" => "value")
+ end
+ end
+
+ describe "when the given value is nil" do
+ def perform
+ transaction.add_session_data("key" => "value")
+ transaction.add_session_data_if_nil(nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data("key" => "value")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("key" => "value")
+ end
+ end
end
- context "when the headers are not set" do
- it "adds the headers to the transaction" do
- headers = { "PATH_INFO" => "value" }
- transaction.add_headers_if_nil(headers)
+ context "when the session data are set" do
+ describe "not updating the session data on the transaction" do
+ def perform
+ transaction.add_session_data("other" => "data")
+ transaction.add_session_data_if_nil("key" => "value")
+ end
- transaction._sample
- expect(transaction).to include_environment(headers)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data("other" => "data")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("other" => "data")
+ end
end
- it "adds the headers to the transaction with a block" do
- headers = { "PATH_INFO" => "value" }
- transaction.add_headers_if_nil { headers }
+ describe "not updating the session data with a block on the transaction" do
+ def perform
+ transaction.add_session_data("other" => "data")
+ transaction.add_session_data_if_nil { { "key" => "value" } }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_session_data("other" => "data")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("other" => "data")
+ end
+ end
+ end
+ end
+
+ describe "#add_headers" do
+ let(:transaction) { new_transaction }
+
+ it "has a #set_headers alias" do
+ expect(transaction.method(:add_headers)).to eq(transaction.method(:set_headers))
+ end
+
+ describe "adding the headers to the transaction" do
+ def perform
+ # A true header (kept, normalized in collector mode) and a CGI var
+ # (kept in agent mode, dropped in collector mode).
+ transaction.add_headers("HTTP_ACCEPT" => "text/html", "PATH_INFO" => "/path")
+ end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
- expect(transaction).to include_environment(headers)
+
+ expect(transaction).to include_environment(
+ "HTTP_ACCEPT" => "text/html",
+ "PATH_INFO" => "/path"
+ )
end
- it "adds the headers block value when both an argument and block are given" do
- arg_data = { "PATH_INFO" => "/arg-path" }
- block_data = { "PATH_INFO" => "/block-path" }
- transaction.add_headers_if_nil(arg_data) { block_data }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ # True headers normalized to the OTel convention; non-header CGI vars
+ # dropped.
+ expect(root_span.attributes["http.request.header.accept"]).to eq("text/html")
+ expect(root_span.attributes).to_not have_key("http.request.header.path-info")
+ end
+ end
+
+ describe "merging the headers on the transaction" do
+ def perform
+ transaction.add_headers("HTTP_ACCEPT" => "text/html")
+ transaction.add_headers("HTTP_RANGE" => "bytes=0-")
+ transaction.add_headers { { "HTTP_CACHE_CONTROL" => "no-cache" } }
+ end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
- expect(transaction).to include_environment(block_data)
+
+ expect(transaction).to include_environment(
+ "HTTP_ACCEPT" => "text/html",
+ "HTTP_RANGE" => "bytes=0-",
+ "HTTP_CACHE_CONTROL" => "no-cache"
+ )
end
- it "does not update the headers on the transaction if the given value is nil" do
- headers = { "PATH_INFO" => "value" }
- transaction.add_headers(headers)
- transaction.add_headers_if_nil(nil)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("text/html")
+ expect(root_span.attributes["http.request.header.range"]).to eq("bytes=0-")
+ expect(root_span.attributes["http.request.header.cache-control"]).to eq("no-cache")
+ end
+ end
+
+ describe "adding the headers to the transaction with a block" do
+ def perform
+ transaction.add_headers { { "HTTP_ACCEPT" => "text/html" } }
+ end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
- expect(transaction).to include_environment(headers)
+
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "text/html")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("text/html")
end
end
- context "when the headers are set" do
- it "does not update the headers on the transaction" do
- preset_headers = { "PATH_INFO" => "/first-path" }
- headers = { "PATH_INFO" => "/other-path" }
- transaction.add_headers(preset_headers)
- transaction.add_headers_if_nil(headers)
+ describe "adding the headers block value when both an argument and block are given" do
+ def perform
+ transaction.add_headers("HTTP_ACCEPT" => "arg") { { "HTTP_ACCEPT" => "block" } }
+ end
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
- expect(transaction).to include_environment(preset_headers)
+
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "block")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("block")
+ end
+ end
+
+ describe "when an error occurs storing the headers" do
+ def perform
+ transaction.add_headers { raise "uh oh" }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ logs = capture_logs { transaction._sample }
+ expect(logs).to contains_log(
+ :error,
+ "Exception while fetching headers: RuntimeError: uh oh"
+ )
end
- it "does not update the headers with a block on the transaction" do
- preset_headers = { "PATH_INFO" => "/first-path" }
- headers = { "PATH_INFO" => "/other-path" }
- transaction.add_headers(preset_headers)
- transaction.add_headers_if_nil { headers }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ logs = capture_logs { transaction.complete }
+ expect(logs).to contains_log(
+ :error,
+ "Exception while fetching headers: RuntimeError: uh oh"
+ )
+ end
+ end
+
+ describe "when the given headers value is nil" do
+ def perform
+ transaction.add_headers("HTTP_ACCEPT" => "text/html")
+ transaction.add_headers(nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
- expect(transaction).to include_environment(preset_headers)
+
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "text/html")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("text/html")
+ end
+ end
+
+ context "with request_headers options" do
+ let(:options) { { :request_headers => ["HTTP_ACCEPT"] } }
+
+ describe "filtering out headers not in the allowlist" do
+ def perform
+ transaction.add_headers("HTTP_ACCEPT" => "text/html", "HTTP_RANGE" => "bytes=0-")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "text/html")
+ expect(transaction).to_not include_environment("HTTP_RANGE" => "bytes=0-")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("text/html")
+ expect(root_span.attributes).to_not have_key("http.request.header.range")
+ end
end
end
end
- describe "#add_tags" do
+ describe "#add_headers_if_nil" do
let(:transaction) { new_transaction }
- let(:long_string) { "a" * 10_001 }
- it "stores tags on the transaction" do
- transaction.add_tags(
- :valid_key => "valid_value",
- "valid_string_key" => "valid_value",
- :both_symbols => :valid_value,
- :integer_value => 1,
- :hash_value => { "invalid" => "hash" },
- :array_value => %w[invalid array],
- :object => Object.new,
- :too_long_value => long_string,
- long_string => "too_long_key",
- :true_tag => true,
- :false_tag => false
- )
- transaction._sample
-
- expect(transaction).to include_tags(
- "valid_key" => "valid_value",
- "valid_string_key" => "valid_value",
- "both_symbols" => "valid_value",
- "integer_value" => 1,
- "too_long_value" => "#{"a" * 10_000}...",
- long_string => "too_long_key",
- "true_tag" => true,
- "false_tag" => false
- )
+ it "has a #set_headers_if_nil alias" do
+ expect(transaction.method(:add_headers_if_nil)).to eq(transaction.method(:set_headers_if_nil))
end
- it "merges the tags when called multiple times" do
- transaction.add_tags(:key1 => "value1")
- transaction.add_tags(:key2 => "value2")
- transaction._sample
+ context "when the headers are not set" do
+ describe "adding the headers to the transaction" do
+ def perform
+ transaction.add_headers_if_nil("HTTP_ACCEPT" => "text/html")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
- expect(transaction).to include_tags(
- "key1" => "value1",
- "key2" => "value2"
- )
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "text/html")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("text/html")
+ end
+ end
+
+ describe "adding the headers to the transaction with a block" do
+ def perform
+ transaction.add_headers_if_nil { { "HTTP_ACCEPT" => "text/html" } }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "text/html")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("text/html")
+ end
+ end
+
+ describe "adding the headers block value when an argument and block are given" do
+ def perform
+ transaction.add_headers_if_nil("HTTP_ACCEPT" => "arg") { { "HTTP_ACCEPT" => "block" } }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "block")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("block")
+ end
+ end
+
+ describe "when the given value is nil" do
+ def perform
+ transaction.add_headers("HTTP_ACCEPT" => "text/html")
+ transaction.add_headers_if_nil(nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "text/html")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("text/html")
+ end
+ end
end
- context "with config default_tags" do
- let(:options) do
- { :default_tags => { "config_tag" => "config_value", "another_tag" => 123 } }
+ context "when the headers are set" do
+ describe "not updating the headers on the transaction" do
+ def perform
+ transaction.add_headers("HTTP_ACCEPT" => "first")
+ transaction.add_headers_if_nil("HTTP_ACCEPT" => "other")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "first")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("first")
+ end
end
- it "includes default_tags from config" do
- transaction._sample
+ describe "not updating the headers with a block on the transaction" do
+ def perform
+ transaction.add_headers("HTTP_ACCEPT" => "first")
+ transaction.add_headers_if_nil { { "HTTP_ACCEPT" => "other" } }
+ end
- expect(transaction).to include_tags(
- "config_tag" => "config_value",
- "another_tag" => 123
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "first")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["http.request.header.accept"]).to eq("first")
+ end
+ end
+ end
+ end
+
+ describe "#add_tags" do
+ let(:transaction) { new_transaction }
+ let(:long_string) { "a" * 10_001 }
+
+ describe "storing tags on the transaction" do
+ def perform
+ transaction.add_tags(
+ :valid_key => "valid_value",
+ "valid_string_key" => "valid_value",
+ :both_symbols => :valid_value,
+ :integer_value => 1,
+ :hash_value => { "invalid" => "hash" },
+ :array_value => %w[invalid array],
+ :object => Object.new,
+ :too_long_value => long_string,
+ long_string => "too_long_key",
+ :true_tag => true,
+ :false_tag => false
)
end
- it "transaction tags override default_tags" do
- transaction.add_tags("config_tag" => "transaction_value")
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
+ # The extension truncates over-long tag values to 10,000 chars + "...".
expect(transaction).to include_tags(
- "config_tag" => "transaction_value",
- "another_tag" => 123
+ "valid_key" => "valid_value",
+ "valid_string_key" => "valid_value",
+ "both_symbols" => "valid_value",
+ "integer_value" => 1,
+ "too_long_value" => "#{"a" * 10_000}...",
+ long_string => "too_long_key",
+ "true_tag" => true,
+ "false_tag" => false
)
end
- it "merges default_tags with transaction tags" do
- transaction.add_tags("transaction_tag" => "transaction_value")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ attributes = root_span.attributes
+ # Each tag is its own `appsignal.tag.` attribute. Symbols are
+ # coerced to strings; over-long values are sent whole (not truncated
+ # like the extension does) -- the collector/server apply their limits.
+ expect(attributes["appsignal.tag.valid_key"]).to eq("valid_value")
+ expect(attributes["appsignal.tag.valid_string_key"]).to eq("valid_value")
+ expect(attributes["appsignal.tag.both_symbols"]).to eq("valid_value")
+ expect(attributes["appsignal.tag.integer_value"]).to eq(1)
+ expect(attributes["appsignal.tag.true_tag"]).to eq(true)
+ expect(attributes["appsignal.tag.false_tag"]).to eq(false)
+ expect(attributes["appsignal.tag.too_long_value"]).to eq(long_string)
+ expect(attributes["appsignal.tag.#{long_string}"]).to eq("too_long_key")
+ # Non-primitive tag values are dropped by `sanitized_tags` in both modes.
+ expect(attributes).to_not have_key("appsignal.tag.hash_value")
+ expect(attributes).to_not have_key("appsignal.tag.array_value")
+ expect(attributes).to_not have_key("appsignal.tag.object")
+ end
+ end
+
+ describe "merging the tags when called multiple times" do
+ def perform
+ transaction.add_tags(:key1 => "value1")
+ transaction.add_tags(:key2 => "value2")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
transaction._sample
expect(transaction).to include_tags(
- "config_tag" => "config_value",
- "another_tag" => 123,
- "transaction_tag" => "transaction_value"
+ "key1" => "value1",
+ "key2" => "value2"
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.key1"]).to eq("value1")
+ expect(root_span.attributes["appsignal.tag.key2"]).to eq("value2")
+ end
+ end
+
+ context "with config default_tags" do
+ let(:options) do
+ { :default_tags => { "config_tag" => "config_value", "another_tag" => 123 } }
+ end
+
+ describe "including default_tags from config" do
+ def perform
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_tags(
+ "config_tag" => "config_value",
+ "another_tag" => 123
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.config_tag"]).to eq("config_value")
+ expect(root_span.attributes["appsignal.tag.another_tag"]).to eq(123)
+ end
+ end
+
+ describe "transaction tags override default_tags" do
+ def perform
+ transaction.add_tags("config_tag" => "transaction_value")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_tags(
+ "config_tag" => "transaction_value",
+ "another_tag" => 123
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.config_tag"]).to eq("transaction_value")
+ expect(root_span.attributes["appsignal.tag.another_tag"]).to eq(123)
+ end
+ end
+
+ describe "merging default_tags with transaction tags" do
+ def perform
+ transaction.add_tags("transaction_tag" => "transaction_value")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_tags(
+ "config_tag" => "config_value",
+ "another_tag" => 123,
+ "transaction_tag" => "transaction_value"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.config_tag"]).to eq("config_value")
+ expect(root_span.attributes["appsignal.tag.another_tag"]).to eq(123)
+ expect(root_span.attributes["appsignal.tag.transaction_tag"]).to eq("transaction_value")
+ end
+ end
end
end
@@ -1247,91 +2155,166 @@
expect(transaction.method(:add_custom_data)).to eq(transaction.method(:set_custom_data))
end
- it "adds a custom Hash data to the transaction" do
- transaction.add_custom_data(
- :user => {
- :id => 123,
- :locale => "abc"
- },
- :organization => {
- :slug => "appsignal",
- :plan => "enterprise"
- }
- )
+ describe "adding a custom Hash data to the transaction" do
+ def perform
+ transaction.add_custom_data(
+ :user => {
+ :id => 123,
+ :locale => "abc"
+ },
+ :organization => {
+ :slug => "appsignal",
+ :plan => "enterprise"
+ }
+ )
+ end
- transaction._sample
- expect(transaction).to include_custom_data(
- "user" => {
- "id" => 123,
- "locale" => "abc"
- },
- "organization" => {
- "slug" => "appsignal",
- "plan" => "enterprise"
+ let(:expected) do
+ {
+ "user" => {
+ "id" => 123,
+ "locale" => "abc"
+ },
+ "organization" => {
+ "slug" => "appsignal",
+ "plan" => "enterprise"
+ }
}
- )
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_custom_data(expected)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.custom_data"])).to eq(expected)
+ end
end
- it "adds a custom Array data to the transaction" do
- transaction.add_custom_data([
- [123, "abc"],
- ["appsignal", "enterprise"]
- ])
+ describe "adding a custom Array data to the transaction" do
+ def perform
+ transaction.add_custom_data([
+ [123, "abc"],
+ ["appsignal", "enterprise"]
+ ])
+ end
- transaction._sample
- expect(transaction).to include_custom_data([
- [123, "abc"],
- ["appsignal", "enterprise"]
- ])
+ let(:expected) { [[123, "abc"], ["appsignal", "enterprise"]] }
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_custom_data(expected)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.custom_data"])).to eq(expected)
+ end
end
- it "does not store non Hash or Array custom data" do
- logs =
- capture_logs do
- transaction.add_custom_data("abc")
- transaction._sample
- expect(transaction).to_not include_custom_data
+ describe "storing non Hash or Array custom data" do
+ def perform
+ transaction.add_custom_data("abc")
+ transaction.add_custom_data(123)
+ transaction.add_custom_data(Object.new)
+ end
- transaction.add_custom_data(123)
- transaction._sample
- expect(transaction).to_not include_custom_data
+ def expect_unsupported_type_logs(logs)
+ expect(logs).to contains_log(
+ :error,
+ %(Sample data 'custom_data': Unsupported data type 'String' received: "abc")
+ )
+ expect(logs).to contains_log(
+ :error,
+ %(Sample data 'custom_data': Unsupported data type 'Integer' received: 123)
+ )
+ expect(logs).to contains_log(
+ :error,
+ %(Sample data 'custom_data': Unsupported data type 'Object' received: # "value")
- transaction.add_custom_data("def" => "value")
+ describe "merging the custom data if called multiple times" do
+ def perform
+ transaction.add_custom_data("abc" => "value")
+ transaction.add_custom_data("def" => "value")
+ end
- transaction._sample
- expect(transaction).to include_custom_data(
- "abc" => "value",
- "def" => "value"
- )
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_custom_data(
+ "abc" => "value",
+ "def" => "value"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.custom_data"])).to eq(
+ "abc" => "value",
+ "def" => "value"
+ )
+ end
end
end
describe "#add_breadcrumb" do
let(:transaction) { new_transaction }
+ # The OpenTelemetry `appsignal.breadcrumb` events recorded across all
+ # finished spans (breadcrumbs attach to the span that was current when they
+ # were added, which may be the root span or an event span).
+ def breadcrumb_events
+ span_exporter.finished_spans.flat_map { |span| Array(span.events) }.select do |event|
+ event.name == "appsignal.breadcrumb"
+ end
+ end
+
context "when over the limit" do
- before do
+ def perform
22.times do |i|
transaction.add_breadcrumb(
"network",
@@ -1341,10 +2324,13 @@
Time.parse("10-10-2010 10:00:00 UTC")
)
end
- transaction._sample
end
- it "stores last breadcrumbs on the transaction" do
+ it "stores last breadcrumbs on the transaction in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction.complete
+
expect(transaction.to_h["sample_data"]["breadcrumbs"].length).to eql(20)
expect(transaction.to_h["sample_data"]["breadcrumbs"][0]).to eq(
"action" => "GET http://localhost",
@@ -1361,13 +2347,58 @@
"time" => 1_286_704_800
)
end
+
+ # Collector mode caps at the first rather than the last: streamed
+ # span events can't be retracted once emitted, so the agent-mode last-N
+ # trim can't be reproduced.
+ it "emits the first breadcrumb events in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ events = breadcrumb_events
+ expect(events.length).to eq(20)
+ expect(events.first.attributes).to include(
+ "category" => "network",
+ "action" => "GET http://localhost",
+ "message" => "User made external network request"
+ )
+ expect(JSON.parse(events.first.attributes["metadata"])).to eq("code" => 1)
+ expect(JSON.parse(events.last.attributes["metadata"])).to eq("code" => 20)
+ end
+ end
+
+ context "when added inside an instrumented event" do
+ def perform
+ transaction.start_event
+ transaction.add_breadcrumb("network", "GET http://localhost")
+ transaction.finish_event("sql.active_record", "User Load", "body",
+ Appsignal::EventFormatter::DEFAULT)
+ end
+
+ it "emits the breadcrumb on the event span, not the root span", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event_span = event_spans.find do |span|
+ span.attributes["appsignal.category"] == "sql.active_record"
+ end
+ expect(event_span.events.map(&:name)).to include("appsignal.breadcrumb")
+ expect(Array(root_span.events).map(&:name)).to_not include("appsignal.breadcrumb")
+ end
end
context "with defaults" do
- it "stores breadcrumb with defaults on transaction" do
- timeframe_start = Time.now.utc.to_i
+ def perform
transaction.add_breadcrumb("user_action", "clicked HOME")
- transaction._sample
+ end
+
+ it "stores breadcrumb with defaults on transaction in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ timeframe_start = Time.now.utc.to_i
+ perform
+ transaction.complete
timeframe_end = Time.now.utc.to_i
expect(transaction).to include_breadcrumb(
@@ -1378,15 +2409,32 @@
be_between(timeframe_start, timeframe_end)
)
end
+
+ it "emits a breadcrumb event with defaults on the span in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ events = breadcrumb_events
+ expect(events.length).to eq(1)
+ expect(events.first.attributes).to include(
+ "category" => "user_action",
+ "action" => "clicked HOME",
+ "message" => "",
+ "metadata" => "{}"
+ )
+ end
end
context "with metadata argument that's not a Hash" do
- it "does not add the breadcrumb and logs and error" do
- logs =
- capture_logs do
- transaction.add_breadcrumb("category", "action", "message", "invalid metadata")
- end
- transaction._sample
+ def perform
+ transaction.add_breadcrumb("category", "action", "message", "invalid metadata")
+ end
+
+ it "does not add the breadcrumb and logs an error in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ logs = capture_logs { perform }
+ transaction.complete
expect(transaction).to_not include_breadcrumbs
expect(logs).to contains_log(
@@ -1394,30 +2442,71 @@
"add_breadcrumb: Cannot add breadcrumb. The given metadata argument is not a Hash."
)
end
+
+ it "does not emit a breadcrumb event and logs an error in collector mode", :collector_mode do
+ start_collector_agent
+ logs = capture_logs { perform }
+ transaction.complete
+
+ expect(breadcrumb_events).to be_empty
+ expect(logs).to contains_log(
+ :error,
+ "add_breadcrumb: Cannot add breadcrumb. The given metadata argument is not a Hash."
+ )
+ end
end
end
describe "#set_action" do
let(:transaction) { new_transaction }
+ let(:action_name) { "PagesController#show" }
context "when the action is set" do
- it "updates the action name on the transaction" do
- action_name = "PagesController#show"
+ def perform
transaction.set_action(action_name)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
expect(transaction.action).to eq(action_name)
expect(transaction).to have_action(action_name)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(transaction.action).to eq(action_name)
+ transaction.complete
+ expect(root_span.name).to eq(action_name)
+ expect(root_span.attributes["appsignal.action_name"]).to eq(action_name)
+ end
end
context "when the action is nil" do
- it "does not update the action name on the transaction" do
- action_name = "PagesController#show"
+ def perform
transaction.set_action(action_name)
transaction.set_action(nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction.action).to eq(action_name)
+ expect(transaction).to have_action(action_name)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
expect(transaction.action).to eq(action_name)
- expect(transaction).to have_action(action_name)
+ transaction.complete
+ expect(root_span.name).to eq(action_name)
+ expect(root_span.attributes["appsignal.action_name"]).to eq(action_name)
end
end
end
@@ -1426,262 +2515,580 @@
let(:transaction) { new_transaction }
context "when the action is not set" do
- it "updates the action name on the transaction" do
+ let(:action_name) { "PagesController#show" }
+
+ def perform
+ transaction.set_action_if_nil(action_name)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
expect(transaction.action).to eq(nil)
expect(transaction).to_not have_action
- action_name = "PagesController#show"
- transaction.set_action_if_nil(action_name)
+ perform
expect(transaction.action).to eq(action_name)
expect(transaction).to have_action(action_name)
end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect(transaction.action).to eq(nil)
+
+ perform
+
+ expect(transaction.action).to eq(action_name)
+ transaction.complete
+ expect(root_span.name).to eq(action_name)
+ expect(root_span.attributes["appsignal.action_name"]).to eq(action_name)
+ end
+
context "when the given action is nil" do
- it "does not update the action name on the transaction" do
- action_name = "something"
- transaction.set_action("something")
+ let(:action_name) { "something" }
+
+ def perform
+ transaction.set_action(action_name)
transaction.set_action_if_nil(nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
expect(transaction.action).to eq(action_name)
expect(transaction).to have_action(action_name)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(transaction.action).to eq(action_name)
+ transaction.complete
+ expect(root_span.attributes["appsignal.action_name"]).to eq(action_name)
+ end
end
end
context "when the action is set" do
- it "does not update the action name on the transaction" do
- action_name = "something"
- transaction.set_action("something")
+ let(:action_name) { "something" }
+
+ def perform
+ transaction.set_action(action_name)
transaction.set_action_if_nil("something else")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
expect(transaction.action).to eq(action_name)
expect(transaction).to have_action(action_name)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(transaction.action).to eq(action_name)
+ transaction.complete
+ expect(root_span.name).to eq(action_name)
+ expect(root_span.attributes["appsignal.action_name"]).to eq(action_name)
+ end
end
end
describe "#set_namespace" do
let(:transaction) { new_transaction }
+ let(:namespace) { "custom" }
context "when the namespace is not nil" do
- it "updates the namespace on the transaction" do
- namespace = "custom"
+ def perform
transaction.set_namespace(namespace)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
expect(transaction.namespace).to eq namespace
expect(transaction).to have_namespace(namespace)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(transaction.namespace).to eq namespace
+ transaction.complete
+ expect(root_span.attributes["appsignal.namespace"]).to eq(namespace)
+ end
end
context "when the namespace is nil" do
- it "does not update the namespace on the transaction" do
- namespace = "custom"
+ def perform
transaction.set_namespace(namespace)
transaction.set_namespace(nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
expect(transaction.namespace).to eq(namespace)
expect(transaction).to have_namespace(namespace)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(transaction.namespace).to eq(namespace)
+ transaction.complete
+ expect(root_span.attributes["appsignal.namespace"]).to eq(namespace)
+ end
+ end
+
+ context "when set_namespace is never called", :collector_mode do
+ it "carries the namespace from creation, converted to its canonical value" do
+ start_collector_agent
+ transaction = http_request_transaction
+ transaction.complete
+
+ expect(root_span.attributes["appsignal.namespace"]).to eq("web")
+ end
end
end
describe "#set_queue_start" do
let(:transaction) { new_transaction }
- it "sets the queue start in extension" do
- transaction.set_queue_start(10)
+ describe "setting the queue start" do
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ transaction.set_queue_start(1_000_000_000_000)
+
+ expect(transaction).to have_queue_start(1_000_000_000_000)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ # An epoch-ms timestamp shortly before the transaction started, so the
+ # duration comes out to a known positive delta.
+ start_time = transaction.backend.instance_variable_get(:@start_time)
+ queue_start = ((start_time.to_f * 1000) - 5_000).round
+ transaction.set_queue_start(queue_start)
+ transaction.complete
- expect(transaction).to have_queue_start(10)
+ event = root_span.events.find { |e| e.name == "appsignal.queue_start" }
+ expect(event).not_to be_nil
+ expect(event.attributes["appsignal.queue_start"]).to eq(queue_start)
+
+ # The "http_request" namespace is emitted as "web".
+ snapshot = metric_snapshot("transaction_queue_duration")
+ expect(snapshot.data_points.map(&:attributes)).to contain_exactly(
+ { "namespace" => "web" },
+ { "namespace" => "web", "hostname" => an_instance_of(String) }
+ )
+ expect(snapshot.data_points.map(&:sum)).to all(be_within(1_000).of(5_000))
+ end
end
- it "does not set the queue start in extension when value is nil" do
- transaction.set_queue_start(nil)
+ describe "when the value is nil" do
+ def perform
+ transaction.set_queue_start(nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to_not have_queue_start
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- expect(transaction).to_not have_queue_start
+ expect(Array(root_span.events).map(&:name)).to_not include("appsignal.queue_start")
+ expect(metric_snapshot("transaction_queue_duration")).to be_nil
+ end
end
- it "does not raise an error when the queue start is too big" do
- expect(transaction.ext).to receive(:set_queue_start).and_raise(RangeError)
+ it_in_both_modes "does not raise an error when the queue start is too big" do
+ expect(transaction.backend).to receive(:set_queue_start).and_raise(RangeError)
expect(Appsignal.internal_logger).to receive(:warn).with("Queue start value 10 is too big")
transaction.set_queue_start(10)
+ # Complete so the collector-mode example detaches its OTel context rather
+ # than leaking it into later examples.
+ transaction.complete
end
end
describe "#set_metadata" do
let(:transaction) { new_transaction }
- it "updates the metadata on the transaction" do
- transaction.set_metadata("request_method", "GET")
+ describe "updating the metadata on the transaction" do
+ def perform
+ transaction.set_metadata("request_method", "GET")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to include_metadata("request_method" => "GET")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- expect(transaction).to include_metadata("request_method" => "GET")
+ # Metadata has no dedicated OTel attribute; it is emitted as a tag.
+ expect(root_span.attributes["appsignal.tag.request_method"]).to eq("GET")
+ end
end
context "when filter_metadata includes metadata key" do
let(:options) { { :filter_metadata => ["filter_key"] } }
- it "does not set the metadata on the transaction" do
- transaction.set_metadata(:filter_key, "filtered value")
- transaction.set_metadata("filter_key", "filtered value")
+ describe "not setting the filtered metadata" do
+ def perform
+ transaction.set_metadata(:filter_key, "filtered value")
+ transaction.set_metadata("filter_key", "filtered value")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to_not include_metadata("filter_key" => anything)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- expect(transaction).to_not include_metadata("filter_key" => anything)
+ expect(root_span.attributes).to_not have_key("appsignal.tag.filter_key")
+ end
end
end
context "when the key is nil" do
- it "does not update the metadata on the transaction" do
- transaction.set_metadata(nil, "GET")
+ describe "not updating the metadata" do
+ def perform
+ transaction.set_metadata(nil, "GET")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to_not include_metadata
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- expect(transaction).to_not include_metadata
+ expect(root_span.attributes.keys.grep(/appsignal\.tag\./)).to be_empty
+ end
end
end
context "when the value is nil" do
- it "does not update the metadata on the transaction" do
- transaction.set_metadata("request_method", nil)
+ describe "not updating the metadata" do
+ def perform
+ transaction.set_metadata("request_method", nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to_not include_metadata
+ end
- expect(transaction).to_not include_metadata
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes.keys.grep(/appsignal\.tag\./)).to be_empty
+ end
end
end
end
+ describe "when metadata and a tag share a key (collector mode)" do
+ # In collector mode both metadata and tags are emitted as `appsignal.tag.*`
+ # span attributes, so a shared key collides on one attribute. `set_metadata`
+ # writes the attribute immediately, while tags are flushed at `complete` (via
+ # the sample data), so the tag is written last and wins -- regardless of the
+ # order the two were set in. (In agent mode they are stored separately and
+ # never collide, so this is collector-specific.)
+ it "the tag value wins", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ transaction.add_tags("shared" => "from_tag")
+ transaction.set_metadata("shared", "from_metadata")
+ transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.shared"]).to eq("from_tag")
+ end
+
+ it "the tag value wins even when the tag is added after the metadata", :collector_mode do
+ start_collector_agent
+ transaction = http_request_transaction
+ transaction.set_metadata("shared", "from_metadata")
+ transaction.add_tags("shared" => "from_tag")
+ transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.shared"]).to eq("from_tag")
+ end
+ end
+
describe "storing sample data" do
let(:transaction) { new_transaction }
- it "stores sample data on the transaction" do
- transaction.set_params(
- "string_param" => "string_value",
- :symbol_param => "symbol_value",
- "integer" => 123,
- "float" => 123.45,
- "array" => ["abc", 456, { "option" => true }],
- "hash" => { "hash_key" => "hash_value" }
- )
+ describe "storing sample data on the transaction" do
+ def perform
+ transaction.set_params(
+ "string_param" => "string_value",
+ :symbol_param => "symbol_value",
+ "integer" => 123,
+ "float" => 123.45,
+ "array" => ["abc", 456, { "option" => true }],
+ "hash" => { "hash_key" => "hash_value" }
+ )
+ end
- transaction._sample
- expect(transaction).to include_params(
- "string_param" => "string_value",
- "symbol_param" => "symbol_value",
- "integer" => 123,
- "float" => 123.45,
- "array" => ["abc", 456, { "option" => true }],
- "hash" => { "hash_key" => "hash_value" }
- )
+ let(:expected) do
+ {
+ "string_param" => "string_value",
+ "symbol_param" => "symbol_value",
+ "integer" => 123,
+ "float" => 123.45,
+ "array" => ["abc", 456, { "option" => true }],
+ "hash" => { "hash_key" => "hash_value" }
+ }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ transaction._sample
+
+ expect(transaction).to include_params(expected)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"])).to eq(expected)
+ end
end
- it "does not store non-Array and non-Hash data" do
- logs =
- capture_logs do
- transaction.set_params("some string")
- transaction._sample
- expect(transaction).to_not include_params
+ describe "storing non-Array and non-Hash data" do
+ def perform
+ transaction.set_params("some string")
+ transaction.set_params(123)
+ transaction.set_params(Class.new)
+ set = Set.new
+ set.add("abc")
+ transaction.set_params(set)
+ end
- transaction.set_params(123)
- transaction._sample
- expect(transaction).to_not include_params
+ def expect_unsupported_type_logs(logs)
+ expect(logs).to contains_log(
+ :error,
+ %(Sample data 'params': Unsupported data type 'String' received: "some string")
+ )
+ expect(logs).to contains_log(
+ :error,
+ %(Sample data 'params': Unsupported data type 'Integer' received: 123)
+ )
+ expect(logs).to contains_log(
+ :error,
+ %(Sample data 'params': Unsupported data type 'Class' received: #|\])/
+ )
+ end
- transaction.set_params(Class.new)
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ logs = capture_logs do
+ perform
transaction._sample
- expect(transaction).to_not include_params
+ end
- set = Set.new
- set.add("abc")
- transaction.set_params(set)
- transaction._sample
- expect(transaction).to_not include_params
+ expect(transaction).to_not include_params
+ expect_unsupported_type_logs(logs)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ logs = capture_logs do
+ perform
+ transaction.complete
end
- expect(logs).to contains_log(
- :error,
- %(Sample data 'params': Unsupported data type 'String' received: "some string")
- )
- expect(logs).to contains_log(
- :error,
- %(Sample data 'params': Unsupported data type 'Integer' received: 123)
- )
- expect(logs).to contains_log(
- :error,
- %(Sample data 'params': Unsupported data type 'Class' received: #|\])/
- )
+ expect(root_span.attributes).to_not have_key("appsignal.request.payload")
+ expect_unsupported_type_logs(logs)
+ end
end
- it "does not store data that can't be converted to JSON" do
- klass = Class.new do
- def initialize
- @calls = 0
- end
+ describe "storing data that can't be serialized" do
+ let(:unserializable) do
+ Class.new do
+ def initialize
+ @calls = 0
+ end
- def to_s
- raise "foo" if @calls > 0 # Cause a deliberate error
+ def to_s
+ raise "foo" if @calls > 0 # Cause a deliberate error
- @calls += 1
+ @calls += 1
+ end
end
end
- transaction.set_params(klass.new => 1)
- logs = capture_logs { transaction._sample }
+ def perform
+ transaction.set_params(unserializable.new => 1)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ logs = capture_logs { transaction._sample }
+
+ expect(transaction).to_not include_params
+ expect(logs).to contains_log :error,
+ "Error generating data (RuntimeError: foo) for"
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ logs = capture_logs { transaction.complete }
- expect(transaction).to_not include_params
- expect(logs).to contains_log :error,
- "Error generating data (RuntimeError: foo) for"
+ expect(root_span.attributes).to_not have_key("appsignal.request.payload")
+ expect(logs).to contains_log :error,
+ "Error generating data (RuntimeError: foo) for"
+ end
end
end
describe "#set_sample_data" do
let(:transaction) { new_transaction }
- it "updates the sample data on the transaction" do
- silence do
- transaction.send(
- :set_sample_data,
- "params",
- :controller => "blog_posts",
- :action => "show",
- :id => "1"
- )
+ describe "updating the sample data on the transaction" do
+ def perform
+ silence do
+ transaction.send(
+ :set_sample_data,
+ "params",
+ :controller => "blog_posts",
+ :action => "show",
+ :id => "1"
+ )
+ end
end
- expect(transaction).to include_params(
- "action" => "show",
- "controller" => "blog_posts",
- "id" => "1"
- )
+ let(:expected) do
+ { "action" => "show", "controller" => "blog_posts", "id" => "1" }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to include_params(expected)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"])).to eq(expected)
+ end
end
context "when the data is no Array or Hash" do
- it "does not update the sample data on the transaction" do
- logs =
- capture_logs do
- silence { transaction.send(:set_sample_data, "params", "string") }
- end
+ describe "not updating the sample data" do
+ def perform
+ silence { transaction.send(:set_sample_data, "params", "string") }
+ end
- expect(transaction.to_h["sample_data"]).to eq({})
- expect(logs).to contains_log :error,
- %(Invalid sample data for 'params'. Value is not an Array or Hash: '"string"')
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ logs = capture_logs { perform }
+
+ expect(transaction.to_h["sample_data"]).to eq({})
+ expect(logs).to contains_log :error,
+ %(Invalid sample data for 'params'. Value is not an Array or Hash: '"string"')
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ logs = capture_logs { perform }
+ transaction.complete
+
+ expect(root_span.attributes).to_not have_key("appsignal.request.payload")
+ expect(logs).to contains_log :error,
+ %(Invalid sample data for 'params'. Value is not an Array or Hash: '"string"')
+ end
end
end
- context "when the data cannot be converted to JSON" do
- it "does not update the sample data on the transaction" do
- klass = Class.new do
- def to_s
- raise "foo" # Cause a deliberate error
+ context "when the data cannot be converted" do
+ # The direct call skips sanitization, so the raw object reaches the
+ # backend serializer (`Data.generate` in agent mode, `JSON.generate` in
+ # collector mode); both call `to_s` and rescue the resulting error.
+ describe "not updating the sample data" do
+ let(:unserializable) do
+ Class.new do
+ def to_s
+ raise "foo" # Cause a deliberate error
+ end
end
end
- logs =
- capture_logs do
- silence { transaction.send(:set_sample_data, "params", klass.new => 1) }
- end
- expect(transaction).to_not include_params
- expect(logs).to contains_log :error,
- "Error generating data (RuntimeError: foo) for"
+ def perform
+ silence { transaction.send(:set_sample_data, "params", unserializable.new => 1) }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ logs = capture_logs { perform }
+
+ expect(transaction).to_not include_params
+ expect(logs).to contains_log :error,
+ "Error generating data (RuntimeError: foo) for"
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ logs = capture_logs { perform }
+ transaction.complete
+
+ expect(root_span.attributes).to_not have_key("appsignal.request.payload")
+ expect(logs).to contains_log :error,
+ "Error generating data (RuntimeError: foo) for"
+ end
end
end
end
@@ -1746,47 +3153,200 @@ def to_s
e
end
- # Report the wrapper on another transaction first
- transaction1 = new_transaction
- transaction1.add_error(error)
- expect(transaction1).to have_error
+ # Report the wrapper on another transaction first
+ transaction1 = new_transaction
+ transaction1.add_error(error)
+ expect(transaction1).to have_error
+
+ transaction2 = new_transaction
+ transaction2.add_error(error.cause)
+ expect(transaction).to_not have_error
+ end
+ end
+
+ context "when a block is given" do
+ it "stores the block in the error blocks" do
+ block = proc { "block" }
+
+ transaction.add_error(error, &block)
+
+ expect(transaction.error_blocks).to eq({
+ error => [block]
+ })
+ end
+ end
+
+ context "when no error is set in the transaction" do
+ it "sets the error on the transaction" do
+ transaction.add_error(error)
+
+ expect(transaction).to have_error(
+ "ExampleStandardError",
+ "test message",
+ ["line 1"]
+ )
+ end
+
+ it "does store the error in the errors" do
+ transaction.add_error(error)
+
+ expect(transaction.error_blocks).to eq({ error => [] })
+ end
+ end
+
+ describe "recording the error on the span" do
+ def perform
+ transaction.add_error(error)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to have_error(
+ "ExampleStandardError",
+ "test message",
+ ["line 1"]
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- transaction2 = new_transaction
- transaction2.add_error(error.cause)
- expect(transaction).to_not have_error
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(event.attributes["exception.message"]).to eq("test message")
+ expect(event.attributes["exception.stacktrace"]).to eq("line 1")
+ expect(event.attributes).not_to have_key("appsignal.error_causes")
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
end
end
- context "when a block is given" do
- it "stores the block in the error blocks" do
- block = proc { "block" }
+ describe "recording an error that has causes" do
+ let(:error) do
+ cause = ExampleStandardError.new("cause message").tap do |e|
+ e.set_backtrace(["/path/cause.rb:1:in `cause_method'"])
+ end
+ ExampleException.new("wrapper message").tap do |e|
+ e.set_backtrace(["/path/wrapper.rb:2:in `wrapper_method'"])
+ allow(e).to receive(:cause).and_return(cause)
+ end
+ end
- transaction.add_error(error, &block)
+ def perform
+ # Hide Rails so the backtrace isn't run through its cleaner, keeping the
+ # asserted lines deterministic (mirrors the error-causes sample-data spec).
+ hide_const("Rails")
+ transaction.add_error(error)
+ end
- expect(transaction.error_blocks).to eq({
- error => [block]
- })
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to have_error("ExampleException", "wrapper message")
+ expect(transaction).to include_error_causes(
+ [hash_including("name" => "ExampleStandardError", "message" => "cause message")]
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ # `appsignal.error_causes` matches the processor's ErrorSubCause shape:
+ # name / message / lines (full cleaned backtrace per cause).
+ expect(JSON.parse(event.attributes["appsignal.error_causes"])).to eq(
+ [
+ {
+ "name" => "ExampleStandardError",
+ "message" => "cause message",
+ "lines" => ["/path/cause.rb:1:in `cause_method'"]
+ }
+ ]
+ )
end
end
- context "when no error is set in the transaction" do
- it "sets the error on the transaction" do
+ describe "recording multiple errors" do
+ let(:other_error) do
+ ExampleStandardError.new("other message").tap { |e| e.set_backtrace(["line 2"]) }
+ end
+
+ def perform
transaction.add_error(error)
+ transaction.add_error(other_error)
+ end
- expect(transaction).to have_error(
- "ExampleStandardError",
- "test message",
- ["line 1"]
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+ # The extension holds one error per transaction, so the extra error is
+ # reported as a duplicate transaction.
+ expect { transaction.complete }.to change { created_transactions.count }.by(1)
+
+ original_transaction, duplicate_transaction = created_transactions
+ expect(original_transaction).to have_error(
+ "ExampleStandardError", "test message", ["line 1"]
+ )
+ expect(duplicate_transaction).to have_error(
+ "ExampleStandardError", "other message", ["line 2"]
)
end
- it "does store the error in the errors" do
- transaction.add_error(error)
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- expect(transaction.error_blocks).to eq({ error => [] })
+ # One trace: a single root span carrying one exception event per error.
+ root_spans = span_exporter.finished_spans.select do |span|
+ [:server, :consumer].include?(span.kind)
+ end
+ expect(root_spans.size).to eq(1)
+
+ events = root_spans.first.events.select { |e| e.name == "exception" }
+ expect(events.map { |e| e.attributes["exception.type"] })
+ .to contain_exactly("ExampleStandardError", "ExampleStandardError")
+ expect(events.map { |e| e.attributes["exception.message"] })
+ .to contain_exactly("test message", "other message")
end
end
+ # Collector-mode-specific behavior (no agent-mode analog): the error is
+ # recorded on the span that is current when `add_error` is called.
+ it "records the error on the current event span", :collector_mode do
+ start_collector_agent
+ transaction.start_event
+ transaction.add_error(error)
+ transaction.finish_event("query", "title", "body", Appsignal::EventFormatter::DEFAULT)
+ transaction.complete
+
+ event_span = event_spans.find { |span| span.attributes["appsignal.category"] == "query" }
+ expect(event_span.events.map(&:name)).to include("exception")
+ expect(Array(root_span.events).map(&:name)).not_to include("exception")
+ end
+
+ # Collector-mode-specific: errors collapse onto one trace, so error blocks
+ # merge onto the transaction in order -- the last-added error wins on a
+ # shared key.
+ it "applies error blocks in order, last-added error wins", :collector_mode do
+ start_collector_agent
+ second_error = ExampleStandardError.new("second message")
+ transaction.add_error(error) { |t| t.set_action("FirstAction") }
+ transaction.add_error(second_error) { |t| t.set_action("SecondAction") }
+ transaction.complete
+
+ expect(root_span.name).to eq("SecondAction")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("SecondAction")
+ end
+
context "when an error is already set in the transaction" do
let(:other_error) do
ExampleStandardError.new("other test message").tap do |e|
@@ -1906,17 +3466,31 @@ def to_s
"\"index_users_on_email\" DETAIL: Key (email)=(test@test.com) already exists."
)
end
- before do
- stub_const("PG::UniqueViolation", Class.new(StandardError))
+ let(:sanitized_message) do
+ "ERROR: duplicate key value violates unique constraint " \
+ "\"index_users_on_email\" DETAIL: Key (email)=(?) already exists."
+ end
+ before { stub_const("PG::UniqueViolation", Class.new(StandardError)) }
+
+ def perform
transaction.add_error(error)
end
- it "returns a sanizited error message" do
- expect(transaction).to have_error(
- "PG::UniqueViolation",
- "ERROR: duplicate key value violates unique constraint " \
- "\"index_users_on_email\" DETAIL: Key (email)=(?) already exists."
- )
+ it "returns a sanizited error message in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to have_error("PG::UniqueViolation", sanitized_message)
+ end
+
+ it "records a sanitized error message in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event = exception_event
+ expect(event.attributes["exception.type"]).to eq("PG::UniqueViolation")
+ expect(event.attributes["exception.message"]).to eq(sanitized_message)
end
end
@@ -1927,26 +3501,45 @@ def to_s
"\"example_constraint\"\nDETAIL: Key (email)=(foo@example.com) already exists."
)
end
- before do
- stub_const("ActiveRecord::RecordNotUnique", Class.new(StandardError))
+ let(:sanitized_message) do
+ "PG::UniqueViolation: ERROR: duplicate key value violates unique constraint " \
+ "\"example_constraint\"\nDETAIL: Key (email)=(?) already exists."
+ end
+ before { stub_const("ActiveRecord::RecordNotUnique", Class.new(StandardError)) }
+
+ def perform
transaction.add_error(error)
end
- it "returns a sanizited error message" do
- expect(transaction).to have_error(
- "ActiveRecord::RecordNotUnique",
- "PG::UniqueViolation: ERROR: duplicate key value violates unique constraint " \
- "\"example_constraint\"\nDETAIL: Key (email)=(?) already exists."
- )
+ it "returns a sanizited error message in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
+ expect(transaction).to have_error("ActiveRecord::RecordNotUnique", sanitized_message)
+ end
+
+ it "records a sanitized error message in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event = exception_event
+ expect(event.attributes["exception.type"]).to eq("ActiveRecord::RecordNotUnique")
+ expect(event.attributes["exception.message"]).to eq(sanitized_message)
end
end
context "with Rails module but without backtrace_cleaner method" do
- it "returns the backtrace uncleaned" do
+ def perform
stub_const("Rails", Module.new)
error = ExampleStandardError.new("error message")
error.set_backtrace(["line 1", "line 2"])
transaction.add_error(error)
+ end
+
+ it "returns the backtrace uncleaned in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
expect(last_transaction).to have_error(
"ExampleStandardError",
@@ -1954,6 +3547,15 @@ def to_s
["line 1", "line 2"]
)
end
+
+ it "records the backtrace uncleaned in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event = exception_event
+ expect(event.attributes["exception.stacktrace"]).to eq("line 1\nline 2")
+ end
end
if rails_present?
@@ -1972,18 +3574,40 @@ def to_s
::Rails.backtrace_cleaner.add_filter(&test_filter)
end
- it "cleans the backtrace with the Rails backtrace cleaner" do
+ def perform
error = ExampleStandardError.new("error message")
error.set_backtrace(["line 1", "line 2"])
transaction.add_error(error)
+ end
+
+ it "cleans the backtrace with the Rails backtrace cleaner in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ perform
+
expect(last_transaction).to have_error(
"ExampleStandardError",
"error message",
["line 1", "line ?"]
)
end
+
+ it "cleans the backtrace with the Rails backtrace cleaner in collector mode",
+ :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event = exception_event
+ expect(event.attributes["exception.stacktrace"]).to eq("line 1\nline ?")
+ end
end
end
+
+ # The completed root span's sole `exception` span-event, for asserting
+ # collector-mode error attributes.
+ def exception_event
+ root_span.events.find { |event| event.name == "exception" }
+ end
end
describe "#_set_error" do
@@ -1995,6 +3619,11 @@ def to_s
end
end
+ # The completed root span's sole `exception` span-event.
+ def exception_event
+ root_span.events.find { |event| event.name == "exception" }
+ end
+
it "responds to add_exception for backwards compatibility" do
expect(transaction).to respond_to(:add_exception)
end
@@ -2018,11 +3647,20 @@ def to_s
end
context "when the error has no causes" do
- it "should set an empty causes array as sample data" do
+ it "should set an empty causes array as sample data", :agent_mode do
+ start_agent(**start_agent_args)
transaction.send(:_set_error, error)
expect(transaction).to include_error_causes([])
end
+
+ it "sets no error causes attribute in collector mode", :collector_mode do
+ start_collector_agent
+ transaction.send(:_set_error, error)
+ transaction.complete
+
+ expect(exception_event.attributes).not_to have_key("appsignal.error_causes")
+ end
end
context "when the error has multiple causes" do
@@ -2060,7 +3698,8 @@ def to_s
end
let(:options) { { :revision => "my_revision" } }
- it "sends the error causes information as sample data" do
+ it "sends the error causes information as sample data", :agent_mode do
+ start_agent(**start_agent_args)
# Hide Rails so we can test the normal Ruby behavior. The Rails
# behavior is tested in another spec.
hide_const("Rails")
@@ -2111,6 +3750,45 @@ def to_s
)
end
+ # The collector-mode cause channel is `appsignal.error_causes`, which
+ # carries the full cleaned backtrace per cause (`lines`) rather than the
+ # agent's `first_line`-only projection.
+ it "records the error causes on the exception event in collector mode", :collector_mode do
+ start_collector_agent
+ hide_const("Rails")
+
+ transaction.send(:_set_error, error)
+ transaction.complete
+
+ expect(JSON.parse(exception_event.attributes["appsignal.error_causes"])).to eq(
+ [
+ {
+ "name" => "RuntimeError",
+ "message" => "cause message",
+ "lines" => [
+ "my_gem (1.2.3) /absolute/path/example.rb:123:in `my_method'",
+ "other_gem (4.5.6) /absolute/path/context.rb:456:in `context_method'",
+ "other_gem (4.5.6) /absolute/path/suite.rb:789:in `suite_method'"
+ ]
+ },
+ {
+ "name" => "StandardError",
+ "message" => "cause message 2",
+ "lines" => [
+ "src/example.rb:123:in `my_method'",
+ "context.rb:456:in `context_method'",
+ "suite.rb:789:in `suite_method'"
+ ]
+ },
+ {
+ "name" => "StandardError",
+ "message" => "cause message 3",
+ "lines" => []
+ }
+ ]
+ )
+ end
+
it "does not keep error causes from previously set errors" do
transaction.send(:_set_error, error)
transaction.send(:_set_error, error_without_cause)
@@ -2282,7 +3960,8 @@ def to_s
e
end
- it "sends only the first causes as sample data" do
+ it "sends only the first causes as sample data", :agent_mode do
+ start_agent(**start_agent_args)
expected_error_causes =
Array.new(10) do |i|
{
@@ -2308,6 +3987,33 @@ def to_s
"will be reported."
)
end
+
+ it "records only the first causes on the exception event in collector mode",
+ :collector_mode do
+ start_collector_agent
+ expected_error_causes =
+ Array.new(10) do |i|
+ {
+ "name" => "ExampleStandardError",
+ "message" => "wrapper error #{9 - i}",
+ "lines" => []
+ }
+ end
+
+ logs = capture_logs do
+ transaction.send(:_set_error, error)
+ transaction.complete
+ end
+
+ expect(JSON.parse(exception_event.attributes["appsignal.error_causes"]))
+ .to eq(expected_error_causes)
+ expect(logs).to contains_log(
+ :debug,
+ "Appsignal::Transaction#add_error: Error has more " \
+ "than 10 error causes. Only the first 10 " \
+ "will be reported."
+ )
+ end
end
context "when error message is nil" do
@@ -2322,7 +4028,8 @@ def to_s
transaction.send(:_set_error, error)
end
- it "sets an error on the transaction without an error message" do
+ it "sets an error on the transaction without an error message", :agent_mode do
+ start_agent(**start_agent_args)
transaction.send(:_set_error, error)
expect(transaction).to have_error(
@@ -2331,6 +4038,16 @@ def to_s
["line 1"]
)
end
+
+ it "records an empty error message on the exception event in collector mode",
+ :collector_mode do
+ start_collector_agent
+ transaction.send(:_set_error, error)
+ transaction.complete
+
+ expect(exception_event.attributes["exception.type"]).to eq("ExampleStandardError")
+ expect(exception_event.attributes["exception.message"]).to eq("")
+ end
end
end
@@ -2471,7 +4188,8 @@ def to_s
end
context "when the transaction has several errors" do
- it "calls the given hook for each of the duplicate error transactions" do
+ it "calls the given hook for each of the duplicate error transactions", :agent_mode do
+ start_agent(**start_agent_args)
block = proc do |transaction, error|
transaction.set_action(error.message)
end
@@ -2496,6 +4214,26 @@ def to_s
have_action("hook_error_second")
)
end
+
+ it "calls the hook once with the first error in collector mode", :collector_mode do
+ start_collector_agent
+ block = proc do |transaction, error|
+ transaction.set_action(error.message)
+ end
+
+ Appsignal::Transaction.before_complete(&block)
+
+ transaction = new_transaction
+ transaction.set_error(ExampleStandardError.new("hook_error_first"))
+ transaction.set_error(ExampleStandardError.new("hook_error_second"))
+
+ expect(block).to receive(:call).once.and_call_original
+
+ transaction.complete
+
+ # One trace, so the hook runs once with the first error.
+ expect(root_span.name).to eq("hook_error_first")
+ end
end
context "when the transaction does not have an error" do
@@ -2545,15 +4283,23 @@ def to_s
let(:transaction) { new_transaction }
it "starts the event in the extension" do
- expect(transaction.ext).to receive(:start_event).with(0).and_call_original
+ expect(transaction.backend).to receive(:start_event)
+ .with(:opentelemetry_kind => nil).and_call_original
transaction.start_event
end
+ it "passes the opentelemetry_kind to the backend" do
+ expect(transaction.backend).to receive(:start_event)
+ .with(:opentelemetry_kind => :client).and_call_original
+
+ transaction.start_event(:opentelemetry_kind => :client)
+ end
+
context "when transaction is paused" do
it "does not start the event" do
transaction.pause!
- expect(transaction.ext).to_not receive(:start_event)
+ expect(transaction.backend).to_not receive(:start_event)
transaction.start_event
end
@@ -2562,15 +4308,13 @@ def to_s
describe "#finish_event" do
let(:transaction) { new_transaction }
- let(:fake_gc_time) { 0 }
it "should finish the event in the extension" do
- expect(transaction.ext).to receive(:finish_event).with(
+ expect(transaction.backend).to receive(:finish_event).with(
"name",
"title",
"body",
- 1,
- fake_gc_time
+ 1
).and_call_original
transaction.finish_event(
@@ -2582,12 +4326,11 @@ def to_s
end
it "should finish the event in the extension with nil arguments" do
- expect(transaction.ext).to receive(:finish_event).with(
+ expect(transaction.backend).to receive(:finish_event).with(
"name",
"",
"",
- 0,
- fake_gc_time
+ 0
).and_call_original
transaction.finish_event(
@@ -2601,7 +4344,7 @@ def to_s
context "when transaction is paused" do
it "does not finish the event" do
transaction.pause!
- expect(transaction.ext).to_not receive(:finish_event)
+ expect(transaction.backend).to_not receive(:finish_event)
transaction.start_event
end
@@ -2610,16 +4353,15 @@ def to_s
describe "#record_event" do
let(:transaction) { new_transaction }
- let(:fake_gc_time) { 0 }
it "should record the event in the extension" do
- expect(transaction.ext).to receive(:record_event).with(
+ expect(transaction.backend).to receive(:record_event).with(
"name",
"title",
"body",
1,
1000,
- fake_gc_time
+ :opentelemetry_kind => nil
).and_call_original
transaction.record_event(
@@ -2632,13 +4374,13 @@ def to_s
end
it "should finish the event in the extension with nil arguments" do
- expect(transaction.ext).to receive(:record_event).with(
+ expect(transaction.backend).to receive(:record_event).with(
"name",
"",
"",
0,
1000,
- fake_gc_time
+ :opentelemetry_kind => nil
).and_call_original
transaction.record_event(
@@ -2653,7 +4395,7 @@ def to_s
context "when transaction is paused" do
it "does not record the event" do
transaction.pause!
- expect(transaction.ext).to_not receive(:record_event)
+ expect(transaction.backend).to_not receive(:record_event)
transaction.record_event(
"name",
@@ -2664,6 +4406,42 @@ def to_s
)
end
end
+
+ describe "recording an event with the given duration" do
+ let(:duration_ns) { 1_000_000_000 }
+
+ def perform(transaction)
+ transaction.record_event("custom.event", "T", "B", duration_ns,
+ Appsignal::EventFormatter::DEFAULT)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ perform(transaction)
+ Appsignal::Transaction.complete_current!
+
+ expect(transaction).to include_event(
+ "name" => "custom.event",
+ "title" => "T",
+ "body" => "B"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ perform(transaction)
+ Appsignal::Transaction.complete_current!
+
+ span = event_spans.first
+ expect(span.name).to eq("T")
+ expect(span.attributes["appsignal.category"]).to eq("custom.event")
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ observed = span.end_timestamp - span.start_timestamp
+ expect(observed).to be_within(50_000_000).of(duration_ns)
+ end
+ end
end
describe "#instrument" do
@@ -2671,6 +4449,185 @@ def to_s
let(:transaction) { new_transaction }
let(:instrumenter) { transaction }
end
+
+ describe "block return value" do
+ it_in_both_modes do
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ result = transaction.instrument("sql.active_record", "Query", "SELECT 1",
+ Appsignal::EventFormatter::SQL_BODY_FORMAT) { 42 }
+
+ expect(result).to eq(42)
+ end
+ end
+
+ describe "block raising an exception" do
+ it_in_both_modes do
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+
+ expect do
+ transaction.instrument("x.y", nil, nil, Appsignal::EventFormatter::DEFAULT) do
+ raise "boom"
+ end
+ end.to raise_error("boom")
+ end
+ end
+
+ describe "instrumenting a SQL event" do
+ def perform(transaction)
+ transaction.instrument("sql.active_record", "Query", "SELECT 1",
+ Appsignal::EventFormatter::SQL_BODY_FORMAT) { nil }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ perform(transaction)
+ Appsignal::Transaction.complete_current!
+
+ expect(transaction).to include_event(
+ "name" => "sql.active_record",
+ "title" => "Query",
+ "body" => "SELECT 1",
+ "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ perform(transaction)
+ Appsignal::Transaction.complete_current!
+
+ span = event_spans.first
+ expect(span.name).to eq("Query")
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes).to include(
+ "db.query.text" => "SELECT 1",
+ "db.system.name" => "other_sql",
+ "appsignal.category" => "sql.active_record"
+ )
+ expect(span.attributes).not_to have_key("appsignal.body")
+ end
+ end
+
+ describe "instrumenting a default-format event" do
+ def perform(transaction)
+ transaction.instrument("custom.event", "Title", "Body",
+ Appsignal::EventFormatter::DEFAULT) { nil }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ perform(transaction)
+ Appsignal::Transaction.complete_current!
+
+ expect(transaction).to include_event(
+ "name" => "custom.event",
+ "title" => "Title",
+ "body" => "Body",
+ "body_format" => Appsignal::EventFormatter::DEFAULT
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ perform(transaction)
+ Appsignal::Transaction.complete_current!
+
+ span = event_spans.first
+ expect(span.name).to eq("Title")
+ expect(span.attributes).to include(
+ "appsignal.body" => "Body",
+ "appsignal.category" => "custom.event"
+ )
+ expect(span.attributes).not_to have_key("db.query.text")
+ expect(span.attributes).not_to have_key("db.system.name")
+ end
+ end
+
+ describe "nesting instrumented events" do
+ def perform(transaction)
+ transaction.instrument("outer.event", "Outer", "outer body",
+ Appsignal::EventFormatter::DEFAULT) do
+ transaction.instrument("inner.event", "Inner", "inner body",
+ Appsignal::EventFormatter::DEFAULT) { nil }
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent(**start_agent_args)
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ perform(transaction)
+ Appsignal::Transaction.complete_current!
+
+ expect(transaction).to include_event(
+ "name" => "outer.event", "title" => "Outer", "body" => "outer body"
+ )
+ expect(transaction).to include_event(
+ "name" => "inner.event", "title" => "Inner", "body" => "inner body"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ perform(transaction)
+ Appsignal::Transaction.complete_current!
+
+ outer = event_spans.find { |s| s.attributes["appsignal.category"] == "outer.event" }
+ inner = event_spans.find { |s| s.attributes["appsignal.category"] == "inner.event" }
+
+ expect(inner.parent_span_id).to eq(outer.span_id)
+ expect(outer.parent_span_id).to eq(root_span.span_id)
+ end
+ end
+
+ describe "with an empty title" do
+ it "names the span after the event name and omits appsignal.title", :collector_mode do
+ start_collector_agent
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ transaction.instrument("custom.event", nil, "Body",
+ Appsignal::EventFormatter::DEFAULT) { nil }
+ Appsignal::Transaction.complete_current!
+
+ span = event_spans.first
+ expect(span.name).to eq("custom.event")
+ expect(span.attributes["appsignal.category"]).to eq("custom.event")
+ expect(span.attributes).not_to have_key("appsignal.title")
+ end
+ end
+
+ describe "with an empty body" do
+ it "omits the body attribute on the span", :collector_mode do
+ start_collector_agent
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ transaction.instrument("custom.event", "Title", nil,
+ Appsignal::EventFormatter::DEFAULT) { nil }
+ Appsignal::Transaction.complete_current!
+
+ attrs = event_spans.first.attributes
+ expect(attrs).not_to have_key("appsignal.body")
+ expect(attrs).not_to have_key("db.query.text")
+ end
+ end
+
+ describe "OpenTelemetry current context during the block" do
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ transaction = create_transaction(Appsignal::Transaction::HTTP_REQUEST)
+ root_span_id = ::OpenTelemetry::Trace.current_span.context.span_id
+
+ event_span_id_during_block = nil
+ transaction.instrument("custom.event", "T", "B", Appsignal::EventFormatter::DEFAULT) do
+ event_span_id_during_block = ::OpenTelemetry::Trace.current_span.context.span_id
+ end
+
+ expect(event_span_id_during_block).not_to eq(root_span_id)
+ expect(::OpenTelemetry::Trace.current_span.context.span_id).to eq(root_span_id)
+ end
+ end
end
# private
@@ -2695,7 +4652,7 @@ def to_s
context "when the extension returns invalid serialized JSON" do
before do
- expect(transaction.ext).to receive(:to_json).and_return("foo")
+ expect(transaction.backend).to receive(:to_json).and_return("foo")
end
it "raises a JSON parse error" do
diff --git a/spec/lib/appsignal_spec.rb b/spec/lib/appsignal_spec.rb
index 423ee99ed..b7b3924d7 100644
--- a/spec/lib/appsignal_spec.rb
+++ b/spec/lib/appsignal_spec.rb
@@ -867,6 +867,39 @@ def on_start
expect(Appsignal.internal_logger.level).to eq Logger::DEBUG
end
end
+
+ if DependencyHelper.opentelemetry_present?
+ context "when collector_endpoint is set but the OpenTelemetry SDK fails to boot" do
+ let(:err_stream) { std_stream }
+ let(:stdout_stream) { std_stream }
+
+ before do
+ # Simulate a failure inside `Appsignal::OpenTelemetry.configure` —
+ # e.g. one of the OTel gems can't be loaded. The rescue inside
+ # `configure` should set `started?` to false instead of letting the
+ # error bubble out.
+ allow(Appsignal::OpenTelemetry).to receive(:require)
+ .with("opentelemetry/sdk")
+ .and_raise(LoadError, "fake load failure")
+ end
+
+ it "falls back to the agent backend rather than silently dropping telemetry" do
+ capture_std_streams(stdout_stream, err_stream) do
+ start_agent(:options => { :collector_endpoint => "http://127.0.0.1:9090" })
+ end
+
+ # Config still records the user's intent.
+ expect(Appsignal.config.collector_mode_configured?).to be(true)
+ # But the active predicate is false because the SDK never booted.
+ expect(Appsignal.config.collector_mode?).to be(false)
+ expect(Appsignal::OpenTelemetry.started?).to be(false)
+
+ # Backends fall through to the extension implementations.
+ expect(Appsignal::Backends.metrics).to eq(Appsignal::Metrics::ExtensionBackend)
+ expect(Appsignal::Backends.logger).to eq(Appsignal::Logger::ExtensionBackend)
+ end
+ end
+ end
end
describe ".load" do
@@ -897,9 +930,25 @@ def on_start
Appsignal.start
end
- it "starts the logger and extension" do
- expect(Appsignal).to receive(:_start_logger)
- expect(Appsignal::Extension).to receive(:start)
+ it "starts the logger before restarting the extension" do
+ expect(Appsignal).to receive(:_start_logger).ordered
+ expect(Appsignal::Extension).to receive(:start).ordered
+
+ expect(Appsignal.forked).to be_nil
+ end
+
+ it "does not stop the extension before restarting it" do
+ allow(Appsignal).to receive(:_start_logger)
+ allow(Appsignal::Extension).to receive(:start)
+ expect(Appsignal::Extension).to_not receive(:stop)
+
+ Appsignal.forked
+ end
+
+ it "does not restart minutely probes (probe thread dies on fork by design)" do
+ allow(Appsignal).to receive(:_start_logger)
+ allow(Appsignal::Extension).to receive(:start)
+ expect(Appsignal::Probes).to_not receive(:start)
Appsignal.forked
end
@@ -934,6 +983,33 @@ def on_start
expect(Appsignal::CheckIn.scheduler).to receive(:stop)
Appsignal.stop
end
+
+ if DependencyHelper.opentelemetry_present?
+ context "in collector mode" do
+ before do
+ Appsignal.clear!
+ start_agent(:options => { :collector_endpoint => "http://127.0.0.1:9090" })
+ end
+
+ it "shuts down the OpenTelemetry providers so buffered telemetry flushes" do
+ expect(::OpenTelemetry.tracer_provider).to receive(:shutdown)
+ expect(::OpenTelemetry.meter_provider).to receive(:shutdown)
+ expect(::OpenTelemetry.logger_provider).to receive(:shutdown)
+ Appsignal.stop
+ end
+ end
+ end
+
+ context "when not in collector mode" do
+ it "calls Appsignal::OpenTelemetry.shutdown, which short-circuits as a no-op" do
+ # `configure` was not called in this spec, so `started?` is false
+ # and `shutdown` returns immediately without touching the API gem's
+ # proxy providers (whose `shutdown` isn't defined until an SDK is
+ # wired up).
+ expect(Appsignal::OpenTelemetry.started?).to be(false)
+ expect { Appsignal.stop }.not_to raise_error
+ end
+ end
end
describe ".started?" do
@@ -1064,94 +1140,241 @@ def on_start
end
context "with config and started" do
- before { start_agent }
+ # Only auto-start for non-mode examples. Mode-tagged examples
+ # (`:agent_mode`/`:collector_mode`) start the agent in their own body (the
+ # dual-mode start principle), so starting it here too would clobber the
+ # collector-mode setup.
+ before do |example|
+ start_agent unless example.metadata[:agent_mode] || example.metadata[:collector_mode]
+ end
around { |example| keep_transactions { example.run } }
describe ".monitor" do
- it "creates a transaction" do
- expect do
+ describe "creating a transaction" do
+ def perform
Appsignal.monitor(:action => "MyAction")
- end.to(change { created_transactions.count }.by(1))
+ end
- transaction = last_transaction
- expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
- expect(transaction).to have_action("MyAction")
- expect(transaction).to_not have_error
- expect(transaction).to_not include_events
- expect(transaction).to_not have_queue_start
- expect(transaction).to be_completed
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ transaction = last_transaction
+ expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ expect(transaction).to have_action("MyAction")
+ expect(transaction).to_not have_error
+ expect(transaction).to_not include_events
+ expect(transaction).to_not have_queue_start
+ expect(transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ expect { perform }.to(change { created_transactions.count }.by(1))
+
+ # HTTP_REQUEST maps to a SERVER span (a subtrace root).
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ expect(root_span.name).to eq("MyAction")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("MyAction")
+ expect(exception_events).to be_empty
+ expect(event_spans).to be_empty
+ expect(last_transaction).to be_completed
+ end
end
- it "returns the block's return value" do
+ it_in_both_modes "returns the block's return value" do
expect(Appsignal.monitor(:action => nil) { :return_value }).to eq(:return_value)
end
- it "sets a custom namespace via the namespace argument" do
- Appsignal.monitor(:namespace => "custom", :action => nil)
+ describe "setting a custom namespace via the namespace argument" do
+ def perform
+ Appsignal.monitor(:namespace => "custom", :action => nil)
+ end
- expect(last_transaction).to have_namespace("custom")
- end
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
- it "doesn't overwrite custom namespace set in the block" do
- Appsignal.monitor(:namespace => "custom", :action => nil) do
- Appsignal.set_namespace("more custom")
+ expect(last_transaction).to have_namespace("custom")
end
- expect(last_transaction).to have_namespace("more custom")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.attributes["appsignal.namespace"]).to eq("custom")
+ end
end
- it "sets the action via the action argument using a string" do
- Appsignal.monitor(:action => "custom")
+ describe "not overwriting a custom namespace set in the block" do
+ def perform
+ Appsignal.monitor(:namespace => "custom", :action => nil) do
+ Appsignal.set_namespace("more custom")
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_namespace("more custom")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to have_action("custom")
+ expect(root_span.attributes["appsignal.namespace"]).to eq("more custom")
+ end
end
- it "sets the action via the action argument using a symbol" do
- Appsignal.monitor(:action => :custom)
+ describe "setting the action via the action argument" do
+ def perform(action)
+ Appsignal.monitor(:action => action)
+ end
+
+ it "in agent mode using a string", :agent_mode do
+ start_agent
+ perform("custom")
+
+ expect(last_transaction).to have_action("custom")
+ end
+
+ it "in collector mode using a string", :collector_mode do
+ start_collector_agent
+ perform("custom")
+
+ expect(root_span.name).to eq("custom")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("custom")
+ end
+
+ it "in agent mode using a symbol", :agent_mode do
+ start_agent
+ perform(:custom)
+
+ expect(last_transaction).to have_action("custom")
+ end
+
+ it "in collector mode using a symbol", :collector_mode do
+ start_collector_agent
+ perform(:custom)
- expect(last_transaction).to have_action("custom")
+ expect(root_span.name).to eq("custom")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("custom")
+ end
end
- it "doesn't overwrite custom action set in the block" do
- Appsignal.monitor(:action => "custom") do
- Appsignal.set_action("more custom")
+ describe "not overwriting a custom action set in the block" do
+ def perform
+ Appsignal.monitor(:action => "custom") do
+ Appsignal.set_action("more custom")
+ end
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_action("more custom")
end
- expect(last_transaction).to have_action("more custom")
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.name).to eq("more custom")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("more custom")
+ end
end
- it "doesn't set the action when value is nil" do
- Appsignal.monitor(:action => nil)
+ describe "not setting the action when the value is nil" do
+ def perform
+ Appsignal.monitor(:action => nil)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to_not have_action
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
- it "doesn't set the action when value is :set_later" do
- Appsignal.monitor(:action => :set_later)
+ describe "not setting the action when the value is :set_later" do
+ def perform
+ Appsignal.monitor(:action => :set_later)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to_not have_action
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to_not have_action
+ expect(root_span.attributes).to_not have_key("appsignal.action_name")
+ end
end
- it "reports exceptions that occur in the block" do
- expect do
- Appsignal.monitor :action => nil do
- raise ExampleException, "error message"
- end
- end.to raise_error(ExampleException, "error message")
+ describe "reporting exceptions that occur in the block" do
+ def perform
+ expect do
+ Appsignal.monitor :action => nil do
+ raise ExampleException, "error message"
+ end
+ end.to raise_error(ExampleException, "error message")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
- expect(last_transaction).to have_error("ExampleException", "error message")
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
- context "with already active transction" do
+ context "with an already active transaction" do
let(:err_stream) { std_stream }
let(:stderr) { err_stream.read }
let(:transaction) { http_request_transaction }
- before do
+
+ # The parent transaction is built lazily inside each example body (after
+ # the mode context has started the agent), per the dual-mode start
+ # principle -- building it in a `before` would create it against the
+ # wrong backend.
+ def activate_parent_transaction
set_current_transaction(transaction)
transaction.set_action("My action")
end
- it "doesn't create a new transaction" do
+ it_in_both_modes "doesn't create a new transaction" do
+ activate_parent_transaction
logs = nil
expect do
logs =
@@ -1167,19 +1390,54 @@ def on_start
expect(stderr).to include("appsignal WARNING: #{warning}")
end
- it "does not overwrite the parent transaction's namespace" do
- silence { Appsignal.monitor(:namespace => "custom", :action => nil) }
+ describe "not overwriting the parent transaction's namespace" do
+ def perform
+ activate_parent_transaction
+ silence { Appsignal.monitor(:namespace => "custom", :action => nil) }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
- expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ end
end
- it "does not overwrite the parent transaction's action" do
- silence { Appsignal.monitor(:action => "custom") }
+ describe "not overwriting the parent transaction's action" do
+ def perform
+ activate_parent_transaction
+ silence { Appsignal.monitor(:action => "custom") }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(transaction).to have_action("My action")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
- expect(transaction).to have_action("My action")
+ expect(root_span.name).to eq("My action")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("My action")
+ end
end
- it "doesn't complete the parent transaction" do
+ it_in_both_modes "doesn't complete the parent transaction" do
+ activate_parent_transaction
silence { Appsignal.monitor(:action => nil) }
expect(transaction).to_not be_completed
@@ -1219,17 +1477,34 @@ def on_start
end
describe ".tag_request" do
- before { start_agent }
+ before do |example|
+ start_agent unless example.metadata[:agent_mode] || example.metadata[:collector_mode]
+ end
context "with transaction" do
let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
- it "sets tags on the current transaction" do
- Appsignal.tag_request("a" => "b")
+ describe "setting tags on the current transaction" do
+ def perform
+ set_current_transaction(transaction)
+ Appsignal.tag_request("a" => "b")
+ end
- transaction._sample
- expect(transaction).to include_tags("a" => "b")
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ transaction._sample
+ expect(transaction).to include_tags("a" => "b")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(root_span.attributes["appsignal.tag.a"]).to eq("b")
+ end
end
end
@@ -1254,23 +1529,44 @@ def on_start
end
describe ".add_params" do
- before { start_agent }
+ before do |example|
+ start_agent unless example.metadata[:agent_mode] || example.metadata[:collector_mode]
+ end
it "has a .set_params alias" do
expect(Appsignal.method(:add_params)).to eq(Appsignal.method(:set_params))
end
- context "with transaction" do
+ describe "adding parameters through the public API" do
let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
- it "adds parameters to the transaction" do
+ def perform
+ set_current_transaction(transaction)
Appsignal.add_params("param1" => "value1")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction._sample
expect(transaction).to include_params("param1" => "value1")
end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.payload"]))
+ .to eq("param1" => "value1")
+ end
+ end
+
+ context "with transaction" do
+ let(:transaction) { http_request_transaction }
+ before { set_current_transaction(transaction) }
+
it "merges the params if called multiple times" do
Appsignal.add_params("param1" => "value1")
Appsignal.add_params("param2" => "value2")
@@ -1317,23 +1613,44 @@ def on_start
end
describe ".add_session_data" do
- before { start_agent }
+ before do |example|
+ start_agent unless example.metadata[:agent_mode] || example.metadata[:collector_mode]
+ end
it "has a .set_session_data alias" do
expect(Appsignal.method(:add_session_data)).to eq(Appsignal.method(:set_session_data))
end
- context "with transaction" do
+ describe "adding session data through the public API" do
let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
- it "adds session data to the transaction" do
+ def perform
+ set_current_transaction(transaction)
Appsignal.add_session_data("data" => "value1")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction._sample
expect(transaction).to include_session_data("data" => "value1")
end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.request.session_data"]))
+ .to eq("data" => "value1")
+ end
+ end
+
+ context "with transaction" do
+ let(:transaction) { http_request_transaction }
+ before { set_current_transaction(transaction) }
+
it "merges the session data if called multiple times" do
Appsignal.set_session_data("data1" => "value1")
Appsignal.set_session_data("data2" => "value2")
@@ -1363,22 +1680,43 @@ def on_start
end
describe ".add_headers" do
- before { start_agent }
+ before do |example|
+ start_agent unless example.metadata[:agent_mode] || example.metadata[:collector_mode]
+ end
it "has a .set_headers alias" do
expect(Appsignal.method(:add_headers)).to eq(Appsignal.method(:set_headers))
end
- context "with transaction" do
+ describe "adding request headers through the public API" do
let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
- it "adds request headers to the transaction" do
- Appsignal.add_headers("PATH_INFO" => "/some-path")
+ def perform
+ set_current_transaction(transaction)
+ Appsignal.add_headers("HTTP_ACCEPT" => "text/html")
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction._sample
- expect(transaction).to include_environment("PATH_INFO" => "/some-path")
+ expect(transaction).to include_environment("HTTP_ACCEPT" => "text/html")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ # True headers are normalized to the OTel http.request.header.* convention.
+ expect(root_span.attributes["http.request.header.accept"]).to eq("text/html")
end
+ end
+
+ context "with transaction" do
+ let(:transaction) { http_request_transaction }
+ before { set_current_transaction(transaction) }
it "merges the request headers if called multiple times" do
Appsignal.add_headers("PATH_INFO" => "/some-path")
@@ -1409,21 +1747,28 @@ def on_start
end
describe ".add_custom_data" do
- before { start_agent }
+ before do |example|
+ start_agent unless example.metadata[:agent_mode] || example.metadata[:collector_mode]
+ end
it "has a .set_custom_data alias" do
expect(Appsignal.method(:add_custom_data)).to eq(Appsignal.method(:set_custom_data))
end
- context "with transaction" do
+ describe "adding custom data through the public API" do
let(:transaction) { http_request_transaction }
- before { set_current_transaction transaction }
- it "adds custom data to the current transaction" do
+ def perform
+ set_current_transaction(transaction)
Appsignal.add_custom_data(
:user => { :id => 123 },
:organization => { :slug => "appsignal" }
)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction._sample
expect(transaction).to include_custom_data(
@@ -1432,6 +1777,22 @@ def on_start
)
end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ expect(JSON.parse(root_span.attributes["appsignal.custom_data"])).to eq(
+ "user" => { "id" => 123 },
+ "organization" => { "slug" => "appsignal" }
+ )
+ end
+ end
+
+ context "with transaction" do
+ let(:transaction) { http_request_transaction }
+ before { set_current_transaction transaction }
+
it "merges the custom data if called multiple times" do
Appsignal.add_custom_data(:abc => "value")
Appsignal.add_custom_data(:def => "value")
@@ -1457,13 +1818,15 @@ def on_start
end
describe ".add_breadcrumb" do
- before { start_agent }
+ before do |example|
+ start_agent unless example.metadata[:agent_mode] || example.metadata[:collector_mode]
+ end
- context "with transaction" do
+ describe "adding a breadcrumb through the public API" do
let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
- it "adds the breadcrumb to the transaction" do
+ def perform
+ set_current_transaction(transaction)
Appsignal.add_breadcrumb(
"Network",
"http",
@@ -1471,8 +1834,13 @@ def on_start
{ :response => 200 },
fixed_time
)
+ end
- transaction._sample
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ transaction.complete
expect(transaction).to include_breadcrumb(
"http",
"Network",
@@ -1481,6 +1849,20 @@ def on_start
fixed_time
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ # Breadcrumbs are emitted as `appsignal.breadcrumb` span events.
+ breadcrumb = root_span.events.find { |e| e.name == "appsignal.breadcrumb" }
+ expect(breadcrumb).not_to be_nil
+ expect(breadcrumb.attributes["category"]).to eq("Network")
+ expect(breadcrumb.attributes["action"]).to eq("http")
+ expect(breadcrumb.attributes["message"]).to eq("User made network request")
+ expect(JSON.parse(breadcrumb.attributes["metadata"])).to eq("response" => 200)
+ end
end
context "without transaction" do
@@ -1492,158 +1874,87 @@ def on_start
end
end
- describe "custom metrics" do
- let(:tags) { { :foo => "bar" } }
+ describe ".internal_logger" do
+ subject { Appsignal.internal_logger }
- describe ".set_gauge" do
- it "should call set_gauge on the extension with a string key and float" do
- expect(Appsignal::Extension).to receive(:set_gauge)
- .with("key", 0.1, Appsignal::Extension.data_map_new)
- Appsignal.set_gauge("key", 0.1)
- end
+ it { is_expected.to be_a Logger }
+ end
- it "should call set_gauge with tags" do
- expect(Appsignal::Extension).to receive(:set_gauge)
- .with("key", 0.1, Appsignal::Utils::Data.generate(tags))
- Appsignal.set_gauge("key", 0.1, tags)
+ describe ".log_formatter" do
+ subject { Appsignal.log_formatter.call("Debug", Time.parse("2015-07-08"), nil, "log line") }
+
+ it "formats a log" do
+ expect(subject).to eq "[2015-07-08T00:00:00 (process) ##{Process.pid}][Debug] log line\n"
+ end
+
+ context "with prefix" do
+ subject do
+ Appsignal.log_formatter("prefix").call("Debug", Time.parse("2015-07-08"), nil, "log line")
end
- it "should call set_gauge on the extension with a symbol key and int" do
- expect(Appsignal::Extension).to receive(:set_gauge)
- .with("key", 1.0, Appsignal::Extension.data_map_new)
- Appsignal.set_gauge(:key, 1)
+ it "adds a prefix" do
+ expect(subject)
+ .to eq "[2015-07-08T00:00:00 (process) ##{Process.pid}][Debug] prefix: log line\n"
end
+ end
+ end
- it "should not raise an exception when out of range" do
- expect(Appsignal::Extension).to receive(:set_gauge).with(
- "key",
- 10,
- Appsignal::Extension.data_map_new
- ).and_raise(RangeError)
- expect(Appsignal.internal_logger).to receive(:warn)
- .with("The gauge value '10' for metric 'key' is too big")
+ describe ".config" do
+ subject { Appsignal.config }
- Appsignal.set_gauge("key", 10)
- end
+ it { is_expected.to be_a Appsignal::Config }
+ it "should return configuration" do
+ expect(subject[:endpoint]).to eq "https://push.appsignal.com"
end
+ end
- describe ".increment_counter" do
- it "should call increment_counter on the extension with a string key" do
- expect(Appsignal::Extension).to receive(:increment_counter)
- .with("key", 1, Appsignal::Extension.data_map_new)
- Appsignal.increment_counter("key")
- end
+ describe ".send_error" do
+ let(:error) { ExampleException.new("error message") }
+ let(:err_stream) { std_stream }
+ let(:stderr) { err_stream.read }
+ around do |example|
+ keep_transactions { example.run }
+ end
- it "should call increment_counter with tags" do
- expect(Appsignal::Extension).to receive(:increment_counter)
- .with("key", 1, Appsignal::Utils::Data.generate(tags))
- Appsignal.increment_counter("key", 1, tags)
+ describe "sending the error" do
+ def perform
+ Appsignal.send_error(error)
end
- it "should call increment_counter on the extension with a symbol key" do
- expect(Appsignal::Extension).to receive(:increment_counter)
- .with("key", 1, Appsignal::Extension.data_map_new)
- Appsignal.increment_counter(:key)
- end
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect { perform }.to(change { created_transactions.count }.by(1))
- it "should call increment_counter on the extension with a count" do
- expect(Appsignal::Extension).to receive(:increment_counter)
- .with("key", 5, Appsignal::Extension.data_map_new)
- Appsignal.increment_counter("key", 5)
- end
-
- it "should not raise an exception when out of range" do
- expect(Appsignal::Extension).to receive(:increment_counter)
- .with("key", 10, Appsignal::Extension.data_map_new).and_raise(RangeError)
- expect(Appsignal.internal_logger).to receive(:warn)
- .with("The counter value '10' for metric 'key' is too big")
-
- Appsignal.increment_counter("key", 10)
- end
- end
-
- describe ".add_distribution_value" do
- it "should call add_distribution_value on the extension with a string key and float" do
- expect(Appsignal::Extension).to receive(:add_distribution_value)
- .with("key", 0.1, Appsignal::Extension.data_map_new)
- Appsignal.add_distribution_value("key", 0.1)
- end
-
- it "should call add_distribution_value with tags" do
- expect(Appsignal::Extension).to receive(:add_distribution_value)
- .with("key", 0.1, Appsignal::Utils::Data.generate(tags))
- Appsignal.add_distribution_value("key", 0.1, tags)
- end
-
- it "should call add_distribution_value on the extension with a symbol key and int" do
- expect(Appsignal::Extension).to receive(:add_distribution_value)
- .with("key", 1.0, Appsignal::Extension.data_map_new)
- Appsignal.add_distribution_value(:key, 1)
- end
-
- it "should not raise an exception when out of range" do
- expect(Appsignal::Extension).to receive(:add_distribution_value)
- .with("key", 10, Appsignal::Extension.data_map_new).and_raise(RangeError)
- expect(Appsignal.internal_logger).to receive(:warn)
- .with("The distribution value '10' for metric 'key' is too big")
-
- Appsignal.add_distribution_value("key", 10)
+ transaction = last_transaction
+ expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ expect(transaction).to_not have_action
+ expect(transaction).to have_error("ExampleException", "error message")
+ expect(transaction).to_not include_tags
+ expect(transaction).to be_completed
end
- end
- end
-
- describe ".internal_logger" do
- subject { Appsignal.internal_logger }
-
- it { is_expected.to be_a Logger }
- end
-
- describe ".log_formatter" do
- subject { Appsignal.log_formatter.call("Debug", Time.parse("2015-07-08"), nil, "log line") }
- it "formats a log" do
- expect(subject).to eq "[2015-07-08T00:00:00 (process) ##{Process.pid}][Debug] log line\n"
- end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ # send_error completes its throwaway transaction inline, so the root
+ # span is already finished and exported.
+ perform
- context "with prefix" do
- subject do
- Appsignal.log_formatter("prefix").call("Debug", Time.parse("2015-07-08"), nil, "log line")
- end
+ expect(root_span).not_to be_nil
+ # HTTP_REQUEST maps to a SERVER span (a subtrace root).
+ expect(root_span.kind).to eq(:server)
+ expect(root_span.attributes["appsignal.namespace"])
+ .to eq("web")
+ expect(root_span.attributes).not_to have_key("appsignal.action_name")
- it "adds a prefix" do
- expect(subject)
- .to eq "[2015-07-08T00:00:00 (process) ##{Process.pid}][Debug] prefix: log line\n"
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
end
end
- end
-
- describe ".config" do
- subject { Appsignal.config }
-
- it { is_expected.to be_a Appsignal::Config }
- it "should return configuration" do
- expect(subject[:endpoint]).to eq "https://push.appsignal.com"
- end
- end
-
- describe ".send_error" do
- let(:error) { ExampleException.new("error message") }
- let(:err_stream) { std_stream }
- let(:stderr) { err_stream.read }
- around do |example|
- keep_transactions { example.run }
- end
-
- it "sends the error to AppSignal" do
- expect { Appsignal.send_error(error) }.to(change { created_transactions.count }.by(1))
-
- transaction = last_transaction
- expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
- expect(transaction).to_not have_action
- expect(transaction).to have_error("ExampleException", "error message")
- expect(transaction).to_not include_tags
- expect(transaction).to be_completed
- end
context "when given error is not an Exception" do
let(:error) { "string value" }
@@ -1663,15 +1974,38 @@ def on_start
end
context "when given a block" do
- it "yields the transaction and allows additional metadata to be set" do
- Appsignal.send_error(StandardError.new("my_error")) do |transaction|
- transaction.set_action("my_action")
- transaction.set_namespace("my_namespace")
+ describe "yielding the transaction to set metadata" do
+ def perform
+ Appsignal.send_error(StandardError.new("my_error")) do |transaction|
+ transaction.set_action("my_action")
+ transaction.set_namespace("my_namespace")
+ end
end
- expect(last_transaction).to have_namespace("my_namespace")
- expect(last_transaction).to have_action("my_action")
- expect(last_transaction).to have_error("StandardError", "my_error")
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to have_namespace("my_namespace")
+ expect(last_transaction).to have_action("my_action")
+ expect(last_transaction).to have_error("StandardError", "my_error")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+
+ expect(root_span.name).to eq("my_action")
+ expect(root_span.attributes["appsignal.action_name"]).to eq("my_action")
+ expect(root_span.attributes["appsignal.namespace"]).to eq("my_namespace")
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("StandardError")
+ expect(event.attributes["exception.message"]).to eq("my_error")
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
it "yields and allows additional metadata to be set with global helpers" do
@@ -1719,11 +2053,18 @@ def on_start
let(:transaction) { http_request_transaction }
around { |example| keep_transactions { example.run } }
- context "when there is an active transaction" do
- before { set_current_transaction(transaction) }
-
- it "adds the error to the active transaction" do
+ describe "adding the error to the active transaction" do
+ # `set_current_transaction` (which builds the transaction's root span)
+ # happens in the body, not a `before`, so in collector mode it uses the
+ # in-memory provider that `start_collector_agent` swaps in.
+ def perform
+ set_current_transaction(transaction)
Appsignal.set_error(error)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
transaction._sample
expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
@@ -1731,6 +2072,24 @@ def on_start
expect(transaction).to_not include_tags
end
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("I am an exception")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
+ end
+
+ context "when there is an active transaction" do
+ before { set_current_transaction(transaction) }
+
context "when the error is not an Exception" do
let(:error) { Object.new }
@@ -1779,7 +2138,6 @@ def on_start
let(:err_stream) { std_stream }
let(:stderr) { err_stream.read }
let(:error) { ExampleException.new("error message") }
- before { start_agent }
around { |example| keep_transactions { example.run } }
context "when the error is not an Exception" do
@@ -1802,16 +2160,34 @@ def on_start
end
context "when there is no active transaction" do
- it "creates a new transaction" do
- expect do
+ describe "reporting the error" do
+ def perform
Appsignal.report_error(error)
- end.to(change { created_transactions.count }.by(1))
- end
+ end
- it "completes the transaction" do
- Appsignal.report_error(error)
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect { perform }.to(change { created_transactions.count }.by(1))
- expect(last_transaction).to be_completed
+ expect(last_transaction).to have_error("ExampleException", "error message")
+ expect(last_transaction).to be_completed
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ # With no active transaction, report_error creates and completes its
+ # own transaction, so the root span is exported.
+ perform
+
+ expect(root_span).not_to be_nil
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
end
context "when given a block" do
@@ -1849,15 +2225,82 @@ def on_start
context "when there is an active transaction" do
let(:transaction) { http_request_transaction }
- before { set_current_transaction(transaction) }
+ # Only for non-mode examples. Mode-tagged examples set the current
+ # transaction in their own body, after starting the agent (collector
+ # mode swaps in the in-memory providers there).
+ before do |example|
+ unless example.metadata[:agent_mode] || example.metadata[:collector_mode]
+ set_current_transaction(transaction)
+ end
+ end
- it "sets the error in the active transaction" do
- Appsignal.report_error(error)
+ describe "reporting the error onto it" do
+ def perform
+ set_current_transaction(transaction)
+ Appsignal.report_error(error)
+ end
- expect(last_transaction).to eq(transaction)
- transaction._sample
- expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
- expect(transaction).to have_error("ExampleException", "error message")
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+
+ expect(last_transaction).to eq(transaction)
+ transaction._sample
+ expect(transaction).to have_namespace(Appsignal::Transaction::HTTP_REQUEST)
+ expect(transaction).to have_error("ExampleException", "error message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ event = root_span.events.find { |e| e.name == "exception" }
+ expect(event).not_to be_nil
+ expect(event.attributes["exception.type"]).to eq("ExampleException")
+ expect(event.attributes["exception.message"]).to eq("error message")
+ expect(event.attributes["exception.stacktrace"]).to be_a(String)
+ expect(event.attributes["appsignal.alert_this_error"]).to eq(true)
+ expect(root_span.status.code).to eq(::OpenTelemetry::Trace::Status::ERROR)
+ end
+ end
+
+ describe "with multiple reported errors" do
+ let(:other_error) do
+ ExampleStandardError.new("other message").tap { |e| e.set_backtrace(["line 2"]) }
+ end
+
+ def perform
+ set_current_transaction(transaction)
+ Appsignal.report_error(error)
+ Appsignal.report_error(other_error)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ perform
+ # The extension holds one error per transaction, so the extra error
+ # is reported as a duplicate transaction.
+ expect { transaction.complete }.to(change { created_transactions.count }.by(1))
+
+ expect(created_transactions.map { |t| t.to_h["error"]["message"] })
+ .to contain_exactly("error message", "other message")
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ perform
+ transaction.complete
+
+ # One trace: a single root span carrying one exception event per error.
+ root_spans = span_exporter.finished_spans.select do |span|
+ [:server, :consumer].include?(span.kind)
+ end
+ expect(root_spans.size).to eq(1)
+ events = root_spans.first.events.select { |e| e.name == "exception" }
+ expect(events.map { |e| e.attributes["exception.message"] })
+ .to contain_exactly("error message", "other message")
+ end
end
context "when the active transaction already has an error" do
@@ -2025,61 +2468,368 @@ def on_start
end
end
end
+ end
- describe ".instrument" do
- it_behaves_like "instrument helper" do
- let(:instrumenter) { Appsignal }
- before { set_current_transaction(transaction) }
+ describe "custom metrics" do
+ let(:tags) { { :foo => "bar" } }
+
+ describe ".set_gauge" do
+ describe "with a string key and float value" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:set_gauge)
+ .with("key", 0.1, Appsignal::Extension.data_map_new)
+ Appsignal.set_gauge("key", 0.1)
+ end
+ end
+
+ describe "with tags" do
+ def perform
+ Appsignal.set_gauge("key", 0.1, tags)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:set_gauge)
+ .with("key", 0.1, Appsignal::Utils::Data.generate(tags))
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ allow(Appsignal::Metrics::OpenTelemetryBackend).to receive(:set_gauge)
+ expect(Appsignal::Extension).not_to receive(:set_gauge)
+ perform
+ expect(Appsignal::Metrics::OpenTelemetryBackend).to have_received(:set_gauge)
+ .with("key", 0.1, tags)
+ end
+ end
+
+ describe "with a symbol key and int value" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:set_gauge)
+ .with("key", 1.0, Appsignal::Extension.data_map_new)
+ Appsignal.set_gauge(:key, 1)
+ end
+ end
+
+ describe "when the value is out of range" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:set_gauge).with(
+ "key",
+ 10,
+ Appsignal::Extension.data_map_new
+ ).and_raise(RangeError)
+ expect(Appsignal.internal_logger).to receive(:warn)
+ .with("The gauge value '10' for metric 'key' is too big")
+
+ Appsignal.set_gauge("key", 10)
+ end
end
end
- describe ".instrument_sql" do
- around { |example| keep_transactions { example.run } }
- before { set_current_transaction(transaction) }
+ describe ".increment_counter" do
+ describe "with a string key" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:increment_counter)
+ .with("key", 1, Appsignal::Extension.data_map_new)
+ Appsignal.increment_counter("key")
+ end
+ end
- it "creates an SQL event on the transaction" do
- result =
- Appsignal.instrument_sql "name", "title", "body" do
- "return value"
- end
+ describe "with tags" do
+ def perform
+ Appsignal.increment_counter("key", 5, tags)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:increment_counter)
+ .with("key", 5, Appsignal::Utils::Data.generate(tags))
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ allow(Appsignal::Metrics::OpenTelemetryBackend).to receive(:increment_counter)
+ expect(Appsignal::Extension).not_to receive(:increment_counter)
+ perform
+ expect(Appsignal::Metrics::OpenTelemetryBackend).to have_received(:increment_counter)
+ .with("key", 5, tags)
+ end
+ end
+
+ describe "with a symbol key" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:increment_counter)
+ .with("key", 1, Appsignal::Extension.data_map_new)
+ Appsignal.increment_counter(:key)
+ end
+ end
+
+ describe "with a count" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:increment_counter)
+ .with("key", 5, Appsignal::Extension.data_map_new)
+ Appsignal.increment_counter("key", 5)
+ end
+ end
+
+ describe "when the value is out of range" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:increment_counter)
+ .with("key", 10, Appsignal::Extension.data_map_new).and_raise(RangeError)
+ expect(Appsignal.internal_logger).to receive(:warn)
+ .with("The counter value '10' for metric 'key' is too big")
+
+ Appsignal.increment_counter("key", 10)
+ end
+ end
+ end
+
+ describe ".add_distribution_value" do
+ describe "with a string key and float value" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:add_distribution_value)
+ .with("key", 0.1, Appsignal::Extension.data_map_new)
+ Appsignal.add_distribution_value("key", 0.1)
+ end
+ end
+
+ describe "with tags" do
+ def perform
+ Appsignal.add_distribution_value("key", 0.1, tags)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:add_distribution_value)
+ .with("key", 0.1, Appsignal::Utils::Data.generate(tags))
+ perform
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ allow(Appsignal::Metrics::OpenTelemetryBackend).to receive(:add_distribution_value)
+ expect(Appsignal::Extension).not_to receive(:add_distribution_value)
+ perform
+ expect(Appsignal::Metrics::OpenTelemetryBackend).to have_received(:add_distribution_value)
+ .with("key", 0.1, tags)
+ end
+ end
+
+ describe "with a symbol key and int value" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:add_distribution_value)
+ .with("key", 1.0, Appsignal::Extension.data_map_new)
+ Appsignal.add_distribution_value(:key, 1)
+ end
+ end
- expect(result).to eq "return value"
+ describe "when the value is out of range" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ expect(Appsignal::Extension).to receive(:add_distribution_value)
+ .with("key", 10, Appsignal::Extension.data_map_new).and_raise(RangeError)
+ expect(Appsignal.internal_logger).to receive(:warn)
+ .with("The distribution value '10' for metric 'key' is too big")
+
+ Appsignal.add_distribution_value("key", 10)
+ end
+ end
+ end
+ end
+
+ describe ".instrument" do
+ describe "block return value" do
+ it_in_both_modes do
+ set_current_transaction(transaction)
+
+ result = Appsignal.instrument("name", "title", "body") { "return value" }
+
+ expect(result).to eq("return value")
+ end
+ end
+
+ describe "recording an event around the block" do
+ def perform
+ Appsignal.instrument("name", "title", "body") { :do_nothing }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
expect(transaction).to include_event(
"name" => "name",
"title" => "title",
"body" => "body",
- "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT
+ "body_format" => Appsignal::EventFormatter::DEFAULT
)
end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("title")
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.category"]).to eq("name")
+ expect(span.attributes["appsignal.body"]).to eq("body")
+ expect(span.attributes).not_to have_key("db.query.text")
+ expect(span.attributes).not_to have_key("db.system.name")
+ end
end
- describe ".ignore_instrumentation_events" do
- around { |example| keep_transactions { example.run } }
- let(:transaction) { http_request_transaction }
+ describe "when an error is raised in the block" do
+ def perform
+ expect do
+ Appsignal.instrument("name", "title", "body") { raise ExampleException, "foo" }
+ end.to raise_error(ExampleException, "foo")
+ end
- context "with current transaction" do
- before { set_current_transaction(transaction) }
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+ expect(transaction).to include_event(
+ "name" => "name", "title" => "title", "body" => "body"
+ )
+ end
- it "does not record events on the transaction" do
- expect(transaction).to receive(:pause!).and_call_original
- expect(transaction).to receive(:resume!).and_call_original
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
- Appsignal.instrument("register.this.event") { :do_nothing }
- Appsignal.ignore_instrumentation_events do
- Appsignal.instrument("dont.register.this.event") { :do_nothing }
- end
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("title")
+ expect(span.attributes["appsignal.category"]).to eq("name")
+ expect(span.attributes["appsignal.body"]).to eq("body")
+ end
+ end
- expect(transaction).to include_event("name" => "register.this.event")
- expect(transaction).to_not include_event("name" => "dont.register.this.event")
+ describe "when a symbol is thrown in the block" do
+ def perform
+ expect do
+ Appsignal.instrument("name", "title", "body") { throw :foo }
+ end.to throw_symbol(:foo)
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ perform
+ expect(transaction).to include_event(
+ "name" => "name", "title" => "title", "body" => "body"
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+ perform
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("title")
+ expect(span.attributes["appsignal.category"]).to eq("name")
+ expect(span.attributes["appsignal.body"]).to eq("body")
+ end
+ end
+ end
+
+ describe ".instrument_sql" do
+ describe "recording a SQL event around the block" do
+ def perform
+ Appsignal.instrument_sql("name", "title", "body") { "return value" }
+ end
+
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+
+ expect(perform).to eq("return value")
+ expect(transaction).to include_event(
+ "name" => "name",
+ "title" => "title",
+ "body" => "body",
+ "body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT
+ )
+ end
+
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
+
+ expect(perform).to eq("return value")
+ Appsignal::Transaction.complete_current!
+
+ expect(event_spans.size).to eq(1)
+ span = event_spans.first
+ expect(span.name).to eq("title")
+ expect(span.parent_span_id).to eq(root_span.span_id)
+ expect(span.attributes["appsignal.category"]).to eq("name")
+ expect(span.attributes["db.query.text"]).to eq("body")
+ expect(span.attributes["db.system.name"]).to eq("other_sql")
+ expect(span.attributes).not_to have_key("appsignal.body")
+ end
+ end
+ end
+
+ describe ".ignore_instrumentation_events" do
+ describe "with a current transaction" do
+ it "in agent mode", :agent_mode do
+ start_agent
+ set_current_transaction(transaction)
+ expect(transaction).to receive(:pause!).and_call_original
+ expect(transaction).to receive(:resume!).and_call_original
+
+ Appsignal.instrument("register.this.event") { :do_nothing }
+ Appsignal.ignore_instrumentation_events do
+ Appsignal.instrument("dont.register.this.event") { :do_nothing }
end
+
+ expect(transaction).to include_event("name" => "register.this.event")
+ expect(transaction).to_not include_event("name" => "dont.register.this.event")
end
- context "without current transaction" do
- let(:transaction) { nil }
+ it "in collector mode", :collector_mode do
+ start_collector_agent
+ set_current_transaction(transaction)
- it "does not crash" do
- Appsignal.ignore_instrumentation_events { :do_nothing }
+ Appsignal.instrument("register.this.event") { :do_nothing }
+ Appsignal.ignore_instrumentation_events do
+ Appsignal.instrument("dont.register.this.event") { :do_nothing }
end
+ Appsignal::Transaction.complete_current!
+
+ names = event_spans.map(&:name)
+ expect(names).to include("register.this.event")
+ expect(names).not_to include("dont.register.this.event")
+ end
+ end
+
+ describe "without a current transaction" do
+ it_in_both_modes do
+ expect do
+ Appsignal.ignore_instrumentation_events { :do_nothing }
+ end.not_to raise_error
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index d4708cadf..b3ebcbcae 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -29,6 +29,9 @@
Dir[File.join(APPSIGNAL_SPEC_DIR, "support/shared_examples", "*.rb")].sort.each do |f|
require f
end
+Dir[File.join(APPSIGNAL_SPEC_DIR, "support/shared_contexts", "*.rb")].sort.each do |f|
+ require f
+end
if DependencyHelper.rails_present?
require File.join(ConfigHelpers.rails_project_fixture_path, "config/application.rb")
end
@@ -78,7 +81,11 @@ def clear_subscribers
config.exclude_pattern = "spec/integration/diagnose/**/*_spec.rb"
config.filter_run_excluding(
:extension_installation_failure => true,
- :jruby => !DependencyHelper.running_jruby?
+ :jruby => !DependencyHelper.running_jruby?,
+ # The OpenTelemetry gems are optional. When they're not installed (e.g. on
+ # Ruby 2.7, or any non-`-collector` gemfile), skip every collector-mode
+ # example rather than crashing on missing constants.
+ :collector_mode => !DependencyHelper.opentelemetry_present?
)
config.mock_with :rspec do |mocks|
mocks.syntax = :expect
@@ -92,7 +99,23 @@ def spec_system_tmp_dir
end
config.before :suite do
- WebMock.disable_net_connect!
+ # The OTLP mock server is only needed by collector-mode specs, which only
+ # run when the OpenTelemetry gems are installed. Always disable real network
+ # connections; when those specs run, relax the rule just enough to reach the
+ # mock server bound by `OTLPCollectorServer`.
+ if DependencyHelper.opentelemetry_present?
+ # Boot first: the server binds an OS-assigned port, so its address is only
+ # known afterwards.
+ OTLPCollectorServer.boot!
+ WebMock.disable_net_connect!(:allow => "127.0.0.1:#{OTLPCollectorServer.port}")
+ else
+ WebMock.disable_net_connect!
+ end
+ end
+
+ config.after do
+ OTLPCollectorServer.clear if defined?(OTLPCollectorServer)
+ Appsignal::OpenTelemetry.reset!
end
config.before :context do
diff --git a/spec/support/helpers/dependency_helper.rb b/spec/support/helpers/dependency_helper.rb
index 52ddc968e..a30fb3ee4 100644
--- a/spec/support/helpers/dependency_helper.rb
+++ b/spec/support/helpers/dependency_helper.rb
@@ -25,6 +25,24 @@ def running_jruby?
Appsignal::System.jruby?
end
+ # Whether the optional OpenTelemetry gems collector mode needs are
+ # installable in this bundle. They're no longer gemspec dependencies, so
+ # the OTel specs only run under the `-collector` gemfiles (Ruby 3.1+). This
+ # actually requires the gems (idempotent) so the guarded specs can use them.
+ def opentelemetry_present?
+ return @opentelemetry_present if defined?(@opentelemetry_present)
+
+ @opentelemetry_present =
+ begin
+ require "opentelemetry/sdk"
+ require "opentelemetry-metrics-sdk"
+ require "opentelemetry-logs-sdk"
+ true
+ rescue LoadError
+ false
+ end
+ end
+
def rails_present?
dependency_present? "rails"
end
diff --git a/spec/support/helpers/mode_helpers.rb b/spec/support/helpers/mode_helpers.rb
new file mode 100644
index 000000000..837b27856
--- /dev/null
+++ b/spec/support/helpers/mode_helpers.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module ModeHelpers
+ # Defines the same example in both agent and collector mode. Pass an optional
+ # description; it is suffixed with " in agent mode" / " in collector mode".
+ #
+ # Per the dual-mode start principle, each generated example starts its own
+ # agent in the body: agent mode via `start_agent`, collector mode via
+ # `start_collector_agent`, before running the shared block. So the block must
+ # NOT start the agent itself, and any mode-dependent arrangement (e.g.
+ # `set_current_transaction`, building a transaction) belongs inside the block
+ # — which runs after the start — rather than in a `before` hook.
+ def it_in_both_modes(description = nil, &block)
+ it([description, "in agent mode"].compact.join(" "), :agent_mode) do
+ start_agent(**(defined?(start_agent_args) ? start_agent_args : {}))
+ instance_exec(&block)
+ end
+ it([description, "in collector mode"].compact.join(" "), :collector_mode) do
+ start_collector_agent
+ instance_exec(&block)
+ end
+ end
+end
+
+RSpec.configure { |config| config.extend ModeHelpers }
diff --git a/spec/support/helpers/otlp_collector_server.rb b/spec/support/helpers/otlp_collector_server.rb
new file mode 100644
index 000000000..db4ef05ff
--- /dev/null
+++ b/spec/support/helpers/otlp_collector_server.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require "socket"
+require "stringio"
+require "timeout"
+require "zlib"
+
+# A mock OTLP/HTTP collector used by the collector-mode integration spec.
+#
+# Accepts POSTs to `/v1/traces`, `/v1/metrics` and `/v1/logs` and stores the
+# raw protobuf-encoded request body in a per-path Queue. Tests call
+# `OTLPCollectorServer.listen_to("/v1/traces")` to block until a request
+# arrives and then decode it with the proto stubs that ship inside the
+# `opentelemetry-exporter-otlp` gem.
+#
+# Hand-rolled on top of `TCPServer` rather than Sinatra/WEBrick so the spec
+# suite doesn't drag those gems into every framework gemfile via the gemspec.
+module OTLPCollectorServer
+ PATHS = %w[/v1/traces /v1/metrics /v1/logs].freeze
+
+ @received = Hash.new { |h, k| h[k] = Queue.new }
+ @booted = false
+ @port = nil
+
+ class << self
+ attr_reader :received
+
+ # The port the mock server is bound to. Assigned by `boot!`, which binds to
+ # an OS-assigned free port rather than a fixed one, so concurrent suite runs
+ # on the same machine don't collide. `nil` until booted.
+ attr_reader :port
+
+ def endpoint
+ "http://127.0.0.1:#{port}"
+ end
+
+ # Env vars that put a spawned runner into collector mode, pointed at this
+ # mock server. Returns a plain Hash so callers can merge in other env
+ # vars, e.g. `OTLPCollectorServer.env.merge("OTEL_..." => "...")`.
+ def env
+ { "APPSIGNAL_COLLECTOR_ENDPOINT" => endpoint }
+ end
+
+ def listen_to(path, timeout: 10)
+ Timeout.timeout(timeout) { received[path].pop }
+ rescue Timeout::Error
+ raise "Timed out after #{timeout}s waiting for OTLP request to #{path}. " \
+ "Other received paths so far: " \
+ "#{received.transform_values(&:size).reject { |_, s| s.zero? }.inspect}"
+ end
+
+ def clear
+ received.each_value(&:clear)
+ end
+
+ def boot!
+ return if @booted
+
+ # Port 0 lets the OS pick a free port; read the assigned one back so
+ # `endpoint`/`env` can hand it to the spawned runners.
+ @server = TCPServer.new("127.0.0.1", 0)
+ @port = @server.addr[1]
+ @booted = true
+ @thread = Thread.new do
+ Thread.current.abort_on_exception = false
+ accept_loop
+ end
+ end
+
+ private
+
+ def accept_loop
+ loop do
+ client = @server.accept
+ Thread.new(client) { |c| handle(c) }
+ end
+ rescue IOError, Errno::EBADF
+ # Server socket was closed; exit the loop.
+ end
+
+ def handle(client)
+ request_line = client.gets
+ return unless request_line
+
+ method, path, _ = request_line.strip.split(" ", 3)
+ headers = read_headers(client)
+
+ length = headers["content-length"].to_i
+ raw_body = length.positive? ? client.read(length) : ""
+ body =
+ if headers["content-encoding"] == "gzip"
+ Zlib::GzipReader.new(StringIO.new(raw_body)).read
+ else
+ raw_body
+ end
+
+ if method == "POST" && PATHS.include?(path)
+ received[path] << { :headers => rack_style_headers(headers), :body => body }
+ write_response(client, 200, "application/x-protobuf", "")
+ else
+ write_response(client, 404, "text/plain", "")
+ end
+ rescue StandardError
+ # Swallow per-connection errors so a malformed request doesn't bring
+ # down the accept loop for the rest of the suite.
+ ensure
+ begin
+ client&.close
+ rescue StandardError
+ # ignore
+ end
+ end
+
+ def read_headers(client)
+ headers = {}
+ while (line = client.gets) && line != "\r\n"
+ key, _, value = line.strip.partition(":")
+ headers[key.downcase] = value.strip
+ end
+ headers
+ end
+
+ # Mimic the rack env header keys the previous Sinatra-based server
+ # exposed so any future spec that introspects `:headers` finds the
+ # same shape.
+ def rack_style_headers(headers)
+ headers.each_with_object({}) do |(k, v), h|
+ env_key =
+ if k == "content-type"
+ "CONTENT_TYPE"
+ else
+ "HTTP_#{k.upcase.tr("-", "_")}"
+ end
+ h[env_key] = v
+ end
+ end
+
+ def write_response(client, status, content_type, body)
+ reason = status == 200 ? "OK" : "Not Found"
+ client.write("HTTP/1.1 #{status} #{reason}\r\n")
+ client.write("Content-Type: #{content_type}\r\n")
+ client.write("Content-Length: #{body.bytesize}\r\n")
+ client.write("Connection: close\r\n")
+ client.write("\r\n")
+ client.write(body)
+ end
+ end
+end
diff --git a/spec/support/helpers/transaction_helpers.rb b/spec/support/helpers/transaction_helpers.rb
index 79191d2db..28098c052 100644
--- a/spec/support/helpers/transaction_helpers.rb
+++ b/spec/support/helpers/transaction_helpers.rb
@@ -33,8 +33,8 @@ def create_transaction(namespace = default_namespace)
Appsignal::Transaction.create(namespace)
end
- def new_transaction(namespace = default_namespace, ext: nil)
- Appsignal::Transaction.new(namespace, :ext => ext)
+ def new_transaction(namespace = default_namespace, backend: nil)
+ Appsignal::Transaction.new(namespace, :backend => backend)
end
def rack_request(env)
diff --git a/spec/support/matchers/transaction.rb b/spec/support/matchers/transaction.rb
index ce1ba959c..ec02c1fba 100644
--- a/spec/support/matchers/transaction.rb
+++ b/spec/support/matchers/transaction.rb
@@ -63,7 +63,7 @@ def define_transaction_sample_matcher_for(matcher_key, value_key = matcher_key)
RSpec::Matchers.define :be_completed do
match(:notify_expectation_failures => true) do |transaction|
- values_match? transaction.ext._completed?, true
+ values_match? transaction.backend._completed?, true
end
end
@@ -163,7 +163,8 @@ def format_event(event)
breadcrumb = format_breadcrumb(action, category, message, metadata, time)
expect(breadcrumbs).to_not include(breadcrumb)
else
- expect(breadcrumbs).to_not be_any
+ # No breadcrumbs added means no breadcrumbs sample data at all (nil).
+ expect(breadcrumbs || []).to_not be_any
end
end
@@ -181,7 +182,7 @@ def format_breadcrumb(action, category, message, metadata, time)
RSpec::Matchers.define :have_queue_start do |queue_start_time|
match(:notify_expectation_failures => true) do |transaction|
- actual_start = transaction.ext.queue_start
+ actual_start = transaction.backend.queue_start
if queue_start_time
expect(actual_start).to eq(queue_start_time)
else
@@ -190,7 +191,7 @@ def format_breadcrumb(action, category, message, metadata, time)
end
match_when_negated(:notify_expectation_failures => true) do |transaction|
- actual_start = transaction.ext.queue_start
+ actual_start = transaction.backend.queue_start
if queue_start_time
expect(actual_start).to_not eq(queue_start_time)
else
diff --git a/spec/support/shared_contexts/agent_mode.rb b/spec/support/shared_contexts/agent_mode.rb
new file mode 100644
index 000000000..10d412d88
--- /dev/null
+++ b/spec/support/shared_contexts/agent_mode.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+RSpec.shared_context "agent mode", :agent_mode do
+ # Dual-mode start principle (see also collector_mode.rb): mode is global
+ # state, so the agent is NOT started in a `before` here -- that fought with
+ # ad-hoc `start_agent` calls elsewhere (last writer wins, order is fragile).
+ # Each `:agent_mode` example starts the agent itself in its body with
+ # `start_agent` (the `it_in_both_modes` helper does this for its shared body).
+ # This context just makes completed transactions readable via `to_h` so the
+ # agent-mode matchers can assert on `include_event` / `include_tags` etc.
+ # after the transaction has been completed. Harmless when the example doesn't
+ # complete the transaction inside the body -- it just sets and unsets a flag.
+ around { |example| keep_transactions { example.run } }
+end
+
+RSpec.configure do |config|
+ config.include_context "agent mode", :agent_mode
+end
diff --git a/spec/support/shared_contexts/collector_mode.rb b/spec/support/shared_contexts/collector_mode.rb
new file mode 100644
index 000000000..a5a796e7c
--- /dev/null
+++ b/spec/support/shared_contexts/collector_mode.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+# The OpenTelemetry gems are optional (not gemspec dependencies), so only
+# require them when present. This shared context is auto-loaded for every run,
+# but its OTel references live in lazy `let`/`before`/`after` blocks that only
+# run for `:collector_mode`-tagged examples — and those specs are themselves
+# guarded on `opentelemetry_present?`, so they don't load without the gems.
+if DependencyHelper.opentelemetry_present?
+ require "opentelemetry/sdk"
+ require "opentelemetry-metrics-sdk"
+ require "opentelemetry-logs-sdk"
+end
+
+RSpec.shared_context "collector mode", :collector_mode do
+ let(:span_exporter) { ::OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new }
+ let(:tracer_provider) do
+ provider = ::OpenTelemetry::SDK::Trace::TracerProvider.new
+ provider.add_span_processor(
+ ::OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(span_exporter)
+ )
+ provider
+ end
+
+ let(:metric_exporter) { ::OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new }
+ let(:meter_provider) do
+ provider = ::OpenTelemetry::SDK::Metrics::MeterProvider.new
+ provider.add_metric_reader(metric_exporter)
+ provider
+ end
+
+ let(:log_exporter) { ::OpenTelemetry::SDK::Logs::Export::InMemoryLogRecordExporter.new }
+ let(:logger_provider) do
+ provider = ::OpenTelemetry::SDK::Logs::LoggerProvider.new
+ provider.add_log_record_processor(
+ ::OpenTelemetry::SDK::Logs::Export::SimpleLogRecordProcessor.new(log_exporter)
+ )
+ provider
+ end
+
+ # Dual-mode start principle: mode is global state, so the agent is NOT
+ # started in a `before` here -- that fought with ad-hoc `start_agent` calls
+ # with fragile ordering. Each `:collector_mode` example calls
+ # `start_collector_agent` itself in its body (the `it_in_both_modes` helper
+ # does this for its shared body). This context provides the in-memory
+ # providers, the `start_collector_agent` helper, the read-back helpers, and
+ # the teardown below.
+ after do
+ # `clear_current_transaction!` in spec_helper clears the thread-local but
+ # not the attached OTel context. `complete_current!` does both.
+ Appsignal::Transaction.complete_current!
+ # Shut down whatever OTel SDK is current at teardown. Usually that's
+ # the threadless in-memory providers (a near no-op), but examples that
+ # boot AppSignal again themselves leave real providers behind, whose
+ # background threads would otherwise accumulate across the suite. The
+ # targeted shutdown, not `Appsignal.stop`: stop's `Extension.stop`
+ # takes ~2 seconds per call, which across every collector-mode example
+ # adds minutes to the suite. Runs before the global
+ # `Appsignal::OpenTelemetry.reset!` hook, so the `started?` gate inside
+ # the shutdown still passes.
+ Appsignal::OpenTelemetry.shutdown
+ # Booting the SDK installs the global W3C propagator as a side effect, and
+ # nothing ever resets it. Left in place it leaks to every later example, so
+ # an unrelated spec can silently pass on a propagator this example happened
+ # to install. Reset it to the API default so collector-mode examples can't
+ # leak trace propagation into the rest of the suite.
+ ::OpenTelemetry.propagation =
+ ::OpenTelemetry::Context::Propagation::NoopTextMapPropagator.new
+ end
+
+ # Boots the agent in collector mode and swaps in the in-memory OTel providers.
+ # Called explicitly from each collector-mode example body.
+ #
+ # Examples can define a `start_agent_args` `let` to pass `:env`/`:options`; the
+ # `collector_endpoint` is always merged into the options so collector mode
+ # stays enabled. Guarded with `defined?` rather than a default `let`, because
+ # an included shared context's `let` would take precedence over the example
+ # group's own `let` override.
+ def start_collector_agent
+ args = (defined?(start_agent_args) ? start_agent_args : {}).dup
+ args[:options] = { :collector_endpoint => OTLPCollectorServer.endpoint }
+ .merge(args[:options] || {})
+ start_agent(**args)
+ # `Appsignal.start` booted a full OTel SDK whose providers each carry a
+ # background export thread (batch span and log processors, periodic
+ # metric reader). Shut it down before the swaps below: after the swap
+ # the booted providers are unreachable and their threads would leak
+ # across examples.
+ Appsignal::OpenTelemetry.shutdown
+ # Swap in the in-memory providers so the test can read spans/metrics/
+ # logs back, and reset the metrics/logger backends so their cached
+ # meter/logger re-resolve against these providers on the next emit.
+ ::OpenTelemetry.tracer_provider = tracer_provider
+ ::OpenTelemetry.meter_provider = meter_provider
+ ::OpenTelemetry.logger_provider = logger_provider
+ Appsignal::Metrics::OpenTelemetryBackend.reset!
+ Appsignal::Logger::OpenTelemetryBackend.reset!
+ end
+
+ def root_span
+ span_exporter.finished_spans.find { |s| [:server, :consumer].include?(s.kind) }
+ end
+
+ def event_spans
+ span_exporter.finished_spans.reject { |s| [:server, :consumer].include?(s.kind) }
+ end
+
+ # The OpenTelemetry `exception` events recorded across all finished spans
+ # (errors attach to the span that was current when they were set, which may
+ # be the root span or an event span).
+ def exception_events
+ span_exporter.finished_spans.flat_map { |span| Array(span.events) }.select do |event|
+ event.name == "exception"
+ end
+ end
+
+ # Pull the current metric snapshots from the in-memory reader. The OTLP
+ # exporter is also a reader, so a `pull` collects everything recorded so far.
+ def metric_snapshots
+ metric_exporter.pull
+ snapshots = metric_exporter.metric_snapshots.dup
+ metric_exporter.reset
+ snapshots
+ end
+
+ def metric_snapshot(name)
+ metric_snapshots.find { |snapshot| snapshot.name == name }
+ end
+
+ def log_records
+ log_exporter.emitted_log_records
+ end
+end
+
+RSpec.configure do |config|
+ config.include_context "collector mode", :collector_mode
+end
diff --git a/spec/support/testing.rb b/spec/support/testing.rb
index 8a3899998..1ad9dd589 100644
--- a/spec/support/testing.rb
+++ b/spec/support/testing.rb
@@ -174,7 +174,7 @@ module AppsignalTest
module Transaction
module ClassMethods
def self.extended(base)
- base.attr_reader :ext, :error_blocks
+ base.attr_reader :backend, :error_blocks
end
# Override the {Appsignal::Transaction.new} method so we can track which
@@ -194,7 +194,32 @@ def _sample
end
end
end
+
+ # Test-only introspection for the transaction backends. These let matchers and
+ # specs read back internal state; they are not part of the production backend
+ # contract (see `Appsignal::Transaction::BaseBackend`).
+ module ExtensionBackend
+ def queue_start
+ @handle.queue_start
+ end
+
+ def _completed?
+ @handle._completed?
+ end
+ end
+
+ module OpenTelemetryBackend
+ def queue_start
+ nil
+ end
+
+ def _completed?
+ @completed
+ end
+ end
end
Appsignal::Transaction.extend(AppsignalTest::Transaction::ClassMethods)
Appsignal::Transaction.prepend(AppsignalTest::Transaction::InstanceMethods)
+Appsignal::Transaction::ExtensionBackend.prepend(AppsignalTest::ExtensionBackend)
+Appsignal::Transaction::OpenTelemetryBackend.prepend(AppsignalTest::OpenTelemetryBackend)