diff --git a/.circleci/config.yml b/.circleci/config.yml index 1f71b3be24..38ab900475 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,16 +6,28 @@ job_defaults: &job_defaults ruby_containers: &ruby_containers - &container-1_9 image: datadog/docker-library:ddtrace_rb_1_9_3 + environment: + - BUNDLE_GEMFILE=/app/Gemfile - &container-2_0 image: datadog/docker-library:ddtrace_rb_2_0_0 + environment: + - BUNDLE_GEMFILE=/app/Gemfile - &container-2_1 image: datadog/docker-library:ddtrace_rb_2_1_10 + environment: + - BUNDLE_GEMFILE=/app/Gemfile - &container-2_2 image: datadog/docker-library:ddtrace_rb_2_2_10 + environment: + - BUNDLE_GEMFILE=/app/Gemfile - &container-2_3 image: datadog/docker-library:ddtrace_rb_2_3_7 + environment: + - BUNDLE_GEMFILE=/app/Gemfile - &container-2_4 image: datadog/docker-library:ddtrace_rb_2_4_4 + environment: + - BUNDLE_GEMFILE=/app/Gemfile test_containers: &test_containers - &container_postgres diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..4301731fd5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +gemfiles/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index c4f4022364..0942ba6080 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,7 @@ build-iPhoneSimulator/ # lock files Gemfile.lock -gemfiles/*.lock +gemfiles/* # bundle config gemfiles/.bundle diff --git a/Appraisals b/Appraisals index 787f3d0c9e..e399b595bf 100644 --- a/Appraisals +++ b/Appraisals @@ -1,6 +1,7 @@ -if RUBY_VERSION < '1.9.3' +if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('1.9.3') raise NotImplementedError, 'Ruby versions < 1.9.3 are not supported!' -elsif '1.9.3' <= RUBY_VERSION && RUBY_VERSION < '2.0.0' +elsif Gem::Version.new('1.9.3') <= Gem::Version.new(RUBY_VERSION) \ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') if RUBY_PLATFORM != 'java' appraise 'rails30-postgres' do gem 'test-unit' @@ -56,26 +57,31 @@ elsif '1.9.3' <= RUBY_VERSION && RUBY_VERSION < '2.0.0' end appraise 'contrib-old' do + gem 'active_model_serializers', '~> 0.9.0' + gem 'activerecord', '3.2.22.5' + gem 'activerecord-mysql-adapter', platform: :ruby + gem 'aws-sdk', '~> 2.0' + gem 'dalli' gem 'elasticsearch-transport' - gem 'mongo', '< 2.5' - gem 'redis', '< 4.0' + gem 'excon' gem 'hiredis' + gem 'mongo', '< 2.5' + gem 'mysql2', '0.3.21', platform: :ruby gem 'rack', '1.4.7' - gem 'rack-test', '0.7.0' gem 'rack-cache', '1.7.1' + gem 'rack-test', '0.7.0' + gem 'rake', '< 12.3' + gem 'redis', '< 4.0' + gem 'resque', '< 2.0' + gem 'sequel', '~> 4.0', '< 4.37' + gem 'sidekiq', '4.0.0' gem 'sinatra', '1.4.5' gem 'sqlite3' - gem 'activerecord', '3.2.22.5' - gem 'sidekiq', '4.0.0' - gem 'aws-sdk', '~> 2.0' gem 'sucker_punch' - gem 'dalli' - gem 'resque', '< 2.0' - gem 'mysql2', '0.3.21', platform: :ruby - gem 'activerecord-mysql-adapter', platform: :ruby end end -elsif '2.0.0' <= RUBY_VERSION && RUBY_VERSION < '2.1.0' +elsif Gem::Version.new('2.0.0') <= Gem::Version.new(RUBY_VERSION) \ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.1.0') if RUBY_PLATFORM != 'java' appraise 'rails30-postgres' do gem 'test-unit' @@ -131,26 +137,31 @@ elsif '2.0.0' <= RUBY_VERSION && RUBY_VERSION < '2.1.0' end appraise 'contrib-old' do + gem 'active_model_serializers', '~> 0.9.0' + gem 'activerecord', '3.2.22.5' + gem 'activerecord-mysql-adapter', platform: :ruby + gem 'aws-sdk', '~> 2.0' + gem 'dalli' gem 'elasticsearch-transport' - gem 'mongo', '< 2.5' - gem 'redis', '< 4.0' + gem 'excon' gem 'hiredis' + gem 'mongo', '< 2.5' + gem 'mysql2', '0.3.21', platform: :ruby gem 'rack', '1.4.7' - gem 'rack-test', '0.7.0' gem 'rack-cache', '1.7.1' + gem 'rack-test', '0.7.0' + gem 'rake', '< 12.3' + gem 'redis', '< 4.0' + gem 'resque', '< 2.0' + gem 'sequel', '~> 4.0', '< 4.37' + gem 'sidekiq', '4.0.0' gem 'sinatra', '1.4.5' gem 'sqlite3' - gem 'activerecord', '3.2.22.5' - gem 'sidekiq', '4.0.0' - gem 'aws-sdk', '~> 2.0' gem 'sucker_punch' - gem 'dalli' - gem 'resque', '< 2.0' - gem 'mysql2', '0.3.21', platform: :ruby - gem 'activerecord-mysql-adapter', platform: :ruby end end -elsif '2.1.0' <= RUBY_VERSION && RUBY_VERSION < '2.2.0' +elsif Gem::Version.new('2.1.0') <= Gem::Version.new(RUBY_VERSION) \ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0') if RUBY_PLATFORM != 'java' appraise 'rails30-postgres' do gem 'test-unit' @@ -226,26 +237,31 @@ elsif '2.1.0' <= RUBY_VERSION && RUBY_VERSION < '2.2.0' end appraise 'contrib-old' do + gem 'active_model_serializers', '~> 0.9.0' + gem 'activerecord', '3.2.22.5' + gem 'activerecord-mysql-adapter', platform: :ruby + gem 'aws-sdk', '~> 2.0' + gem 'dalli' gem 'elasticsearch-transport' - gem 'mongo', '< 2.5' - gem 'redis', '< 4.0' + gem 'excon' gem 'hiredis' + gem 'mongo', '< 2.5' + gem 'mysql2', '0.3.21', platform: :ruby gem 'rack', '1.4.7' - gem 'rack-test', '0.7.0' gem 'rack-cache', '1.7.1' + gem 'rack-test', '0.7.0' + gem 'rake', '< 12.3' + gem 'redis', '< 4.0' + gem 'resque', '< 2.0' + gem 'sequel', '~> 4.0', '< 4.37' + gem 'sidekiq', '4.0.0' gem 'sinatra', '1.4.5' gem 'sqlite3' - gem 'activerecord', '3.2.22.5' - gem 'sidekiq', '4.0.0' - gem 'aws-sdk', '~> 2.0' gem 'sucker_punch' - gem 'dalli' - gem 'resque', '< 2.0' - gem 'mysql2', '0.3.21', platform: :ruby - gem 'activerecord-mysql-adapter', platform: :ruby end end -elsif '2.2.0' <= RUBY_VERSION && RUBY_VERSION < '2.3.0' +elsif Gem::Version.new('2.2.0') <= Gem::Version.new(RUBY_VERSION) \ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.0') if RUBY_PLATFORM != 'java' appraise 'rails30-postgres' do gem 'test-unit' @@ -353,27 +369,33 @@ elsif '2.2.0' <= RUBY_VERSION && RUBY_VERSION < '2.3.0' end appraise 'contrib' do + gem 'active_model_serializers', '>= 0.10.0' + gem 'activerecord', '< 5.1.5' + gem 'aws-sdk' + gem 'dalli' gem 'elasticsearch-transport' - gem 'mongo', '< 2.5' - gem 'graphql' + gem 'excon' gem 'grape' + gem 'graphql' + gem 'grpc' + gem 'hiredis' + gem 'mongo', '< 2.5' + gem 'mysql2', '< 0.5', platform: :ruby + gem 'racecar', '>= 0.3.5' gem 'rack' gem 'rack-test' + gem 'rake', '>= 12.3' gem 'redis', '< 4.0' - gem 'hiredis' + gem 'resque', '< 2.0' + gem 'sequel' + gem 'sidekiq' gem 'sinatra' gem 'sqlite3' - gem 'activerecord', '< 5.1.5' - gem 'sidekiq' - gem 'aws-sdk' gem 'sucker_punch' - gem 'dalli' - gem 'resque', '< 2.0' - gem 'racecar', '>= 0.3.5' - gem 'mysql2', '< 0.5', platform: :ruby end end -elsif '2.3.0' <= RUBY_VERSION && RUBY_VERSION < '2.4.0' +elsif Gem::Version.new('2.3.0') <= Gem::Version.new(RUBY_VERSION) \ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.4.0') if RUBY_PLATFORM != 'java' appraise 'rails30-postgres' do gem 'test-unit' @@ -481,47 +503,57 @@ elsif '2.3.0' <= RUBY_VERSION && RUBY_VERSION < '2.4.0' end appraise 'contrib' do + gem 'active_model_serializers', '>= 0.10.0' + gem 'activerecord', '< 5.1.5' + gem 'aws-sdk' + gem 'dalli' gem 'elasticsearch-transport' - gem 'mongo', '< 2.5' - gem 'graphql' + gem 'excon' gem 'grape' + gem 'graphql' + gem 'grpc' + gem 'hiredis' + gem 'mongo', '< 2.5' + gem 'mysql2', '< 0.5', platform: :ruby + gem 'racecar', '>= 0.3.5' gem 'rack' gem 'rack-test' + gem 'rake', '>= 12.3' gem 'redis', '< 4.0' - gem 'hiredis' + gem 'resque', '< 2.0' + gem 'sequel' + gem 'sidekiq' gem 'sinatra' gem 'sqlite3' - gem 'activerecord', '< 5.1.5' - gem 'sidekiq' - gem 'aws-sdk' gem 'sucker_punch' - gem 'dalli' - gem 'resque', '< 2.0' - gem 'racecar', '>= 0.3.5' - gem 'mysql2', '< 0.5', platform: :ruby end end -elsif '2.4.0' <= RUBY_VERSION +elsif Gem::Version.new('2.4.0') <= Gem::Version.new(RUBY_VERSION) if RUBY_PLATFORM != 'java' appraise 'contrib' do + gem 'active_model_serializers', '>= 0.10.0' + gem 'activerecord', '< 5.1.5' + gem 'aws-sdk' + gem 'dalli' gem 'elasticsearch-transport' - gem 'mongo', '< 2.5' - gem 'graphql' + gem 'excon' gem 'grape' + gem 'graphql' + gem 'grpc' + gem 'hiredis' + gem 'mongo', '< 2.5' + gem 'mysql2', '< 0.5', platform: :ruby + gem 'racecar', '>= 0.3.5' gem 'rack' gem 'rack-test' + gem 'rake', '>= 12.3' gem 'redis', '< 4.0' - gem 'hiredis' + gem 'resque', '< 2.0' + gem 'sequel' + gem 'sidekiq' gem 'sinatra' gem 'sqlite3' - gem 'activerecord', '< 5.1.5' - gem 'sidekiq' - gem 'aws-sdk' gem 'sucker_punch' - gem 'dalli' - gem 'resque', '< 2.0' - gem 'racecar', '>= 0.3.5' - gem 'mysql2', '< 0.5', platform: :ruby end end end diff --git a/CHANGELOG.md b/CHANGELOG.md index bf68eefa22..da50a9c5e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,59 @@ ## [Unreleased (beta)] +## [0.13.0] - 2018-06-20 + +Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.13.0 + +Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.12.1...v0.13.0 + +### Added + +- Sequel integration (supporting Ruby 2.0+) (#171, #367) (@randy-girard, @twe4ked, @palin) +- gRPC integration (supporting Ruby 2.2+) (#379, #403) (@Jared-Prime) +- ActiveModelSerializers integration (#340) (@sullimander) +- Excon integration (#211, #426) (@walterking, @jeffjo) +- Rake integration (supporting Ruby 2.0+, Rake 12.0+) (#409) +- Request queuing tracing to Rack (experimental) (#272) +- ActiveSupport::Notifications::Event helper for event tracing (#400) +- Request and response header tags to Rack (#389) +- Request and response header tags to Sinatra (#427, #375) +- MySQL2 integration (#453) (@jamiehodge) +- Sidekiq job delay tag (#443, #418) (@gottfrois) + +### Fixed + +- Elasticsearch quantization of ids (#458) +- MongoDB to allow quantization of collection name (#463) + +### Refactored + +- Hash quantization into core library (#410) +- MongoDB integration to use Hash quantization library (#463) + +### Changed + +- Hash quantization truncates arrays with nested objects (#463) + +## [0.13.0.beta1] - 2018-05-09 + +Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.13.0.beta1 + +Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.12.0...v0.13.0.beta1 + +### Added +- Sequel integration (supporting Ruby 2.0+) (#171, #367) (@randy-girard, @twe4ked, @palin) +- gRPC integration (supporting Ruby 2.2+) (#379, #403) (@Jared-Prime) +- ActiveModelSerializers integration (#340) (@sullimander) +- Excon integration (#211) (@walterking) +- Rake integration (supporting Ruby 2.0+, Rake 12.0+) (#409) +- Request queuing tracing to Rack (experimental) (#272) +- ActiveSupport::Notifications::Event helper for event tracing (#400) +- Request and response header tags to Rack (#389) + +### Refactored +- Hash quantization into core library (#410) + ## [0.12.1] - 2018-06-12 Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.12.1 @@ -307,8 +360,10 @@ Release notes: https://github.com/DataDog/dd-trace-rb/releases/tag/v0.3.1 Git diff: https://github.com/DataDog/dd-trace-rb/compare/v0.3.0...v0.3.1 -[Unreleased (stable)]: https://github.com/DataDog/dd-trace-rb/compare/v0.12.1...master -[Unreleased (beta)]: https://github.com/DataDog/dd-trace-rb/compare/v0.12.1...0.13-dev +[Unreleased (stable)]: https://github.com/DataDog/dd-trace-rb/compare/v0.13.0...master +[Unreleased (beta)]: https://github.com/DataDog/dd-trace-rb/compare/v0.13.0...0.14-dev +[0.13.0]: https://github.com/DataDog/dd-trace-rb/compare/v0.12.1...v0.13.0 +[0.13.0.beta1]: https://github.com/DataDog/dd-trace-rb/compare/v0.12.0...v0.13.0.beta1 [0.12.1]: https://github.com/DataDog/dd-trace-rb/compare/v0.12.0...v0.12.1 [0.12.0]: https://github.com/DataDog/dd-trace-rb/compare/v0.11.4...v0.12.0 [0.12.0.rc1]: https://github.com/DataDog/dd-trace-rb/compare/v0.11.4...v0.12.0.rc1 diff --git a/Rakefile b/Rakefile index f1e5b80ac6..d968e76316 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,6 @@ require 'bundler/gem_tasks' require 'ddtrace/version' -require 'rubocop/rake_task' if RUBY_VERSION >= '2.1.0' +require 'rubocop/rake_task' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.1.0') require 'rspec/core/rake_task' require 'rake/testtask' require 'appraisal' @@ -40,20 +40,26 @@ namespace :spec do end [ + :active_model_serializers, :active_record, :active_support, :aws, :dalli, :elasticsearch, + :excon, :faraday, :grape, :graphql, + :grpc, :http, :mongodb, + :mysql2, :racecar, :rack, + :rake, :redis, :resque, + :sequel, :sidekiq, :sinatra, :sucker_punch @@ -114,10 +120,7 @@ namespace :test do :elasticsearch, :grape, :http, - :mongodb, - :resque, :rack, - :resque, :sidekiq, :sinatra, :sucker_punch @@ -139,7 +142,7 @@ Rake::TestTask.new(:benchmark) do |t| t.test_files = FileList['test/benchmark_test.rb'] end -if RUBY_VERSION >= '2.1.0' +if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.1.0') RuboCop::RakeTask.new(:rubocop) do |t| t.options << ['-D'] t.patterns = ['lib/**/*.rb', 'test/**/*.rb', 'spec/**/*.rb', 'Gemfile', 'Rakefile'] @@ -196,261 +199,301 @@ task :'release:docs' => :docs do sh "aws s3 cp --recursive doc/ s3://#{S3_BUCKET}/#{S3_DIR}/docs/" end -# rubocop:disable Style/YodaCondition desc 'CI task; it runs all tests for current version of Ruby' task :ci do - if RUBY_VERSION < '1.9.3' + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('1.9.3') raise NotImplementedError, 'Ruby versions < 1.9.3 are not supported!' - elsif '1.9.3' <= RUBY_VERSION && RUBY_VERSION < '2.0.0' + elsif Gem::Version.new('1.9.3') <= Gem::Version.new(RUBY_VERSION) \ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') # Main library - sh 'rake test:main' - sh 'rake spec:main' + sh 'bundle exec rake test:main' + sh 'bundle exec rake spec:main' if RUBY_PLATFORM != 'java' # Contrib minitests - sh 'appraisal contrib-old rake test:aws' - sh 'appraisal contrib-old rake test:elasticsearch' - sh 'appraisal contrib-old rake test:http' - sh 'appraisal contrib-old rake test:mongodb' - sh 'appraisal contrib-old rake test:monkey' - sh 'appraisal contrib-old rake test:rack' - sh 'appraisal contrib-old rake test:resque' - sh 'appraisal contrib-old rake test:sinatra' - sh 'appraisal contrib-old rake test:sucker_punch' + sh 'bundle exec appraisal contrib-old rake test:aws' + sh 'bundle exec appraisal contrib-old rake test:elasticsearch' + sh 'bundle exec appraisal contrib-old rake test:http' + sh 'bundle exec appraisal contrib-old rake test:monkey' + sh 'bundle exec appraisal contrib-old rake test:rack' + sh 'bundle exec appraisal contrib-old rake test:sinatra' + sh 'bundle exec appraisal contrib-old rake test:sucker_punch' # Contrib specs - sh 'appraisal contrib-old rake spec:active_record' - sh 'appraisal contrib-old rake spec:active_support' - sh 'appraisal contrib-old rake spec:dalli' - sh 'appraisal contrib-old rake spec:faraday' - sh 'appraisal contrib-old rake spec:http' - sh 'appraisal contrib-old rake spec:redis' + sh 'bundle exec appraisal contrib-old rake spec:active_model_serializers' + sh 'bundle exec appraisal contrib-old rake spec:active_record' + sh 'bundle exec appraisal contrib-old rake spec:active_support' + sh 'bundle exec appraisal contrib-old rake spec:dalli' + sh 'bundle exec appraisal contrib-old rake spec:excon' + sh 'bundle exec appraisal contrib-old rake spec:faraday' + sh 'bundle exec appraisal contrib-old rake spec:http' + sh 'bundle exec appraisal contrib-old rake spec:mongodb' + sh 'bundle exec appraisal contrib-old rake spec:mysql2' + sh 'bundle exec appraisal contrib-old rake spec:rake' + sh 'bundle exec appraisal contrib-old rake spec:redis' + sh 'bundle exec appraisal contrib-old rake spec:resque' + sh 'bundle exec appraisal contrib-old rake spec:sequel' # Rails minitests - sh 'appraisal rails30-postgres rake test:rails' - sh 'appraisal rails30-postgres rake test:railsdisableenv' - sh 'appraisal rails32-mysql2 rake test:rails' - sh 'appraisal rails32-postgres rake test:rails' - sh 'appraisal rails32-postgres-redis rake test:railsredis' - sh 'appraisal rails32-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails30-postgres rake test:rails' + sh 'bundle exec appraisal rails30-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails32-mysql2 rake test:rails' + sh 'bundle exec appraisal rails32-postgres rake test:rails' + sh 'bundle exec appraisal rails32-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails32-postgres rake test:railsdisableenv' # Rails specs - sh 'appraisal rails30-postgres rake spec:rails' - sh 'appraisal rails32-mysql2 rake spec:rails' - sh 'appraisal rails32-postgres rake spec:rails' + sh 'bundle exec appraisal rails30-postgres rake spec:rails' + sh 'bundle exec appraisal rails32-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails32-postgres rake spec:rails' end - elsif '2.0.0' <= RUBY_VERSION && RUBY_VERSION < '2.1.0' + elsif Gem::Version.new('2.0.0') <= Gem::Version.new(RUBY_VERSION) \ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.1.0') # Main library - sh 'rake test:main' - sh 'rake spec:main' + sh 'bundle exec rake test:main' + sh 'bundle exec rake spec:main' if RUBY_PLATFORM != 'java' # Contrib minitests - sh 'appraisal contrib-old rake test:aws' - sh 'appraisal contrib-old rake test:elasticsearch' - sh 'appraisal contrib-old rake test:http' - sh 'appraisal contrib-old rake test:mongodb' - sh 'appraisal contrib-old rake test:monkey' - sh 'appraisal contrib-old rake test:rack' - sh 'appraisal contrib-old rake test:resque' - sh 'appraisal contrib-old rake test:sinatra' - sh 'appraisal contrib-old rake test:sucker_punch' + sh 'bundle exec appraisal contrib-old rake test:aws' + sh 'bundle exec appraisal contrib-old rake test:elasticsearch' + sh 'bundle exec appraisal contrib-old rake test:http' + sh 'bundle exec appraisal contrib-old rake test:monkey' + sh 'bundle exec appraisal contrib-old rake test:rack' + sh 'bundle exec appraisal contrib-old rake test:sinatra' + sh 'bundle exec appraisal contrib-old rake test:sucker_punch' # Contrib specs - sh 'appraisal contrib-old rake spec:active_record' - sh 'appraisal contrib-old rake spec:active_support' - sh 'appraisal contrib-old rake spec:dalli' - sh 'appraisal contrib-old rake spec:faraday' - sh 'appraisal contrib-old rake spec:http' - sh 'appraisal contrib-old rake spec:redis' + sh 'bundle exec appraisal contrib-old rake spec:active_model_serializers' + sh 'bundle exec appraisal contrib-old rake spec:active_record' + sh 'bundle exec appraisal contrib-old rake spec:active_support' + sh 'bundle exec appraisal contrib-old rake spec:dalli' + sh 'bundle exec appraisal contrib-old rake spec:excon' + sh 'bundle exec appraisal contrib-old rake spec:faraday' + sh 'bundle exec appraisal contrib-old rake spec:http' + sh 'bundle exec appraisal contrib-old rake spec:mongodb' + sh 'bundle exec appraisal contrib-old rake spec:mysql2' + sh 'bundle exec appraisal contrib-old rake spec:rake' + sh 'bundle exec appraisal contrib-old rake spec:redis' + sh 'bundle exec appraisal contrib-old rake spec:resque' + sh 'bundle exec appraisal contrib-old rake spec:sequel' # Rails minitests - sh 'appraisal contrib-old rake test:sidekiq' - sh 'appraisal rails30-postgres rake test:rails' - sh 'appraisal rails30-postgres rake test:railsdisableenv' - sh 'appraisal rails32-mysql2 rake test:rails' - sh 'appraisal rails32-postgres rake test:rails' - sh 'appraisal rails32-postgres-redis rake test:railsredis' - sh 'appraisal rails32-postgres rake test:railsdisableenv' - sh 'appraisal rails30-postgres-sidekiq rake test:railssidekiq' - sh 'appraisal rails32-postgres-sidekiq rake test:railssidekiq' + sh 'bundle exec appraisal contrib-old rake test:sidekiq' + sh 'bundle exec appraisal rails30-postgres rake test:rails' + sh 'bundle exec appraisal rails30-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails32-mysql2 rake test:rails' + sh 'bundle exec appraisal rails32-postgres rake test:rails' + sh 'bundle exec appraisal rails32-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails32-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails30-postgres-sidekiq rake test:railssidekiq' + sh 'bundle exec appraisal rails32-postgres-sidekiq rake test:railssidekiq' # Rails specs - sh 'appraisal rails30-postgres rake spec:rails' - sh 'appraisal rails32-mysql2 rake spec:rails' - sh 'appraisal rails32-postgres rake spec:rails' + sh 'bundle exec appraisal rails30-postgres rake spec:rails' + sh 'bundle exec appraisal rails32-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails32-postgres rake spec:rails' end - elsif '2.1.0' <= RUBY_VERSION && RUBY_VERSION < '2.2.0' + elsif Gem::Version.new('2.1.0') <= Gem::Version.new(RUBY_VERSION) \ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0') # Main library - sh 'rake test:main' - sh 'rake spec:main' + sh 'bundle exec rake test:main' + sh 'bundle exec rake spec:main' if RUBY_PLATFORM != 'java' # Contrib minitests - sh 'appraisal contrib-old rake test:aws' - sh 'appraisal contrib-old rake test:elasticsearch' - sh 'appraisal contrib-old rake test:http' - sh 'appraisal contrib-old rake test:mongodb' - sh 'appraisal contrib-old rake test:monkey' - sh 'appraisal contrib-old rake test:rack' - sh 'appraisal contrib-old rake test:resque' - sh 'appraisal contrib-old rake test:sinatra' - sh 'appraisal contrib-old rake test:sucker_punch' + sh 'bundle exec appraisal contrib-old rake test:aws' + sh 'bundle exec appraisal contrib-old rake test:elasticsearch' + sh 'bundle exec appraisal contrib-old rake test:http' + sh 'bundle exec appraisal contrib-old rake test:monkey' + sh 'bundle exec appraisal contrib-old rake test:rack' + sh 'bundle exec appraisal contrib-old rake test:sinatra' + sh 'bundle exec appraisal contrib-old rake test:sucker_punch' # Contrib specs - sh 'appraisal contrib-old rake spec:active_record' - sh 'appraisal contrib-old rake spec:active_support' - sh 'appraisal contrib-old rake spec:dalli' - sh 'appraisal contrib-old rake spec:faraday' - sh 'appraisal contrib-old rake spec:http' - sh 'appraisal contrib-old rake spec:redis' + sh 'bundle exec appraisal contrib-old rake spec:active_model_serializers' + sh 'bundle exec appraisal contrib-old rake spec:active_record' + sh 'bundle exec appraisal contrib-old rake spec:active_support' + sh 'bundle exec appraisal contrib-old rake spec:dalli' + sh 'bundle exec appraisal contrib-old rake spec:excon' + sh 'bundle exec appraisal contrib-old rake spec:faraday' + sh 'bundle exec appraisal contrib-old rake spec:http' + sh 'bundle exec appraisal contrib-old rake spec:mongodb' + sh 'bundle exec appraisal contrib-old rake spec:mysql2' + sh 'bundle exec appraisal contrib-old rake spec:rake' + sh 'bundle exec appraisal contrib-old rake spec:redis' + sh 'bundle exec appraisal contrib-old rake spec:resque' + sh 'bundle exec appraisal contrib-old rake spec:sequel' # Rails minitests - sh 'appraisal contrib-old rake test:sidekiq' - sh 'appraisal rails30-postgres rake test:rails' - sh 'appraisal rails30-postgres rake test:railsdisableenv' - sh 'appraisal rails32-mysql2 rake test:rails' - sh 'appraisal rails32-postgres rake test:rails' - sh 'appraisal rails32-postgres-redis rake test:railsredis' - sh 'appraisal rails32-postgres rake test:railsdisableenv' - sh 'appraisal rails4-mysql2 rake test:rails' - sh 'appraisal rails4-postgres rake test:rails' - sh 'appraisal rails4-postgres-redis rake test:railsredis' - sh 'appraisal rails4-postgres rake test:railsdisableenv' - sh 'appraisal rails30-postgres-sidekiq rake test:railssidekiq' - sh 'appraisal rails32-postgres-sidekiq rake test:railssidekiq' + sh 'bundle exec appraisal contrib-old rake test:sidekiq' + sh 'bundle exec appraisal rails30-postgres rake test:rails' + sh 'bundle exec appraisal rails30-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails32-mysql2 rake test:rails' + sh 'bundle exec appraisal rails32-postgres rake test:rails' + sh 'bundle exec appraisal rails32-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails32-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails4-mysql2 rake test:rails' + sh 'bundle exec appraisal rails4-postgres rake test:rails' + sh 'bundle exec appraisal rails4-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails4-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails30-postgres-sidekiq rake test:railssidekiq' + sh 'bundle exec appraisal rails32-postgres-sidekiq rake test:railssidekiq' # Rails specs - sh 'appraisal rails30-postgres rake spec:rails' - sh 'appraisal rails32-mysql2 rake spec:rails' - sh 'appraisal rails32-postgres rake spec:rails' - sh 'appraisal rails4-mysql2 rake spec:rails' - sh 'appraisal rails4-postgres rake spec:rails' + sh 'bundle exec appraisal rails30-postgres rake spec:rails' + sh 'bundle exec appraisal rails32-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails32-postgres rake spec:rails' + sh 'bundle exec appraisal rails4-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails4-postgres rake spec:rails' end - elsif '2.2.0' <= RUBY_VERSION && RUBY_VERSION < '2.3.0' + elsif Gem::Version.new('2.2.0') <= Gem::Version.new(RUBY_VERSION)\ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3.0') # Main library - sh 'rake test:main' - sh 'rake spec:main' + sh 'bundle exec rake test:main' + sh 'bundle exec rake spec:main' if RUBY_PLATFORM != 'java' # Contrib minitests - sh 'appraisal contrib rake test:aws' - sh 'appraisal contrib rake test:elasticsearch' - sh 'appraisal contrib rake test:grape' - sh 'appraisal contrib rake test:http' - sh 'appraisal contrib rake test:mongodb' - sh 'appraisal contrib rake test:rack' - sh 'appraisal contrib rake test:resque' - sh 'appraisal contrib rake test:sucker_punch' + sh 'bundle exec appraisal contrib rake test:aws' + sh 'bundle exec appraisal contrib rake test:elasticsearch' + sh 'bundle exec appraisal contrib rake test:grape' + sh 'bundle exec appraisal contrib rake test:http' + sh 'bundle exec appraisal contrib rake test:rack' + sh 'bundle exec appraisal contrib rake test:sinatra' + sh 'bundle exec appraisal contrib rake test:sucker_punch' # Contrib specs - sh 'appraisal contrib rake spec:active_record' - sh 'appraisal contrib rake spec:active_support' - sh 'appraisal contrib rake spec:dalli' - sh 'appraisal contrib rake spec:faraday' - sh 'appraisal contrib rake spec:graphql' - sh 'appraisal contrib rake spec:http' - sh 'appraisal contrib rake spec:racecar' - sh 'appraisal contrib rake spec:redis' + sh 'bundle exec appraisal contrib rake spec:active_model_serializers' + sh 'bundle exec appraisal contrib rake spec:active_record' + sh 'bundle exec appraisal contrib rake spec:active_support' + sh 'bundle exec appraisal contrib rake spec:dalli' + sh 'bundle exec appraisal contrib rake spec:excon' + sh 'bundle exec appraisal contrib rake spec:faraday' + sh 'bundle exec appraisal contrib rake spec:graphql' + sh 'bundle exec appraisal contrib rake spec:grpc' + sh 'bundle exec appraisal contrib rake spec:http' + sh 'bundle exec appraisal contrib rake spec:mongodb' + sh 'bundle exec appraisal contrib rake spec:mysql2' + sh 'bundle exec appraisal contrib rake spec:racecar' + sh 'bundle exec appraisal contrib rake spec:rake' + sh 'bundle exec appraisal contrib rake spec:redis' + sh 'bundle exec appraisal contrib rake spec:resque' + sh 'bundle exec appraisal contrib rake spec:sequel' # Rails minitests - sh 'appraisal contrib rake test:sidekiq' - sh 'appraisal rails30-postgres rake test:rails' - sh 'appraisal rails30-postgres rake test:railsdisableenv' - sh 'appraisal rails32-mysql2 rake test:rails' - sh 'appraisal rails32-postgres rake test:rails' - sh 'appraisal rails32-postgres-redis rake test:railsredis' - sh 'appraisal rails32-postgres rake test:railsdisableenv' - sh 'appraisal rails4-mysql2 rake test:rails' - sh 'appraisal rails4-postgres rake test:rails' - sh 'appraisal rails4-postgres-redis rake test:railsredis' - sh 'appraisal rails4-postgres rake test:railsdisableenv' - sh 'appraisal rails4-postgres-sidekiq rake test:railssidekiq' - sh 'appraisal rails4-postgres-sidekiq rake test:railsactivejob' - sh 'appraisal rails5-mysql2 rake test:rails' - sh 'appraisal rails5-postgres rake test:rails' - sh 'appraisal rails5-postgres-redis rake test:railsredis' - sh 'appraisal rails5-postgres-sidekiq rake test:railssidekiq' - sh 'appraisal rails5-postgres-sidekiq rake test:railsactivejob' - sh 'appraisal rails5-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal contrib rake test:sidekiq' + sh 'bundle exec appraisal rails30-postgres rake test:rails' + sh 'bundle exec appraisal rails30-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails32-mysql2 rake test:rails' + sh 'bundle exec appraisal rails32-postgres rake test:rails' + sh 'bundle exec appraisal rails32-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails32-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails4-mysql2 rake test:rails' + sh 'bundle exec appraisal rails4-postgres rake test:rails' + sh 'bundle exec appraisal rails4-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails4-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails4-postgres-sidekiq rake test:railssidekiq' + sh 'bundle exec appraisal rails4-postgres-sidekiq rake test:railsactivejob' + sh 'bundle exec appraisal rails5-mysql2 rake test:rails' + sh 'bundle exec appraisal rails5-postgres rake test:rails' + sh 'bundle exec appraisal rails5-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails5-postgres-sidekiq rake test:railssidekiq' + sh 'bundle exec appraisal rails5-postgres-sidekiq rake test:railsactivejob' + sh 'bundle exec appraisal rails5-postgres rake test:railsdisableenv' # Rails specs - sh 'appraisal rails30-postgres rake spec:rails' - sh 'appraisal rails32-mysql2 rake spec:rails' - sh 'appraisal rails32-postgres rake spec:rails' - sh 'appraisal rails4-mysql2 rake spec:rails' - sh 'appraisal rails4-postgres rake spec:rails' - sh 'appraisal rails5-mysql2 rake spec:rails' - sh 'appraisal rails5-postgres rake spec:rails' + sh 'bundle exec appraisal rails30-postgres rake spec:rails' + sh 'bundle exec appraisal rails32-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails32-postgres rake spec:rails' + sh 'bundle exec appraisal rails4-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails4-postgres rake spec:rails' + sh 'bundle exec appraisal rails5-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails5-postgres rake spec:rails' end - elsif '2.3.0' <= RUBY_VERSION && RUBY_VERSION < '2.4.0' + elsif Gem::Version.new('2.3.0') <= Gem::Version.new(RUBY_VERSION) \ + && Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.4.0') # Main library - sh 'rake test:main' - sh 'rake spec:main' + sh 'bundle exec rake test:main' + sh 'bundle exec rake spec:main' if RUBY_PLATFORM != 'java' # Contrib minitests - sh 'appraisal contrib rake test:aws' - sh 'appraisal contrib rake test:elasticsearch' - sh 'appraisal contrib rake test:grape' - sh 'appraisal contrib rake test:http' - sh 'appraisal contrib rake test:mongodb' - sh 'appraisal contrib rake test:rack' - sh 'appraisal contrib rake test:resque' - sh 'appraisal contrib rake test:sucker_punch' + sh 'bundle exec appraisal contrib rake test:aws' + sh 'bundle exec appraisal contrib rake test:elasticsearch' + sh 'bundle exec appraisal contrib rake test:grape' + sh 'bundle exec appraisal contrib rake test:http' + sh 'bundle exec appraisal contrib rake test:rack' + sh 'bundle exec appraisal contrib rake test:sinatra' + sh 'bundle exec appraisal contrib rake test:sucker_punch' # Contrib specs - sh 'appraisal contrib rake spec:active_record' - sh 'appraisal contrib rake spec:active_support' - sh 'appraisal contrib rake spec:dalli' - sh 'appraisal contrib rake spec:faraday' - sh 'appraisal contrib rake spec:graphql' - sh 'appraisal contrib rake spec:graphql' - sh 'appraisal contrib rake spec:racecar' - sh 'appraisal contrib rake spec:redis' + sh 'bundle exec appraisal contrib rake spec:active_model_serializers' + sh 'bundle exec appraisal contrib rake spec:active_record' + sh 'bundle exec appraisal contrib rake spec:active_support' + sh 'bundle exec appraisal contrib rake spec:dalli' + sh 'bundle exec appraisal contrib rake spec:excon' + sh 'bundle exec appraisal contrib rake spec:faraday' + sh 'bundle exec appraisal contrib rake spec:graphql' + sh 'bundle exec appraisal contrib rake spec:grpc' + sh 'bundle exec appraisal contrib rake spec:http' + sh 'bundle exec appraisal contrib rake spec:mongodb' + sh 'bundle exec appraisal contrib rake spec:mysql2' + sh 'bundle exec appraisal contrib rake spec:racecar' + sh 'bundle exec appraisal contrib rake spec:rake' + sh 'bundle exec appraisal contrib rake spec:redis' + sh 'bundle exec appraisal contrib rake spec:resque' + sh 'bundle exec appraisal contrib rake spec:sequel' # Rails minitests - sh 'appraisal contrib rake test:sidekiq' - sh 'appraisal rails30-postgres rake test:rails' - sh 'appraisal rails30-postgres rake test:railsdisableenv' - sh 'appraisal rails32-mysql2 rake test:rails' - sh 'appraisal rails32-postgres rake test:rails' - sh 'appraisal rails32-postgres-redis rake test:railsredis' - sh 'appraisal rails32-postgres rake test:railsdisableenv' - sh 'appraisal rails4-mysql2 rake test:rails' - sh 'appraisal rails4-postgres rake test:rails' - sh 'appraisal rails4-postgres-redis rake test:railsredis' - sh 'appraisal rails4-postgres rake test:railsdisableenv' - sh 'appraisal rails4-postgres-sidekiq rake test:railssidekiq' - sh 'appraisal rails4-postgres-sidekiq rake test:railsactivejob' - sh 'appraisal rails5-mysql2 rake test:rails' - sh 'appraisal rails5-postgres rake test:rails' - sh 'appraisal rails5-postgres-redis rake test:railsredis' - sh 'appraisal rails5-postgres-sidekiq rake test:railssidekiq' - sh 'appraisal rails5-postgres-sidekiq rake test:railsactivejob' - sh 'appraisal rails5-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal contrib rake test:sidekiq' + sh 'bundle exec appraisal rails30-postgres rake test:rails' + sh 'bundle exec appraisal rails30-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails32-mysql2 rake test:rails' + sh 'bundle exec appraisal rails32-postgres rake test:rails' + sh 'bundle exec appraisal rails32-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails32-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails4-mysql2 rake test:rails' + sh 'bundle exec appraisal rails4-postgres rake test:rails' + sh 'bundle exec appraisal rails4-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails4-postgres rake test:railsdisableenv' + sh 'bundle exec appraisal rails4-postgres-sidekiq rake test:railssidekiq' + sh 'bundle exec appraisal rails4-postgres-sidekiq rake test:railsactivejob' + sh 'bundle exec appraisal rails5-mysql2 rake test:rails' + sh 'bundle exec appraisal rails5-postgres rake test:rails' + sh 'bundle exec appraisal rails5-postgres-redis rake test:railsredis' + sh 'bundle exec appraisal rails5-postgres-sidekiq rake test:railssidekiq' + sh 'bundle exec appraisal rails5-postgres-sidekiq rake test:railsactivejob' + sh 'bundle exec appraisal rails5-postgres rake test:railsdisableenv' # Rails specs - sh 'appraisal rails30-postgres rake spec:rails' - sh 'appraisal rails32-mysql2 rake spec:rails' - sh 'appraisal rails32-postgres rake spec:rails' - sh 'appraisal rails4-mysql2 rake spec:rails' - sh 'appraisal rails4-postgres rake spec:rails' - sh 'appraisal rails5-mysql2 rake spec:rails' - sh 'appraisal rails5-postgres rake spec:rails' + sh 'bundle exec appraisal rails30-postgres rake spec:rails' + sh 'bundle exec appraisal rails32-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails32-postgres rake spec:rails' + sh 'bundle exec appraisal rails4-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails4-postgres rake spec:rails' + sh 'bundle exec appraisal rails5-mysql2 rake spec:rails' + sh 'bundle exec appraisal rails5-postgres rake spec:rails' end - elsif '2.4.0' <= RUBY_VERSION + elsif Gem::Version.new('2.4.0') <= Gem::Version.new(RUBY_VERSION) # Main library - sh 'rake test:main' - sh 'rake spec:main' + sh 'bundle exec rake test:main' + sh 'bundle exec rake spec:main' if RUBY_PLATFORM != 'java' # Contrib minitests - sh 'appraisal contrib rake test:aws' - sh 'appraisal contrib rake test:elasticsearch' - sh 'appraisal contrib rake test:grape' - sh 'appraisal contrib rake test:http' - sh 'appraisal contrib rake test:mongodb' - sh 'appraisal contrib rake test:rack' - sh 'appraisal contrib rake test:resque' - sh 'appraisal contrib rake test:sucker_punch' + sh 'bundle exec appraisal contrib rake test:aws' + sh 'bundle exec appraisal contrib rake test:elasticsearch' + sh 'bundle exec appraisal contrib rake test:grape' + sh 'bundle exec appraisal contrib rake test:http' + sh 'bundle exec appraisal contrib rake test:rack' + sh 'bundle exec appraisal contrib rake test:sinatra' + sh 'bundle exec appraisal contrib rake test:sucker_punch' # Contrib specs - sh 'appraisal contrib rake spec:active_record' - sh 'appraisal contrib rake spec:active_support' - sh 'appraisal contrib rake spec:dalli' - sh 'appraisal contrib rake spec:faraday' - sh 'appraisal contrib rake spec:graphql' - sh 'appraisal contrib rake spec:graphql' - sh 'appraisal contrib rake spec:racecar' - sh 'appraisal contrib rake spec:redis' + sh 'bundle exec appraisal contrib rake spec:active_model_serializers' + sh 'bundle exec appraisal contrib rake spec:active_record' + sh 'bundle exec appraisal contrib rake spec:active_support' + sh 'bundle exec appraisal contrib rake spec:dalli' + sh 'bundle exec appraisal contrib rake spec:excon' + sh 'bundle exec appraisal contrib rake spec:faraday' + sh 'bundle exec appraisal contrib rake spec:graphql' + sh 'bundle exec appraisal contrib rake spec:grpc' + sh 'bundle exec appraisal contrib rake spec:http' + sh 'bundle exec appraisal contrib rake spec:mongodb' + sh 'bundle exec appraisal contrib rake spec:mysql2' + sh 'bundle exec appraisal contrib rake spec:racecar' + sh 'bundle exec appraisal contrib rake spec:rake' + sh 'bundle exec appraisal contrib rake spec:redis' + sh 'bundle exec appraisal contrib rake spec:resque' + sh 'bundle exec appraisal contrib rake spec:sequel' # Rails minitests - sh 'appraisal contrib rake test:sidekiq' - sh 'rake benchmark' + sh 'bundle exec appraisal contrib rake test:sidekiq' + sh 'bundle exec rake benchmark' end end end diff --git a/ddtrace.gemspec b/ddtrace.gemspec index 46592e19be..9e0a5b6bf6 100644 --- a/ddtrace.gemspec +++ b/ddtrace.gemspec @@ -34,7 +34,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'msgpack' - spec.add_development_dependency 'rake', '~> 10.5' + spec.add_development_dependency 'rake', '>= 10.5' spec.add_development_dependency 'rubocop', '= 0.49.1' if RUBY_VERSION >= '2.1.0' spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'rspec-collection_matchers', '~> 1.1' diff --git a/docker-compose.yml b/docker-compose.yml index e84882b374..d1dcd7b4c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - redis env_file: ./.env environment: + - BUNDLE_GEMFILE=/app/Gemfile - TEST_DATADOG_INTEGRATION=1 - TEST_DDAGENT_HOST=ddagent - TEST_ELASTICSEARCH_HOST=elasticsearch @@ -28,6 +29,7 @@ services: volumes: - .:/app - bundle-1.9:/usr/local/bundle + - gemfiles-1.9:/app/gemfiles tracer-2.0: build: context: ./.circleci/images/primary @@ -43,6 +45,7 @@ services: - redis env_file: ./.env environment: + - BUNDLE_GEMFILE=/app/Gemfile - TEST_DATADOG_INTEGRATION=1 - TEST_DDAGENT_HOST=ddagent - TEST_ELASTICSEARCH_HOST=elasticsearch @@ -56,6 +59,7 @@ services: volumes: - .:/app - bundle-2.0:/usr/local/bundle + - gemfiles-2.0:/app/gemfiles tracer-2.1: build: context: ./.circleci/images/primary @@ -71,6 +75,7 @@ services: - redis env_file: ./.env environment: + - BUNDLE_GEMFILE=/app/Gemfile - TEST_DATADOG_INTEGRATION=1 - TEST_DDAGENT_HOST=ddagent - TEST_ELASTICSEARCH_HOST=elasticsearch @@ -84,6 +89,7 @@ services: volumes: - .:/app - bundle-2.1:/usr/local/bundle + - gemfiles-2.1:/app/gemfiles tracer-2.2: build: context: ./.circleci/images/primary @@ -99,6 +105,7 @@ services: - redis env_file: ./.env environment: + - BUNDLE_GEMFILE=/app/Gemfile - TEST_DATADOG_INTEGRATION=1 - TEST_DDAGENT_HOST=ddagent - TEST_ELASTICSEARCH_HOST=elasticsearch @@ -112,6 +119,7 @@ services: volumes: - .:/app - bundle-2.2:/usr/local/bundle + - gemfiles-2.2:/app/gemfiles tracer-2.3: build: context: ./.circleci/images/primary @@ -127,6 +135,7 @@ services: - redis env_file: ./.env environment: + - BUNDLE_GEMFILE=/app/Gemfile - TEST_DATADOG_INTEGRATION=1 - TEST_DDAGENT_HOST=ddagent - TEST_ELASTICSEARCH_HOST=elasticsearch @@ -140,6 +149,7 @@ services: volumes: - .:/app - bundle-2.3:/usr/local/bundle + - gemfiles-2.3:/app/gemfiles tracer-2.4: build: context: ./.circleci/images/primary @@ -155,6 +165,7 @@ services: - redis env_file: ./.env environment: + - BUNDLE_GEMFILE=/app/Gemfile - TEST_DATADOG_INTEGRATION=1 - TEST_DDAGENT_HOST=ddagent - TEST_ELASTICSEARCH_HOST=elasticsearch @@ -168,6 +179,7 @@ services: volumes: - .:/app - bundle-2.4:/usr/local/bundle + - gemfiles-2.4:/app/gemfiles ddagent: image: datadog/docker-dd-agent environment: @@ -234,4 +246,10 @@ volumes: bundle-2.1: bundle-2.2: bundle-2.3: - bundle-2.4: \ No newline at end of file + bundle-2.4: + gemfiles-1.9: + gemfiles-2.0: + gemfiles-2.1: + gemfiles-2.2: + gemfiles-2.3: + gemfiles-2.4: diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index 8f18986c8d..0be28e971d 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -27,7 +27,9 @@ For descriptions of terminology used in APM, take a look at the [official docume - [AWS](#aws) - [Dalli](#dalli) - [Elastic Search](#elastic-search) + - [Excon](#excon) - [Faraday](#faraday) + - [gRPC](#grpc) - [Grape](#grape) - [GraphQL](#graphql) - [MongoDB](#mongodb) @@ -35,8 +37,10 @@ For descriptions of terminology used in APM, take a look at the [official docume - [Racecar](#racecar) - [Rack](#rack) - [Rails](#rails) + - [Rake](#rake) - [Redis](#redis) - [Resque](#resque) + - [Sequel](#sequel) - [Sidekiq](#sidekiq) - [Sinatra](#sinatra) - [Sucker Punch](#sucker-punch) @@ -47,6 +51,7 @@ For descriptions of terminology used in APM, take a look at the [official docume - [Sampling](#sampling) - [Priority sampling](#priority-sampling) - [Distributed tracing](#distributed-tracing) + - [HTTP request queuing](#http-request-queuing) - [Processing pipeline](#processing-pipeline) - [Filtering](#filtering) - [Processing](#processing) @@ -253,7 +258,9 @@ For a list of available integrations, and their configuration options, please re | AWS | `aws` | `>= 2.0` | *[Link](#aws)* | *[Link](https://github.com/aws/aws-sdk-ruby)* | | Dalli | `dalli` | `>= 2.7` | *[Link](#dalli)* | *[Link](https://github.com/petergoldstein/dalli)* | | Elastic Search | `elasticsearch` | `>= 6.0` | *[Link](#elastic-search)* | *[Link](https://github.com/elastic/elasticsearch-ruby)* | +| Excon | `excon` | `>= 0.62` | *[Link](#excon)* | *[Link](https://github.com/excon/excon)* | | Faraday | `faraday` | `>= 0.14` | *[Link](#faraday)* | *[Link](https://github.com/lostisland/faraday)* | +| gRPC | `grpc` | `>= 1.10` | *[Link](#grpc)* | *[Link](https://github.com/grpc/grpc/tree/master/src/rubyc)* | | Grape | `grape` | `>= 1.0` | *[Link](#grape)* | *[Link](https://github.com/ruby-grape/grape)* | | GraphQL | `graphql` | `>= 1.7.9` | *[Link](#graphql)* | *[Link](https://github.com/rmosolgo/graphql-ruby)* | | MongoDB | `mongo` | `>= 2.0, < 2.5` | *[Link](#mongodb)* | *[Link](https://github.com/mongodb/mongo-ruby-driver)* | @@ -261,8 +268,10 @@ For a list of available integrations, and their configuration options, please re | Racecar | `racecar` | `>= 0.3.5` | *[Link](#racecar)* | *[Link](https://github.com/zendesk/racecar)* | | Rack | `rack` | `>= 1.4.7` | *[Link](#rack)* | *[Link](https://github.com/rack/rack)* | | Rails | `rails` | `>= 3.2, < 5.2` | *[Link](#rails)* | *[Link](https://github.com/rails/rails)* | +| Rake | `rake` | `>= 12.0` | *[Link](#rake)* | *[Link](https://github.com/ruby/rake)* | | Redis | `redis` | `>= 3.2, < 4.0` | *[Link](#redis)* | *[Link](https://github.com/redis/redis-rb)* | | Resque | `resque` | `>= 1.0, < 2.0` | *[Link](#resque)* | *[Link](https://github.com/resque/resque)* | +| Sequel | `sequel` | `>= 3.41` | *[Link](#sequel)* | *[Link](https://github.com/jeremyevans/sequel)* | | Sidekiq | `sidekiq` | `>= 4.0` | *[Link](#sidekiq)* | *[Link](https://github.com/mperham/sidekiq)* | | Sinatra | `sinatra` | `>= 1.4.5` | *[Link](#sinatra)* | *[Link](https://github.com/sinatra/sinatra)* | | Sucker Punch | `sucker_punch` | `>= 2.0` | *[Link](#sucker-punch)* | *[Link](https://github.com/brandonhilkert/sucker_punch)* | @@ -362,6 +371,58 @@ Where `options` is an optional `Hash` that accepts the following parameters: | ``service_name`` | Service name used for `elasticsearch` instrumentation | elasticsearch | | ``quantize`` | Hash containing options for quantization. May include `:show` with an Array of keys to not quantize (or `:all` to skip quantization), or `:exclude` with Array of keys to exclude entirely. | {} | +### Excon + +The `excon` integration is available through the `ddtrace` middleware: + +```ruby +require 'excon' +require 'ddtrace' + +# Configure default Excon tracing behavior +Datadog.configure do |c| + c.use :excon, service_name: 'excon' +end + +connection = Excon.new('https://example.com') +connection.get +``` + +Where `options` is an optional `Hash` that accepts the following parameters: + +| Key | Description | Default | +| --- | --- | --- | +| `service_name` | Service name for Excon instrumentation. When provided to middleware for a specific connection, it applies only to that connection object. | `'excon'` | +| `split_by_domain` | Uses the request domain as the service name when set to `true`. | `false` | +| `distributed_tracing` | Enables [distributed tracing](#distributed-tracing) | `false` | +| `error_handler` | A `Proc` that accepts a `response` parameter. If it evaluates to a *truthy* value, the trace span is marked as an error. By default only sets 5XX responses as errors. | `nil` | +| `tracer` | A `Datadog::Tracer` instance used to instrument the application. Usually you don't need to set that. | `Datadog.tracer` | + +**Configuring connections to use different settings** + +If you use multiple connections with Excon, you can give each of them different settings by configuring their constructors with middleware: + +```ruby +# Wrap the Datadog tracing middleware around the default middleware stack +Excon.new( + 'http://example.com', + middlewares: Datadog::Contrib::Excon::Middleware.with(options).around_default_stack +) + +# Insert the middleware into a custom middleware stack. +# NOTE: Trace middleware must be inserted after ResponseParser! +Excon.new( + 'http://example.com', + middlewares: [ + Excon::Middleware::ResponseParser, + Datadog::Contrib::Excon::Middleware.with(options), + Excon::Middleware::Idempotent + ] +) +``` + +Where `options` is a Hash that contains any of the parameters listed in the table above. + ### Faraday The `faraday` integration is available through the `ddtrace` middleware: @@ -384,12 +445,63 @@ connection.get('/foo') Where `options` is an optional `Hash` that accepts the following parameters: -| Key | Default | Description | +| Key | Description | Default | +| --- | --- | --- | +| `service_name` | Service name for Faraday instrumentation. When provided to middleware for a specific connection, it applies only to that connection object. | `'faraday'` | +| `split_by_domain` | Uses the request domain as the service name when set to `true`. | `false` | +| `distributed_tracing` | Enables [distributed tracing](#distributed-tracing) | `false` | +| `error_handler` | A `Proc` that accepts a `response` parameter. If it evaluates to a *truthy* value, the trace span is marked as an error. By default only sets 5XX responses as errors. | ``5xx`` evaluated as errors | +| `tracer` | A `Datadog::Tracer` instance used to instrument the application. Usually you don't need to set that. | `Datadog.tracer` | + +### gRPC + +The `grpc` integration adds both client and server interceptors, which run as middleware prior to executing the service's remote procedure call. As gRPC applications are often distributed, the integration shares trace information between client and server. + +To setup your integration, use the ``Datadog.configure`` method like so: + +```ruby +require 'grpc' +require 'ddtrace' + +Datadog.configure do |c| + c.use :grpc, options +end + +# run your application normally + +# server side +server = GRPC::RpcServer.new +server.add_http2_port('localhost:50051', :this_port_is_insecure) +server.handle(Demo) +server.run_till_terminated + +# client side +client = Demo.rpc_stub_class.new('localhost:50051', :this_channel_is_insecure) +client.my_endpoint(DemoMessage.new(contents: 'hello!')) +``` + +In situations where you have multiple clients calling multiple distinct services, you may pass the Datadog interceptor directly, like so + +```ruby +configured_interceptor = Datadog::Contrib::GRPC::DatadogInterceptor::Client.new do |c| + c.service_name = "Alternate" +end + +alternate_client = Demo::Echo::Service.rpc_stub_class.new( + 'localhost:50052', + :this_channel_is_insecure, + :interceptors => [configured_interceptor] +) +``` + +The integration will ensure that the ``configured_interceptor`` establishes a unique tracing setup for that client instance. + +The following configuration options are supported: + +| Key | Description | Default | | --- | --- | --- | -| `service_name` | Global service name (default: `faraday`) | Service name for this specific connection object. | -| `split_by_domain` | `false` | Uses the request domain as the service name when set to `true`. | -| `distributed_tracing` | `false` | Propagates tracing context along the HTTP request when set to `true`. | -| `error_handler` | ``5xx`` evaluated as errors | A callable object that receives a single argument – the request environment. If it evaluates to a *truthy* value, the trace span is marked as an error. | +| ``service_name`` | Service name used for `grpc` instrumentation | grpc | +| ``tracer`` | Datadog tracer used for `grpc` instrumentation | Datadog.tracer | ### Grape @@ -499,6 +611,7 @@ Where `options` is an optional `Hash` that accepts the following parameters: | Key | Description | Default | | --- | --- | --- | | ``service_name`` | Service name used for `mongo` instrumentation | mongodb | +| ``quantize`` | Hash containing options for quantization. May include `:show` with an Array of keys to not quantize (or `:all` to skip quantization), or `:exclude` with Array of keys to exclude entirely. | ```{ show: [:collection, :database, :operation] }``` | ### Net/HTTP @@ -525,10 +638,9 @@ Where `options` is an optional `Hash` that accepts the following parameters: | Key | Description | Default | | --- | --- | --- | | ``service_name`` | Service name used for `http` instrumentation | net/http | -| ``distributed_tracing`` | Enables distributed tracing | ``false`` | +| ``distributed_tracing`` | Enables [distributed tracing](#distributed-tracing) | ``false`` | | ``tracer`` | A ``Datadog::Tracer`` instance used to instrument the application. Usually you don't need to set that. | ``Datadog.tracer`` | - If you wish to configure each connection object individually, you may use the ``Datadog.configure`` as it follows: ```ruby @@ -594,6 +706,9 @@ Where `options` is an optional `Hash` that accepts the following parameters: | ``quantize.fragment`` | Defines behavior for URL fragments. Removes fragments by default. May be `:show` to show URL fragments. Option must be nested inside the `quantize` option. | ``nil`` | | ``application`` | Your Rack application. Necessary for enabling middleware resource names. | ``nil`` | | ``tracer`` | A ``Datadog::Tracer`` instance used to instrument the application. Usually you don't need to set that. | ``Datadog.tracer`` | +| ``request_queuing`` | Track HTTP request time spent in the queue of the frontend server. See [HTTP request queuing](#http-request-queuing) for setup details. Set to `true` to enable. | ``false`` | +| ``web_service_name`` | Service name for frontend server request queuing spans. (e.g. `'nginx'`) | ``'web-server'`` | +| ``headers`` | Hash of HTTP request or response headers to add as tags to the `rack.request`. Accepts `request` and `response` keys with Array values e.g. `['Last-Modified']`. Adds `http.request.headers.*` and `http.response.headers.*` tags respectively. | ``{ response: ['Content-Type', 'X-Request-ID'] }`` | **Configuring URL quantization behavior** @@ -653,6 +768,71 @@ Where `options` is an optional `Hash` that accepts the following parameters: | ``template_base_path`` | Used when the template name is parsed. If you don't store your templates in the ``views/`` folder, you may need to change this value | ``views/`` | | ``tracer`` | A ``Datadog::Tracer`` instance used to instrument the application. Usually you don't need to set that. | ``Datadog.tracer`` | +### Rake + +You can add instrumentation around your Rake tasks by activating the `rake` integration. Each task and its subsequent subtasks will be traced. + +To activate Rake task tracing, add the following to your `Rakefile`: + +```ruby +# At the top of your Rakefile: +require 'rake' +require 'ddtrace' + +Datadog.configure do |c| + c.use :rake, options +end + +task :my_task do + # Do something task work here... +end + +Rake::Task['my_task'].invoke +``` + +Where `options` is an optional `Hash` that accepts the following parameters: + +| Key | Description | Default | +| --- | --- | --- | +| ``enabled`` | Defines whether Rake tasks should be traced. Useful for temporarily disabling tracing. `true` or `false` | ``true`` | +| ``quantize`` | Hash containing options for quantization of task arguments. See below for more details and examples. | ``{}`` | +| ``service_name`` | Service name which the Rake task traces should be grouped under. | ``rake`` | +| ``tracer`` | A ``Datadog::Tracer`` instance used to instrument the application. Usually you don't need to set that. | ``Datadog.tracer`` | + +**Configuring task quantization behavior** + +```ruby +Datadog.configure do |c| + # Given a task that accepts :one, :two, :three... + # Invoked with 'foo', 'bar', 'baz'. + + # Default behavior: all arguments are quantized. + # `rake.invoke.args` tag --> ['?'] + # `rake.execute.args` tag --> { one: '?', two: '?', three: '?' } + c.use :rake + + # Show values for any argument matching :two exactly + # `rake.invoke.args` tag --> ['?'] + # `rake.execute.args` tag --> { one: '?', two: 'bar', three: '?' } + c.use :rake, quantize: { args: { show: [:two] } } + + # Show all values for all arguments. + # `rake.invoke.args` tag --> ['foo', 'bar', 'baz'] + # `rake.execute.args` tag --> { one: 'foo', two: 'bar', three: 'baz' } + c.use :rake, quantize: { args: { show: :all } } + + # Totally exclude any argument matching :three exactly + # `rake.invoke.args` tag --> ['?'] + # `rake.execute.args` tag --> { one: '?', two: '?' } + c.use :rake, quantize: { args: { exclude: [:three] } } + + # Remove the arguments entirely + # `rake.invoke.args` tag --> ['?'] + # `rake.execute.args` tag --> {} + c.use :rake, quantize: { args: { exclude: :all } } +end +``` + ### Redis The Redis integration will trace simple calls as well as pipelines. @@ -715,6 +895,54 @@ Where `options` is an optional `Hash` that accepts the following parameters: | ``service_name`` | Service name used for `resque` instrumentation | resque | | ``workers`` | An array including all worker classes you want to trace (eg ``[MyJob]``) | ``[]`` | +### Sequel + +The Sequel integration traces queries made to your database. + +```ruby +require 'sequel' +require 'ddtrace' + +# Connect to database +database = Sequel.sqlite + +# Create a table +database.create_table :articles do + primary_key :id + String :name +end + +Datadog.configure do |c| + c.use :sequel, options +end + +# Perform a query +articles = database[:articles] +articles.all +``` + +Where `options` is an optional `Hash` that accepts the following parameters: + +| Key | Description | Default | +| --- | --- | --- | +| ``service_name`` | Service name used for `sequel.query` spans. | Name of database adapter (e.g. `mysql2`) | +| ``tracer`` | A ``Datadog::Tracer`` instance used to instrument the application. Usually you don't need to set that. | ``Datadog.tracer`` | + +Only Ruby 2.0+ is supported. + +**Configuring databases to use different settings** + +If you use multiple databases with Sequel, you can give each of them different settings by configuring their respective `Sequel::Database` objects: + +```ruby +sqlite_database = Sequel.sqlite +postgres_database = Sequel.connect('postgres://user:password@host:port/database_name') + +# Configure each database with different service names +Datadog.configure(sqlite_database, service_name: 'my-sqlite-db') +Datadog.configure(postgres_database, service_name: 'my-postgres-db') +``` + ### Sidekiq The Sidekiq integration is a server-side middleware which will trace job executions. @@ -764,6 +992,7 @@ Where `options` is an optional `Hash` that accepts the following parameters: | ``resource_script_names`` | Prepend resource names with script name | ``false`` | | ``distributed_tracing`` | Enables [distributed tracing](#distributed-tracing) so that this service trace is connected with a trace of another service if tracing headers are received | `false` | | ``tracer`` | A ``Datadog::Tracer`` instance used to instrument the application. Usually you don't need to set that. | ``Datadog.tracer`` | +| ``headers`` | Hash of HTTP request or response headers to add as tags to the `sinatra.request`. Accepts `request` and `response` keys with Array values e.g. `['Last-Modified']`. Adds `http.request.headers.*` and `http.response.headers.*` tags respectively. | ``{ response: ['Content-Type', 'X-Request-ID'] }`` | ### Sucker Punch @@ -1007,6 +1236,7 @@ Many integrations included in `ddtrace` support distributed tracing. Distributed For more details on how to activate distributed tracing for integrations, see their documentation: +- [Excon](#excon) - [Faraday](#faraday) - [Net/HTTP](#nethttp) - [Rack](#rack) @@ -1036,6 +1266,30 @@ Datadog.tracer.trace('web.work') do |span| end ``` +### HTTP request queuing + +Traces that originate from HTTP requests can be configured to include the time spent in a frontend web server or load balancer queue, before the request reaches the Ruby application. + +This functionality is **experimental** and deactivated by default. + +To activate this feature, you must add a ``X-Request-Start`` or ``X-Queue-Start`` header from your web server (i.e. Nginx). The following is an Nginx configuration example: + +``` +# /etc/nginx/conf.d/ruby_service.conf +server { + listen 8080; + + location / { + proxy_set_header X-Request-Start "t=${msec}"; + proxy_pass http://web:3000; + } +} +``` + +Then you must enable the request queuing feature in the integration handling the request. + +For Rack based applications, see the [documentation](#rack) for details for enabling this feature. + ### Processing Pipeline Some applications might require that traces be altered or filtered out before they are sent upstream. The processing pipeline allows users to create *processors* to define such behavior. diff --git a/gemfiles/contrib.gemfile b/gemfiles/contrib.gemfile deleted file mode 100644 index 745020d9ba..0000000000 --- a/gemfiles/contrib.gemfile +++ /dev/null @@ -1,25 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "elasticsearch-transport" -gem "mongo", "< 2.5" -gem "graphql" -gem "grape" -gem "rack" -gem "rack-test" -gem "redis", "< 4.0" -gem "hiredis" -gem "sinatra" -gem "sqlite3" -gem "activerecord", "< 5.1.5" -gem "sidekiq" -gem "aws-sdk" -gem "sucker_punch" -gem "dalli" -gem "resque", "< 2.0" -gem "racecar", ">= 0.3.5" -gem "mysql2", "< 0.5", platform: :ruby - -gemspec path: "../" diff --git a/gemfiles/contrib_old.gemfile b/gemfiles/contrib_old.gemfile deleted file mode 100644 index 61def1225e..0000000000 --- a/gemfiles/contrib_old.gemfile +++ /dev/null @@ -1,24 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "elasticsearch-transport" -gem "mongo", "< 2.5" -gem "redis", "< 4.0" -gem "hiredis" -gem "rack", "1.4.7" -gem "rack-test", "0.7.0" -gem "rack-cache", "1.7.1" -gem "sinatra", "1.4.5" -gem "sqlite3" -gem "activerecord", "3.2.22.5" -gem "sidekiq", "4.0.0" -gem "aws-sdk", "~> 2.0" -gem "sucker_punch" -gem "dalli" -gem "resque", "< 2.0" -gem "mysql2", "0.3.21", platform: :ruby -gem "activerecord-mysql-adapter", platform: :ruby - -gemspec path: "../" diff --git a/gemfiles/rails30_postgres.gemfile b/gemfiles/rails30_postgres.gemfile deleted file mode 100644 index dbb4c57d10..0000000000 --- a/gemfiles/rails30_postgres.gemfile +++ /dev/null @@ -1,12 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "test-unit" -gem "rails", "3.0.20" -gem "pg", "0.15.1", platform: :ruby -gem "activerecord-jdbcpostgresql-adapter", platform: :jruby -gem "rack-cache", "1.7.1" - -gemspec path: "../" diff --git a/gemfiles/rails30_postgres_sidekiq.gemfile b/gemfiles/rails30_postgres_sidekiq.gemfile deleted file mode 100644 index ce19e424bd..0000000000 --- a/gemfiles/rails30_postgres_sidekiq.gemfile +++ /dev/null @@ -1,13 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "test-unit" -gem "rails", "3.0.20" -gem "pg", "0.15.1", platform: :ruby -gem "activerecord-jdbcpostgresql-adapter", platform: :jruby -gem "sidekiq", "4.0.0" -gem "rack-cache", "1.7.1" - -gemspec path: "../" diff --git a/gemfiles/rails32_mysql2.gemfile b/gemfiles/rails32_mysql2.gemfile deleted file mode 100644 index e855420fcd..0000000000 --- a/gemfiles/rails32_mysql2.gemfile +++ /dev/null @@ -1,13 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "test-unit" -gem "rails", "3.2.22.5" -gem "mysql2", "0.3.21", platform: :ruby -gem "activerecord-mysql-adapter", platform: :ruby -gem "activerecord-jdbcmysql-adapter", platform: :jruby -gem "rack-cache", "1.7.1" - -gemspec path: "../" diff --git a/gemfiles/rails32_postgres.gemfile b/gemfiles/rails32_postgres.gemfile deleted file mode 100644 index ed59d33f10..0000000000 --- a/gemfiles/rails32_postgres.gemfile +++ /dev/null @@ -1,12 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "test-unit" -gem "rails", "3.2.22.5" -gem "pg", "0.15.1", platform: :ruby -gem "activerecord-jdbcpostgresql-adapter", platform: :jruby -gem "rack-cache", "1.7.1" - -gemspec path: "../" diff --git a/gemfiles/rails32_postgres_redis.gemfile b/gemfiles/rails32_postgres_redis.gemfile deleted file mode 100644 index 834e228703..0000000000 --- a/gemfiles/rails32_postgres_redis.gemfile +++ /dev/null @@ -1,14 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "test-unit" -gem "rails", "3.2.22.5" -gem "pg", "0.15.1", platform: :ruby -gem "activerecord-jdbcpostgresql-adapter", platform: :jruby -gem "redis-rails" -gem "redis", "< 4.0" -gem "rack-cache", "1.7.1" - -gemspec path: "../" diff --git a/gemfiles/rails32_postgres_sidekiq.gemfile b/gemfiles/rails32_postgres_sidekiq.gemfile deleted file mode 100644 index f1955a1e59..0000000000 --- a/gemfiles/rails32_postgres_sidekiq.gemfile +++ /dev/null @@ -1,13 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "test-unit" -gem "rails", "3.2.22.5" -gem "pg", "0.15.1", platform: :ruby -gem "activerecord-jdbcpostgresql-adapter", platform: :jruby -gem "sidekiq", "4.0.0" -gem "rack-cache", "1.7.1" - -gemspec path: "../" diff --git a/gemfiles/rails4_mysql2.gemfile b/gemfiles/rails4_mysql2.gemfile deleted file mode 100644 index 2f0de14c5f..0000000000 --- a/gemfiles/rails4_mysql2.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "rails", "4.2.7.1" -gem "mysql2", "< 0.5", platform: :ruby -gem "activerecord-jdbcmysql-adapter", platform: :jruby - -gemspec path: "../" diff --git a/gemfiles/rails4_postgres.gemfile b/gemfiles/rails4_postgres.gemfile deleted file mode 100644 index d5733afa7c..0000000000 --- a/gemfiles/rails4_postgres.gemfile +++ /dev/null @@ -1,10 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "rails", "4.2.7.1" -gem "pg", "< 1.0", platform: :ruby -gem "activerecord-jdbcpostgresql-adapter", platform: :jruby - -gemspec path: "../" diff --git a/gemfiles/rails4_postgres_redis.gemfile b/gemfiles/rails4_postgres_redis.gemfile deleted file mode 100644 index e2a1e07281..0000000000 --- a/gemfiles/rails4_postgres_redis.gemfile +++ /dev/null @@ -1,12 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "rails", "4.2.7.1" -gem "pg", "< 1.0", platform: :ruby -gem "activerecord-jdbcpostgresql-adapter", platform: :jruby -gem "redis-rails" -gem "redis", "< 4.0" - -gemspec path: "../" diff --git a/gemfiles/rails4_postgres_sidekiq.gemfile b/gemfiles/rails4_postgres_sidekiq.gemfile deleted file mode 100644 index 3ee856d94d..0000000000 --- a/gemfiles/rails4_postgres_sidekiq.gemfile +++ /dev/null @@ -1,12 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "rails", "4.2.7.1" -gem "pg", "< 1.0", platform: :ruby -gem "activerecord-jdbcpostgresql-adapter", platform: :jruby -gem "sidekiq" -gem "activejob" - -gemspec path: "../" diff --git a/gemfiles/rails5_mysql2.gemfile b/gemfiles/rails5_mysql2.gemfile deleted file mode 100644 index 769964d72a..0000000000 --- a/gemfiles/rails5_mysql2.gemfile +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "rails", "~> 5.1.6" -gem "mysql2", "< 0.5", platform: :ruby - -gemspec path: "../" diff --git a/gemfiles/rails5_postgres.gemfile b/gemfiles/rails5_postgres.gemfile deleted file mode 100644 index e6e8b71c06..0000000000 --- a/gemfiles/rails5_postgres.gemfile +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "rails", "~> 5.1.6" -gem "pg", "< 1.0", platform: :ruby - -gemspec path: "../" diff --git a/gemfiles/rails5_postgres_redis.gemfile b/gemfiles/rails5_postgres_redis.gemfile deleted file mode 100644 index 7cb02d1dbd..0000000000 --- a/gemfiles/rails5_postgres_redis.gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "rails", "~> 5.1.6" -gem "pg", "< 1.0", platform: :ruby -gem "redis-rails" -gem "redis" - -gemspec path: "../" diff --git a/gemfiles/rails5_postgres_sidekiq.gemfile b/gemfiles/rails5_postgres_sidekiq.gemfile deleted file mode 100644 index 6e27dd1322..0000000000 --- a/gemfiles/rails5_postgres_sidekiq.gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "pry-nav", git: "https://github.com/nixme/pry-nav.git", branch: "master" -gem "rails", "~> 5.1.6" -gem "pg", "< 1.0", platform: :ruby -gem "sidekiq" -gem "activejob" - -gemspec path: "../" diff --git a/lib/ddtrace.rb b/lib/ddtrace.rb index eb213184e2..5127c7868e 100644 --- a/lib/ddtrace.rb +++ b/lib/ddtrace.rb @@ -4,6 +4,7 @@ require 'ddtrace/pin' require 'ddtrace/tracer' require 'ddtrace/error' +require 'ddtrace/quantization/hash' require 'ddtrace/quantization/http' require 'ddtrace/pipeline' require 'ddtrace/configuration' @@ -56,18 +57,24 @@ def configure(target = configuration, opts = {}) require 'ddtrace/contrib/base' require 'ddtrace/contrib/rack/patcher' require 'ddtrace/contrib/rails/patcher' +require 'ddtrace/contrib/active_model_serializers/patcher' require 'ddtrace/contrib/active_record/patcher' +require 'ddtrace/contrib/sequel/patcher' require 'ddtrace/contrib/elasticsearch/patcher' require 'ddtrace/contrib/faraday/patcher' require 'ddtrace/contrib/grape/patcher' require 'ddtrace/contrib/graphql/patcher' +require 'ddtrace/contrib/grpc/patcher' require 'ddtrace/contrib/redis/patcher' require 'ddtrace/contrib/http/patcher' require 'ddtrace/contrib/aws/patcher' require 'ddtrace/contrib/sucker_punch/patcher' require 'ddtrace/contrib/mongodb/patcher' require 'ddtrace/contrib/dalli/patcher' +require 'ddtrace/contrib/rake/patcher' require 'ddtrace/contrib/resque/patcher' require 'ddtrace/contrib/racecar/patcher' require 'ddtrace/contrib/sidekiq/patcher' +require 'ddtrace/contrib/excon/patcher' +require 'ddtrace/contrib/mysql2/patcher' require 'ddtrace/monkey' diff --git a/lib/ddtrace/contrib/active_model_serializers/event.rb b/lib/ddtrace/contrib/active_model_serializers/event.rb new file mode 100644 index 0000000000..7aab0ecebb --- /dev/null +++ b/lib/ddtrace/contrib/active_model_serializers/event.rb @@ -0,0 +1,57 @@ +require 'ddtrace/contrib/active_support/notifications/event' + +module Datadog + module Contrib + module ActiveModelSerializers + # Defines basic behaviors for an ActiveModelSerializers event. + module Event + def self.included(base) + base.send(:include, ActiveSupport::Notifications::Event) + base.send(:extend, ClassMethods) + end + + # Class methods for ActiveModelSerializers events. + # Note, they share the same process method and before_trace method. + module ClassMethods + def span_options + { service: configuration[:service_name] } + end + + def tracer + configuration[:tracer] + end + + def configuration + Datadog.configuration[:active_model_serializers] + end + + def process(span, event, _id, payload) + span.service = configuration[:service_name] + + # Set the resource name and serializer name + res = resource(payload[:serializer]) + span.resource = res + span.set_tag('active_model_serializers.serializer', res) + + span.span_type = Datadog::Ext::HTTP::TEMPLATE + + # Will be nil in 0.9 + span.set_tag('active_model_serializers.adapter', payload[:adapter].class) unless payload[:adapter].nil? + end + + private + + def resource(serializer) + # Depending on the version of ActiveModelSerializers + # serializer will be a string or an object. + if serializer.respond_to?(:name) + serializer.name + else + serializer + end + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/active_model_serializers/events.rb b/lib/ddtrace/contrib/active_model_serializers/events.rb new file mode 100644 index 0000000000..aff75e6e48 --- /dev/null +++ b/lib/ddtrace/contrib/active_model_serializers/events.rb @@ -0,0 +1,30 @@ +require 'ddtrace/contrib/active_model_serializers/events/render' +require 'ddtrace/contrib/active_model_serializers/events/serialize' + +module Datadog + module Contrib + module ActiveModelSerializers + # Defines collection of instrumented ActiveModelSerializers events + module Events + ALL = [ + Events::Render, + Events::Serialize + ].freeze + + module_function + + def all + self::ALL + end + + def subscriptions + all.collect(&:subscriptions).collect(&:to_a).flatten + end + + def subscribe! + all.each(&:subscribe!) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/active_model_serializers/events/render.rb b/lib/ddtrace/contrib/active_model_serializers/events/render.rb new file mode 100644 index 0000000000..28f2b198ba --- /dev/null +++ b/lib/ddtrace/contrib/active_model_serializers/events/render.rb @@ -0,0 +1,32 @@ +require 'ddtrace/contrib/active_model_serializers/event' + +module Datadog + module Contrib + module ActiveModelSerializers + module Events + # Defines instrumentation for render.active_model_serializers event + module Render + include ActiveModelSerializers::Event + + EVENT_NAME = 'render.active_model_serializers'.freeze + SPAN_NAME = 'active_model_serializers.render'.freeze + + module_function + + def supported? + Gem.loaded_specs['active_model_serializers'] \ + && Gem.loaded_specs['active_model_serializers'].version >= Gem::Version.new('0.10') + end + + def event_name + self::EVENT_NAME + end + + def span_name + self::SPAN_NAME + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/active_model_serializers/events/serialize.rb b/lib/ddtrace/contrib/active_model_serializers/events/serialize.rb new file mode 100644 index 0000000000..22ce802241 --- /dev/null +++ b/lib/ddtrace/contrib/active_model_serializers/events/serialize.rb @@ -0,0 +1,35 @@ +require 'ddtrace/contrib/active_model_serializers/event' + +module Datadog + module Contrib + module ActiveModelSerializers + module Events + # Defines instrumentation for !serialize.active_model_serializers event + module Serialize + include ActiveModelSerializers::Event + + EVENT_NAME = '!serialize.active_model_serializers'.freeze + SPAN_NAME = 'active_model_serializers.serialize'.freeze + + module_function + + def supported? + Gem.loaded_specs['active_model_serializers'] \ + && ( \ + Gem.loaded_specs['active_model_serializers'].version >= Gem::Version.new('0.9') \ + && Gem.loaded_specs['active_model_serializers'].version < Gem::Version.new('0.10') \ + ) + end + + def event_name + self::EVENT_NAME + end + + def span_name + self::SPAN_NAME + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/active_model_serializers/patcher.rb b/lib/ddtrace/contrib/active_model_serializers/patcher.rb new file mode 100644 index 0000000000..569c443f6d --- /dev/null +++ b/lib/ddtrace/contrib/active_model_serializers/patcher.rb @@ -0,0 +1,62 @@ +require 'ddtrace/ext/app_types' +require 'ddtrace/ext/http' +require 'ddtrace/contrib/active_model_serializers/events' + +module Datadog + module Contrib + module ActiveModelSerializers + # Provides instrumentation for ActiveModelSerializers through ActiveSupport instrumentation signals + module Patcher + include Base + + VERSION_REQUIRED = Gem::Version.new('0.9.0') + + register_as :active_model_serializers + + option :service_name, default: 'active_model_serializers' + option :tracer, default: Datadog.tracer do |value| + (value || Datadog.tracer).tap do |v| + # Make sure to update tracers of all subscriptions + Events.subscriptions.each do |subscription| + subscription.tracer = v + end + end + end + + class << self + def patch + return patched? if patched? || !compatible? + + # Subscribe to ActiveModelSerializers events + Events.subscribe! + + # Set service info + configuration[:tracer].set_service_info( + configuration[:service_name], + 'active_model_serializers', + Ext::AppTypes::WEB + ) + + @patched = true + end + + def patched? + return @patched if defined?(@patched) + @patched = false + end + + private + + def configuration + Datadog.configuration[:active_model_serializers] + end + + def compatible? + Gem.loaded_specs['active_model_serializers'] && Gem.loaded_specs['activesupport'] \ + && Gem.loaded_specs['active_model_serializers'].version >= VERSION_REQUIRED + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/active_record/event.rb b/lib/ddtrace/contrib/active_record/event.rb new file mode 100644 index 0000000000..83704cc2c0 --- /dev/null +++ b/lib/ddtrace/contrib/active_record/event.rb @@ -0,0 +1,30 @@ +require 'ddtrace/contrib/active_support/notifications/event' + +module Datadog + module Contrib + module ActiveRecord + # Defines basic behaviors for an ActiveRecord event. + module Event + def self.included(base) + base.send(:include, ActiveSupport::Notifications::Event) + base.send(:extend, ClassMethods) + end + + # Class methods for ActiveRecord events. + module ClassMethods + def span_options + { service: configuration[:service_name] } + end + + def tracer + configuration[:tracer] + end + + def configuration + Datadog.configuration[:active_record] + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/active_record/events.rb b/lib/ddtrace/contrib/active_record/events.rb new file mode 100644 index 0000000000..c682401591 --- /dev/null +++ b/lib/ddtrace/contrib/active_record/events.rb @@ -0,0 +1,30 @@ +require 'ddtrace/contrib/active_record/events/instantiation' +require 'ddtrace/contrib/active_record/events/sql' + +module Datadog + module Contrib + module ActiveRecord + # Defines collection of instrumented ActiveRecord events + module Events + ALL = [ + Events::Instantiation, + Events::SQL + ].freeze + + module_function + + def all + self::ALL + end + + def subscriptions + all.collect(&:subscriptions).collect(&:to_a).flatten + end + + def subscribe! + all.each(&:subscribe!) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/active_record/events/instantiation.rb b/lib/ddtrace/contrib/active_record/events/instantiation.rb new file mode 100644 index 0000000000..f651f3727b --- /dev/null +++ b/lib/ddtrace/contrib/active_record/events/instantiation.rb @@ -0,0 +1,51 @@ +require 'ddtrace/contrib/active_record/event' + +module Datadog + module Contrib + module ActiveRecord + module Events + # Defines instrumentation for instantiation.active_record event + module Instantiation + include ActiveRecord::Event + + EVENT_NAME = 'instantiation.active_record'.freeze + SPAN_NAME = 'active_record.instantiation'.freeze + DEFAULT_SERVICE_NAME = 'active_record'.freeze + + module_function + + def supported? + Gem.loaded_specs['activerecord'] \ + && Gem.loaded_specs['activerecord'].version >= Gem::Version.new('4.2') + end + + def event_name + self::EVENT_NAME + end + + def span_name + self::SPAN_NAME + end + + def process(span, event, _id, payload) + # Inherit service name from parent, if available. + span.service = if configuration[:orm_service_name] + configuration[:orm_service_name] + elsif span.parent + span.parent.service + else + self::DEFAULT_SERVICE_NAME + end + + span.resource = payload.fetch(:class_name) + span.span_type = 'custom' + span.set_tag('active_record.instantiation.class_name', payload.fetch(:class_name)) + span.set_tag('active_record.instantiation.record_count', payload.fetch(:record_count)) + rescue StandardError => e + Datadog::Tracer.log.debug(e.message) + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/active_record/events/sql.rb b/lib/ddtrace/contrib/active_record/events/sql.rb new file mode 100644 index 0000000000..634ad328b6 --- /dev/null +++ b/lib/ddtrace/contrib/active_record/events/sql.rb @@ -0,0 +1,48 @@ +require 'ddtrace/contrib/active_record/event' + +module Datadog + module Contrib + module ActiveRecord + module Events + # Defines instrumentation for sql.active_record event + module SQL + include ActiveRecord::Event + + EVENT_NAME = 'sql.active_record'.freeze + SPAN_NAME = 'active_record.sql'.freeze + + module_function + + def event_name + self::EVENT_NAME + end + + def span_name + self::SPAN_NAME + end + + def process(span, event, _id, payload) + connection_config = Utils.connection_config(payload[:connection_id]) + span.name = "#{connection_config[:adapter_name]}.query" + span.service = configuration[:service_name] + span.resource = payload.fetch(:sql) + span.span_type = Datadog::Ext::SQL::TYPE + + # Find out if the SQL query has been cached in this request. This meta is really + # helpful to users because some spans may have 0ns of duration because the query + # is simply cached from memory, so the notification is fired with start == finish. + cached = payload[:cached] || (payload[:name] == 'CACHE') + + span.set_tag('active_record.db.vendor', connection_config[:adapter_name]) + span.set_tag('active_record.db.name', connection_config[:database_name]) + span.set_tag('active_record.db.cached', cached) if cached + span.set_tag('out.host', connection_config[:adapter_host]) + span.set_tag('out.port', connection_config[:adapter_port]) + rescue StandardError => e + Datadog::Tracer.log.debug(e.message) + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/active_record/patcher.rb b/lib/ddtrace/contrib/active_record/patcher.rb index f136e5adf2..fb814db042 100644 --- a/lib/ddtrace/contrib/active_record/patcher.rb +++ b/lib/ddtrace/contrib/active_record/patcher.rb @@ -1,7 +1,7 @@ require 'ddtrace/ext/sql' require 'ddtrace/ext/app_types' require 'ddtrace/contrib/active_record/utils' -require 'ddtrace/contrib/active_support/notifications/subscriber' +require 'ddtrace/contrib/active_record/events' module Datadog module Contrib @@ -9,10 +9,6 @@ module ActiveRecord # Patcher enables patching of 'active_record' module. module Patcher include Base - include ActiveSupport::Notifications::Subscriber - - NAME_SQL = 'sql.active_record'.freeze - NAME_INSTANTIATION = 'instantiation.active_record'.freeze register_as :active_record, auto_patch: false option :service_name, depends_on: [:tracer] do |value| @@ -24,7 +20,7 @@ module Patcher option :tracer, default: Datadog.tracer do |value| (value || Datadog.tracer).tap do |v| # Make sure to update tracers of all subscriptions - subscriptions.each do |subscription| + Events.subscriptions.each do |subscription| subscription.tracer = v end end @@ -32,28 +28,6 @@ module Patcher @patched = false - on_subscribe do - # sql.active_record - subscribe( - self::NAME_SQL, # Event name - 'active_record.sql', # Span name - { service: get_option(:service_name) }, # Span options - get_option(:tracer), # Tracer - &method(:sql) # Handler - ) - - # instantiation.active_record - if instantiation_tracing_supported? - subscribe( - self::NAME_INSTANTIATION, # Event name - 'active_record.instantiation', # Span name - { service: get_option(:service_name) }, # Span options - get_option(:tracer), # Tracer - &method(:instantiation) # Handler - ) - end - end - module_function # patched? tells whether patch has been successfully applied @@ -64,7 +38,7 @@ def patched? def patch if !@patched && defined?(::ActiveRecord) begin - subscribe! + Events.subscribe! @patched = true rescue StandardError => e Datadog::Tracer.log.error("Unable to apply Active Record integration: #{e}") @@ -73,50 +47,6 @@ def patch @patched end - - def instantiation_tracing_supported? - Gem.loaded_specs['activerecord'] \ - && Gem.loaded_specs['activerecord'].version >= Gem::Version.new('4.2') - end - - def sql(span, event, _id, payload) - connection_config = Utils.connection_config(payload[:connection_id]) - span.name = "#{connection_config[:adapter_name]}.query" - span.service = get_option(:service_name) - span.resource = payload.fetch(:sql) - span.span_type = Datadog::Ext::SQL::TYPE - - # Find out if the SQL query has been cached in this request. This meta is really - # helpful to users because some spans may have 0ns of duration because the query - # is simply cached from memory, so the notification is fired with start == finish. - cached = payload[:cached] || (payload[:name] == 'CACHE'.freeze) - - span.set_tag('active_record.db.vendor'.freeze, connection_config[:adapter_name]) - span.set_tag('active_record.db.name'.freeze, connection_config[:database_name]) - span.set_tag('active_record.db.cached'.freeze, cached) if cached - span.set_tag('out.host'.freeze, connection_config[:adapter_host]) - span.set_tag('out.port'.freeze, connection_config[:adapter_port]) - rescue StandardError => e - Datadog::Tracer.log.debug(e.message) - end - - def instantiation(span, event, _id, payload) - # Inherit service name from parent, if available. - span.service = if get_option(:orm_service_name) - get_option(:orm_service_name) - elsif span.parent - span.parent.service - else - 'active_record'.freeze - end - - span.resource = payload.fetch(:class_name) - span.span_type = 'custom'.freeze - span.set_tag('active_record.instantiation.class_name'.freeze, payload.fetch(:class_name)) - span.set_tag('active_record.instantiation.record_count'.freeze, payload.fetch(:record_count)) - rescue StandardError => e - Datadog::Tracer.log.debug(e.message) - end end end end diff --git a/lib/ddtrace/contrib/active_record/utils.rb b/lib/ddtrace/contrib/active_record/utils.rb index 00315e463d..3b74b6c044 100644 --- a/lib/ddtrace/contrib/active_record/utils.rb +++ b/lib/ddtrace/contrib/active_record/utils.rb @@ -3,20 +3,6 @@ module Contrib module ActiveRecord # Common utilities for Rails module Utils - # Return a canonical name for a type of database - def self.normalize_vendor(vendor) - case vendor - when nil - 'defaultdb' - when 'postgresql' - 'postgres' - when 'sqlite3' - 'sqlite' - else - vendor - end - end - def self.adapter_name connection_config[:adapter_name] end @@ -36,7 +22,7 @@ def self.adapter_port def self.connection_config(object_id = nil) config = object_id.nil? ? default_connection_config : connection_config_by_id(object_id) { - adapter_name: normalize_vendor(config[:adapter]), + adapter_name: Datadog::Utils::Database.normalize_vendor(config[:adapter]), adapter_host: config[:host], adapter_port: config[:port], database_name: config[:database] diff --git a/lib/ddtrace/contrib/active_support/notifications/event.rb b/lib/ddtrace/contrib/active_support/notifications/event.rb new file mode 100644 index 0000000000..bfcda011ae --- /dev/null +++ b/lib/ddtrace/contrib/active_support/notifications/event.rb @@ -0,0 +1,62 @@ +require 'ddtrace/contrib/active_support/notifications/subscriber' + +module Datadog + module Contrib + module ActiveSupport + module Notifications + # Defines behaviors for an ActiveSupport::Notifications event. + # Compose this into a module or class, then define + # #event_name, #span_name, and #process. You can then + # invoke Event.subscribe! to more easily subscribe to an event. + module Event + def self.included(base) + base.send(:include, Subscriber) + base.send(:extend, ClassMethods) + base.send(:on_subscribe) { base.subscribe } + end + + # Redefines some class behaviors for a Subscriber to make + # it a bit simpler for an Event. + module ClassMethods + def subscribe! + super + end + + def subscription(span_name = nil, options = nil, tracer = nil) + super( + span_name || self.span_name, + options || span_options, + tracer || self.tracer, + &method(:process) + ) + end + + def subscribe(pattern = nil, span_name = nil, options = nil, tracer = nil) + if supported? + super( + pattern || event_name, + span_name || self.span_name, + options || span_options, + tracer || self.tracer, + &method(:process) + ) + end + end + + def supported? + true + end + + def span_options + {} + end + + def tracer + Datadog.tracer + end + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/elasticsearch/quantize.rb b/lib/ddtrace/contrib/elasticsearch/quantize.rb index 1f9d66ce99..8ea7019e2a 100644 --- a/lib/ddtrace/contrib/elasticsearch/quantize.rb +++ b/lib/ddtrace/contrib/elasticsearch/quantize.rb @@ -4,28 +4,26 @@ module Elasticsearch # Quantize contains ES-specific resource quantization tools. module Quantize PLACEHOLDER = '?'.freeze + ID_PLACEHOLDER = '\1?'.freeze EXCLUDE_KEYS = [].freeze SHOW_KEYS = [:_index, :_type, :_id].freeze - DEFAULT_OPTIONS = { exclude: EXCLUDE_KEYS, show: SHOW_KEYS }.freeze - - ID_REGEXP = %r{\/([0-9]+)([\/\?]|$)} - ID_PLACEHOLDER = '/?\2'.freeze - - INDEX_REGEXP = /[0-9]{2,}/ - INDEX_PLACEHOLDER = '?'.freeze + DEFAULT_OPTIONS = { + exclude: EXCLUDE_KEYS, + show: SHOW_KEYS, + placeholder: PLACEHOLDER + }.freeze module_function - # Very basic quantization, complex processing should be done in the agent def format_url(url) - quantized_url = url.gsub(ID_REGEXP, ID_PLACEHOLDER) - quantized_url.gsub(INDEX_REGEXP, INDEX_PLACEHOLDER) + sanitize_fragment_with_id(url) + .gsub(/(?:[\d]+)/, PLACEHOLDER) end def format_body(body, options = {}) format_body!(body, options) rescue StandardError - PLACEHOLDER + options[:placeholder] || PLACEHOLDER end def format_body!(body, options = {}) @@ -36,48 +34,12 @@ def format_body!(body, options = {}) # Parse each statement and quantize them. statements.collect do |string| - reserialize_json(string) do |obj| - format_statement(obj, options) + reserialize_json(string, options[:placeholder]) do |obj| + Datadog::Quantization::Hash.format(obj, options) end end.join("\n") end - def format_statement(statement, options = {}) - return statement if options[:show] == :all - - case statement - when Hash - statement.each_with_object({}) do |(key, value), quantized| - if options[:show].include?(key.to_sym) - quantized[key] = value - elsif !options[:exclude].include?(key.to_sym) - quantized[key] = format_value(value, options) - end - end - else - format_value(statement, options) - end - end - - def format_value(value, options = {}) - return value if options[:show] == :all - - case value - when Hash - format_statement(value, options) - when Array - # If any are objects, format them. - if value.any? { |v| v.class <= Hash || v.class <= Array } - value.collect { |i| format_value(i, options) } - # Otherwise short-circuit and return single placeholder - else - PLACEHOLDER - end - else - PLACEHOLDER - end - end - def merge_options(original, additional) {}.tap do |options| # Show @@ -105,6 +67,13 @@ def reserialize_json(string, fail_value = PLACEHOLDER) fail_value end end + + # Sanitizes URL fragment by changing it to ? whenever a number is detected + # This is meant as simple heuristic that attempts to detect if particular fragment + # represents document Id. This is meant to reduce the cardinality in most frequent cases. + def sanitize_fragment_with_id(url) + url.gsub(%r{^(/?[^/]*/[^/]*/)(?:[^\?/\d]*[\d]+[^\?/]*)}, ID_PLACEHOLDER) + end end end end diff --git a/lib/ddtrace/contrib/excon/middleware.rb b/lib/ddtrace/contrib/excon/middleware.rb new file mode 100644 index 0000000000..8db7270acb --- /dev/null +++ b/lib/ddtrace/contrib/excon/middleware.rb @@ -0,0 +1,139 @@ +require 'excon' +require 'ddtrace/ext/http' +require 'ddtrace/ext/net' +require 'ddtrace/ext/distributed' +require 'ddtrace/propagation/http_propagator' + +module Datadog + module Contrib + module Excon + # Middleware implements an excon-middleware for ddtrace instrumentation + class Middleware < ::Excon::Middleware::Base + SPAN_NAME = 'excon.request'.freeze + DEFAULT_ERROR_HANDLER = lambda do |response| + Ext::HTTP::ERROR_RANGE.cover?(response[:status]) + end + + def initialize(stack, options = {}) + super(stack) + @options = Datadog.configuration[:excon].merge(options) + end + + def request_call(datum) + begin + unless datum.key?(:datadog_span) + tracer.trace(SPAN_NAME).tap do |span| + datum[:datadog_span] = span + annotate!(span, datum) + propagate!(span, datum) if distributed_tracing? + end + end + rescue StandardError => e + Datadog::Tracer.log.debug(e.message) + end + + @stack.request_call(datum) + end + + def response_call(datum) + @stack.response_call(datum).tap do |d| + handle_response(d) + end + end + + def error_call(datum) + handle_response(datum) + @stack.error_call(datum) + end + + # Returns a child class of this trace middleware + # With options given as defaults. + def self.with(options = {}) + Class.new(self) do + @options = options + + # rubocop:disable Style/TrivialAccessors + def self.options + @options + end + + def initialize(stack) + super(stack, self.class.options) + end + end + end + + # Returns a copy of the default stack with the trace middleware injected + def self.around_default_stack + ::Excon.defaults[:middlewares].dup.tap do |default_stack| + # If the default stack contains a version of the trace middleware already... + existing_trace_middleware = default_stack.find { |m| m <= Middleware } + default_stack.delete(existing_trace_middleware) if existing_trace_middleware + + # Inject after the ResponseParser middleware + response_middleware_index = default_stack.index(::Excon::Middleware::ResponseParser).to_i + default_stack.insert(response_middleware_index + 1, self) + end + end + + private + + def tracer + @options[:tracer] + end + + def distributed_tracing? + @options[:distributed_tracing] == true && tracer.enabled + end + + def error_handler + @options[:error_handler] || DEFAULT_ERROR_HANDLER + end + + def split_by_domain? + @options[:split_by_domain] == true + end + + def annotate!(span, datum) + span.resource = datum[:method].to_s.upcase + span.service = service_name(datum) + span.span_type = Ext::HTTP::TYPE + span.set_tag(Ext::HTTP::URL, datum[:path]) + span.set_tag(Ext::HTTP::METHOD, datum[:method].to_s.upcase) + span.set_tag(Ext::NET::TARGET_HOST, datum[:host]) + span.set_tag(Ext::NET::TARGET_PORT, datum[:port].to_s) + end + + def handle_response(datum) + if datum.key?(:datadog_span) + datum[:datadog_span].tap do |span| + return span if span.finished? + + if datum.key?(:response) + response = datum[:response] + if error_handler.call(response) + span.set_error(["Error #{response[:status]}", response[:body]]) + end + span.set_tag(Ext::HTTP::STATUS_CODE, response[:status]) + end + span.set_error(datum[:error]) if datum.key?(:error) + span.finish + datum.delete(:datadog_span) + end + end + rescue StandardError => e + Datadog::Tracer.log.debug(e.message) + end + + def propagate!(span, datum) + Datadog::HTTPPropagator.inject!(span.context, datum[:headers]) + end + + def service_name(datum) + # TODO: Change this to implement more sensible multiplexing + split_by_domain? ? datum[:host] : @options[:service_name] + end + end + end + end +end diff --git a/lib/ddtrace/contrib/excon/patcher.rb b/lib/ddtrace/contrib/excon/patcher.rb new file mode 100644 index 0000000000..861142cc93 --- /dev/null +++ b/lib/ddtrace/contrib/excon/patcher.rb @@ -0,0 +1,50 @@ +require 'ddtrace/ext/app_types' + +module Datadog + module Contrib + module Excon + # Responsible for hooking the instrumentation into Excon + module Patcher + include Base + + DEFAULT_SERVICE = 'excon'.freeze + + register_as :excon + option :tracer, default: Datadog.tracer + option :service_name, default: DEFAULT_SERVICE + option :distributed_tracing, default: false + option :split_by_domain, default: false + option :error_handler, default: nil + + @patched = false + + module_function + + def patch + return @patched if patched? || !compatible? + + require 'ddtrace/contrib/excon/middleware' + + add_middleware + + @patched = true + rescue => e + Tracer.log.error("Unable to apply Excon integration: #{e}") + @patched + end + + def patched? + @patched + end + + def compatible? + defined?(::Excon) + end + + def add_middleware + ::Excon.defaults[:middlewares] = Middleware.around_default_stack + end + end + end + end +end diff --git a/lib/ddtrace/contrib/grpc/datadog_interceptor.rb b/lib/ddtrace/contrib/grpc/datadog_interceptor.rb new file mode 100644 index 0000000000..123f97ed88 --- /dev/null +++ b/lib/ddtrace/contrib/grpc/datadog_interceptor.rb @@ -0,0 +1,65 @@ +module Datadog + module Contrib + module GRPC + # :nodoc: + module DatadogInterceptor + # :nodoc: + class Base < ::GRPC::Interceptor + attr_accessor :datadog_pin + + def initialize(options = {}) + datadog_pin_configuration { |c| yield(c) if block_given? } + end + + def request_response(**keywords) + trace(keywords) { yield } + end + + def client_streamer(**keywords) + trace(keywords) { yield } + end + + def server_streamer(**keywords) + trace(keywords) { yield } + end + + def bidi_streamer(**keywords) + trace(keywords) { yield } + end + + private + + def datadog_pin_configuration + pin = default_datadog_pin + + if block_given? + pin = Pin.new( + pin.service_name, + app: pin.app, + app_type: pin.app_type, + tracer: pin.tracer + ) + + yield(pin) + end + + pin.onto(self) + + pin + end + + def default_datadog_pin + Pin.get_from(::GRPC) + end + + def tracer + datadog_pin.tracer + end + end + + require_relative 'datadog_interceptor/client' + require_relative 'datadog_interceptor/server' + end + end + end +end diff --git a/lib/ddtrace/contrib/grpc/datadog_interceptor/client.rb b/lib/ddtrace/contrib/grpc/datadog_interceptor/client.rb new file mode 100644 index 0000000000..80ca2f85d3 --- /dev/null +++ b/lib/ddtrace/contrib/grpc/datadog_interceptor/client.rb @@ -0,0 +1,49 @@ +module Datadog + module Contrib + module GRPC + module DatadogInterceptor + # The DatadogInterceptor::Client implements the tracing strategy + # for gRPC client-side endpoitns. This middleware compoent will + # inject trace context information into gRPC metadata prior to + # sending the request to the server. + class Client < Base + def trace(keywords) + keywords[:metadata] ||= {} + + options = { + span_type: Datadog::Ext::GRPC::TYPE, + service: datadog_pin.service_name, + resource: format_resource(keywords[:method]) + } + + tracer.trace('grpc.client', options) do |span| + annotate!(span, keywords[:metadata]) + + yield + end + end + + private + + def annotate!(span, metadata) + metadata.each do |header, value| + span.set_tag(header, value) + end + + Datadog::GRPCPropagator + .inject!(span.context, metadata) + rescue StandardError => e + Datadog::Tracer.log.debug("GRPC client trace failed: #{e}") + end + + def format_resource(proto_method) + proto_method.downcase + .split('/') + .reject(&:empty?) + .join('.') + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/grpc/datadog_interceptor/server.rb b/lib/ddtrace/contrib/grpc/datadog_interceptor/server.rb new file mode 100644 index 0000000000..df4954355d --- /dev/null +++ b/lib/ddtrace/contrib/grpc/datadog_interceptor/server.rb @@ -0,0 +1,66 @@ +module Datadog + module Contrib + module GRPC + module DatadogInterceptor + # The DatadogInterceptor::Server implements the tracing strategy + # for gRPC server-side endpoints. When the datadog fields have been + # added to the gRPC call metadata, this middleware component will + # extract any client-side tracing information, attempting to associate + # its tracing context with a parent client-side context + class Server < Base + def trace(keywords) + options = { + span_type: Datadog::Ext::GRPC::TYPE, + service: datadog_pin.service_name, + resource: format_resource(keywords[:method]) + } + metadata = keywords[:call].metadata + + set_distributed_context!(tracer, metadata) + + tracer.trace('grpc.service', options) do |span| + annotate!(span, metadata) + + yield + end + end + + private + + def set_distributed_context!(tracer, metadata) + tracer.provider.context = Datadog::GRPCPropagator + .extract(metadata) + rescue StandardError => e + Datadog::Tracer.log.debug( + "unable to propagate GRPC metadata to context: #{e}" + ) + end + + def annotate!(span, metadata) + metadata.each do |header, value| + next if reserved_headers.include?(header) + span.set_tag(header, value) + end + rescue StandardError => e + Datadog::Tracer.log.debug("GRPC client trace failed: #{e}") + end + + def reserved_headers + [Datadog::Ext::DistributedTracing::GRPC_METADATA_TRACE_ID, + Datadog::Ext::DistributedTracing::GRPC_METADATA_PARENT_ID, + Datadog::Ext::DistributedTracing::GRPC_METADATA_SAMPLING_PRIORITY] + end + + def format_resource(proto_method) + proto_method.owner + .to_s + .downcase + .split('::') + .<<(proto_method.name) + .join('.') + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/grpc/intercept_with_datadog.rb b/lib/ddtrace/contrib/grpc/intercept_with_datadog.rb new file mode 100644 index 0000000000..f1e1c4c0c9 --- /dev/null +++ b/lib/ddtrace/contrib/grpc/intercept_with_datadog.rb @@ -0,0 +1,49 @@ +require_relative 'datadog_interceptor' + +module Datadog + module Contrib + module GRPC + # :nodoc: + # The `#intercept!` method is implemented in gRPC; this module + # will be prepended to the original class, effectively injecting + # our tracing middleware into the head of the call chain. + module InterceptWithDatadog + def intercept!(type, args = {}) + if should_prepend? + datadog_interceptor = choose_datadog_interceptor(args) + + @interceptors.unshift(datadog_interceptor.new) if datadog_interceptor + + @trace_started = true + end + + super + end + + private + + def should_prepend? + !trace_started? && !already_prepended? + end + + def trace_started? + defined?(@trace_started) && @trace_started + end + + def already_prepended? + @interceptors.any? do |interceptor| + interceptor.class.ancestors.include?(Datadog::Contrib::GRPC::DatadogInterceptor::Base) + end + end + + def choose_datadog_interceptor(args) + if args.key?(:metadata) + Datadog::Contrib::GRPC::DatadogInterceptor::Client + elsif args.key?(:call) + Datadog::Contrib::GRPC::DatadogInterceptor::Server + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/grpc/patcher.rb b/lib/ddtrace/contrib/grpc/patcher.rb new file mode 100644 index 0000000000..5c4b761849 --- /dev/null +++ b/lib/ddtrace/contrib/grpc/patcher.rb @@ -0,0 +1,62 @@ +# requirements should be kept minimal as Patcher is a shared requirement. + +module Datadog + module Contrib + module GRPC + SERVICE = 'grpc'.freeze + + # Patcher enables patching of 'grpc' module. + module Patcher + include Base + register_as :grpc, auto_patch: true + option :tracer, default: Datadog.tracer + option :service_name, default: SERVICE + + @patched = false + + module_function + + def patch + return false unless compatible? + return @patched if @patched + + require 'ddtrace/ext/grpc' + require 'ddtrace/propagation/grpc_propagator' + require 'ddtrace/contrib/grpc/datadog_interceptor' + require 'ddtrace/contrib/grpc/intercept_with_datadog' + + add_pin + prepend_interceptor + + @patched = true + rescue StandardError => e + Datadog::Tracer.log.error("Unable to apply gRPC integration: #{e}") + ensure + @patched + end + + def compatible? + defined?(::GRPC::VERSION) && Gem::Version.new(::GRPC::VERSION) >= Gem::Version.new('0.10.0') + end + + def patched? + @patched + end + + def add_pin + Pin.new( + get_option(:service_name), + app: 'grpc', + app_type: 'grpc', + tracer: get_option(:tracer) + ).onto(::GRPC) + end + + def prepend_interceptor + ::GRPC::InterceptionContext + .prepend(Datadog::Contrib::GRPC::InterceptWithDatadog) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/mongodb/parsers.rb b/lib/ddtrace/contrib/mongodb/parsers.rb index 62f213e30d..1647f1f43e 100644 --- a/lib/ddtrace/contrib/mongodb/parsers.rb +++ b/lib/ddtrace/contrib/mongodb/parsers.rb @@ -2,35 +2,37 @@ module Datadog module Contrib # MongoDB module includes classes and functions to instrument MongoDB clients module MongoDB + EXCLUDE_KEYS = [:_id].freeze + SHOW_KEYS = [].freeze + DEFAULT_OPTIONS = { exclude: EXCLUDE_KEYS, show: SHOW_KEYS }.freeze + module_function # skipped keys are related to command names, since they are already # extracted by the query_builder - SKIP_KEYS = [:_id].freeze PLACEHOLDER = '?'.freeze # returns a formatted and normalized query def query_builder(command_name, database_name, command) - # always skip the command name - skip = SKIP_KEYS | [command_name.to_s] - - result = { - operation: command_name, - database: database_name, - collection: command.values.first - } + # always exclude the command name + options = Quantization::Hash.merge_options(quantization_options, exclude: [command_name.to_s]) - command.each do |key, value| - result[key] = quantize_statement(value, skip) unless skip.include?(key) - end + # quantized statements keys are strings to avoid leaking Symbols in older Rubies + # as Symbols are not GC'ed in Rubies prior to 2.2 + base_info = Quantization::Hash.format({ + 'operation' => command_name, + 'database' => database_name, + 'collection' => command.values.first + }, options) - result + base_info.merge(Quantization::Hash.format(command, options)) end # removes the values from the given query; this quantization recursively # replace elements available in a given query, so that Arrays, Hashes and so # on are compacted. It ensures a low cardinality so that it can be used # as a Span resource. + # @deprecated def quantize_statement(statement, skip = []) case statement when Hash @@ -42,6 +44,7 @@ def quantize_statement(statement, skip = []) end end + # @deprecated def quantize_value(value, skip = []) case value when Hash @@ -52,6 +55,14 @@ def quantize_value(value, skip = []) PLACEHOLDER end end + + def quantization_options + Datadog::Quantization::Hash.merge_options(DEFAULT_OPTIONS, configuration[:quantize]) + end + + def configuration + Datadog.configuration[:mongo] + end end end end diff --git a/lib/ddtrace/contrib/mongodb/patcher.rb b/lib/ddtrace/contrib/mongodb/patcher.rb index 1929f07e2e..2290cf2b31 100644 --- a/lib/ddtrace/contrib/mongodb/patcher.rb +++ b/lib/ddtrace/contrib/mongodb/patcher.rb @@ -12,6 +12,7 @@ module Patcher include Base register_as :mongo, auto_patch: true option :service_name, default: SERVICE + option :quantize, default: { show: [:collection, :database, :operation] } @patched = false diff --git a/lib/ddtrace/contrib/mongodb/subscribers.rb b/lib/ddtrace/contrib/mongodb/subscribers.rb index ee148d85a1..8909b6479b 100644 --- a/lib/ddtrace/contrib/mongodb/subscribers.rb +++ b/lib/ddtrace/contrib/mongodb/subscribers.rb @@ -18,14 +18,15 @@ def started(event) Thread.current[:datadog_mongo_span] = span # build a quantized Query using the Parser module - query = Datadog::Contrib::MongoDB.query_builder(event.command_name, event.database_name, event.command) + query = Datadog::Contrib::MongoDB + .query_builder(event.command_name, event.database_name, event.command) serialized_query = query.to_s # add operation tags; the full query is stored and used as a resource, # since it has been quantized and reduced - span.set_tag(Datadog::Ext::Mongo::DB, query[:database]) - span.set_tag(Datadog::Ext::Mongo::COLLECTION, query[:collection]) - span.set_tag(Datadog::Ext::Mongo::OPERATION, query[:operation]) + span.set_tag(Datadog::Ext::Mongo::DB, query['database']) + span.set_tag(Datadog::Ext::Mongo::COLLECTION, query['collection']) + span.set_tag(Datadog::Ext::Mongo::OPERATION, query['operation']) span.set_tag(Datadog::Ext::Mongo::QUERY, serialized_query) span.set_tag(Datadog::Ext::NET::TARGET_HOST, event.address.host) span.set_tag(Datadog::Ext::NET::TARGET_PORT, event.address.port) diff --git a/lib/ddtrace/contrib/mysql2/client.rb b/lib/ddtrace/contrib/mysql2/client.rb new file mode 100644 index 0000000000..91a8498dba --- /dev/null +++ b/lib/ddtrace/contrib/mysql2/client.rb @@ -0,0 +1,60 @@ +require 'ddtrace/ext/sql' +require 'ddtrace/ext/app_types' + +module Datadog + module Contrib + module Mysql2 + # Mysql2::Client patch module + module Client + module_function + + def included(base) + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') + base.class_eval do + alias_method :aliased_query, :query + remove_method :query + include InstanceMethods + end + else + base.send(:prepend, InstanceMethods) + end + end + + # Mysql2::Client patch 1.9.3 instance methods + module InstanceMethodsCompatibility + def query(*args) + aliased_query(*args) + end + end + + # Mysql2::Client patch instance methods + module InstanceMethods + if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0') + include InstanceMethodsCompatibility + end + + def query(sql, options = {}) + datadog_pin.tracer.trace('mysql2.query') do |span| + span.resource = sql + span.service = datadog_pin.service + span.span_type = Datadog::Ext::SQL::TYPE + span.set_tag('mysql2.db.name', query_options[:database]) + span.set_tag('out.host', query_options[:host]) + span.set_tag('out.port', query_options[:port]) + super(sql, options) + end + end + + def datadog_pin + @datadog_pin ||= Datadog::Pin.new( + Datadog.configuration[:mysql2][:service_name], + app: 'mysql2', + app_type: Datadog::Ext::AppTypes::DB, + tracer: Datadog.configuration[:mysql2][:tracer] + ) + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/mysql2/patcher.rb b/lib/ddtrace/contrib/mysql2/patcher.rb new file mode 100644 index 0000000000..27dbe65a39 --- /dev/null +++ b/lib/ddtrace/contrib/mysql2/patcher.rb @@ -0,0 +1,43 @@ +require 'ddtrace/contrib/mysql2/client' + +module Datadog + module Contrib + module Mysql2 + # Mysql2 patcher + module Patcher + include Base + + register_as :mysql2 + option :service_name, default: 'mysql2' + option :tracer, default: Datadog.tracer + + @patched = false + + module_function + + def patch + return @patched if patched? || !compatible? + + patch_mysql2_client + + @patched = true + rescue StandardError => e + Tracer.log.error("Unable to apply mysql2 integration: #{e}") + @patched + end + + def patched? + @patched + end + + def compatible? + defined?(::Mysql2) + end + + def patch_mysql2_client + ::Mysql2::Client.send(:include, Client) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/racecar/event.rb b/lib/ddtrace/contrib/racecar/event.rb new file mode 100644 index 0000000000..d167e9391e --- /dev/null +++ b/lib/ddtrace/contrib/racecar/event.rb @@ -0,0 +1,61 @@ +require 'ddtrace/contrib/active_support/notifications/event' + +module Datadog + module Contrib + module Racecar + # Defines basic behaviors for an ActiveRecord event. + module Event + def self.included(base) + base.send(:include, ActiveSupport::Notifications::Event) + base.send(:extend, ClassMethods) + end + + # Class methods for Racecar events. + # Note, they share the same process method and before_trace method. + module ClassMethods + def subscription(*args) + super.tap do |subscription| + subscription.before_trace { ensure_clean_context! } + end + end + + def span_options + { service: configuration[:service_name] } + end + + def tracer + configuration[:tracer] + end + + def configuration + Datadog.configuration[:racecar] + end + + def process(span, event, _id, payload) + span.service = configuration[:service_name] + span.resource = payload[:consumer_class] + + span.set_tag('kafka.topic', payload[:topic]) + span.set_tag('kafka.consumer', payload[:consumer_class]) + span.set_tag('kafka.partition', payload[:partition]) + span.set_tag('kafka.offset', payload[:offset]) if payload.key?(:offset) + span.set_tag('kafka.first_offset', payload[:first_offset]) if payload.key?(:first_offset) + span.set_tag('kafka.message_count', payload[:message_count]) if payload.key?(:message_count) + span.set_error(payload[:exception_object]) if payload[:exception_object] + end + + private + + # Context objects are thread-bound. + # If Racecar re-uses threads, context from a previous trace + # could leak into the new trace. This "cleans" current context, + # preventing such a leak. + def ensure_clean_context! + return unless configuration[:tracer].call_context.current_span + configuration[:tracer].provider.context = Context.new + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/racecar/events.rb b/lib/ddtrace/contrib/racecar/events.rb new file mode 100644 index 0000000000..f9abc604d3 --- /dev/null +++ b/lib/ddtrace/contrib/racecar/events.rb @@ -0,0 +1,30 @@ +require 'ddtrace/contrib/racecar/events/batch' +require 'ddtrace/contrib/racecar/events/message' + +module Datadog + module Contrib + module Racecar + # Defines collection of instrumented Racecar events + module Events + ALL = [ + Events::Batch, + Events::Message + ].freeze + + module_function + + def all + self::ALL + end + + def subscriptions + all.collect(&:subscriptions).collect(&:to_a).flatten + end + + def subscribe! + all.each(&:subscribe!) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/racecar/events/batch.rb b/lib/ddtrace/contrib/racecar/events/batch.rb new file mode 100644 index 0000000000..96b6a276d4 --- /dev/null +++ b/lib/ddtrace/contrib/racecar/events/batch.rb @@ -0,0 +1,27 @@ +require 'ddtrace/contrib/racecar/event' + +module Datadog + module Contrib + module Racecar + module Events + # Defines instrumentation for process_batch.racecar event + module Batch + include Racecar::Event + + EVENT_NAME = 'process_batch.racecar'.freeze + SPAN_NAME = 'racecar.batch'.freeze + + module_function + + def event_name + self::EVENT_NAME + end + + def span_name + self::SPAN_NAME + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/racecar/events/message.rb b/lib/ddtrace/contrib/racecar/events/message.rb new file mode 100644 index 0000000000..85901b7242 --- /dev/null +++ b/lib/ddtrace/contrib/racecar/events/message.rb @@ -0,0 +1,27 @@ +require 'ddtrace/contrib/racecar/event' + +module Datadog + module Contrib + module Racecar + module Events + # Defines instrumentation for process_message.racecar event + module Message + include Racecar::Event + + EVENT_NAME = 'process_message.racecar'.freeze + SPAN_NAME = 'racecar.message'.freeze + + module_function + + def event_name + self::EVENT_NAME + end + + def span_name + self::SPAN_NAME + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/racecar/patcher.rb b/lib/ddtrace/contrib/racecar/patcher.rb index 71df0968bd..fd190cfa4a 100644 --- a/lib/ddtrace/contrib/racecar/patcher.rb +++ b/lib/ddtrace/contrib/racecar/patcher.rb @@ -1,5 +1,5 @@ require 'ddtrace/ext/app_types' -require 'ddtrace/contrib/active_support/notifications/subscriber' +require 'ddtrace/contrib/racecar/events' module Datadog module Contrib @@ -7,50 +7,26 @@ module Racecar # Provides instrumentation for `racecar` through ActiveSupport instrumentation signals module Patcher include Base - include ActiveSupport::Notifications::Subscriber - NAME_MESSAGE = 'racecar.message'.freeze - NAME_BATCH = 'racecar.batch'.freeze register_as :racecar option :service_name, default: 'racecar' option :tracer, default: Datadog.tracer do |value| (value || Datadog.tracer).tap do |v| # Make sure to update tracers of all subscriptions - subscriptions.each do |subscription| + Events.subscriptions.each do |subscription| subscription.tracer = v end end end - on_subscribe do - # Subscribe to single messages - subscription( - self::NAME_MESSAGE, - { service: configuration[:service_name] }, - configuration[:tracer], - &method(:process) - ).tap do |subscription| - subscription.before_trace { ensure_clean_context! } - subscription.subscribe('process_message.racecar') - end - - # Subscribe to batch messages - subscription( - self::NAME_BATCH, - { service: configuration[:service_name] }, - configuration[:tracer], - &method(:process) - ).tap do |subscription| - subscription.before_trace { ensure_clean_context! } - subscription.subscribe('process_batch.racecar') - end - end - class << self def patch return patched? if patched? || !compatible? - subscribe! + # Subscribe to Racecar events + Events.subscribe! + + # Set service info configuration[:tracer].set_service_info( configuration[:service_name], 'racecar', @@ -65,19 +41,6 @@ def patched? @patched = false end - def process(span, event, _, payload) - span.service = configuration[:service_name] - span.resource = payload[:consumer_class] - - span.set_tag('kafka.topic', payload[:topic]) - span.set_tag('kafka.consumer', payload[:consumer_class]) - span.set_tag('kafka.partition', payload[:partition]) - span.set_tag('kafka.offset', payload[:offset]) if payload.key?(:offset) - span.set_tag('kafka.first_offset', payload[:first_offset]) if payload.key?(:first_offset) - span.set_tag('kafka.message_count', payload[:message_count]) if payload.key?(:message_count) - span.set_error(payload[:exception_object]) if payload[:exception_object] - end - private def configuration @@ -87,15 +50,6 @@ def configuration def compatible? defined?(::Racecar) && defined?(::ActiveSupport::Notifications) end - - # Context objects are thread-bound. - # If Racecar re-uses threads, context from a previous trace - # could leak into the new trace. This "cleans" current context, - # preventing such a leak. - def ensure_clean_context! - return unless configuration[:tracer].call_context.current_span - configuration[:tracer].provider.context = Context.new - end end end end diff --git a/lib/ddtrace/contrib/rack/middlewares.rb b/lib/ddtrace/contrib/rack/middlewares.rb index d071072c9e..9e164aa457 100644 --- a/lib/ddtrace/contrib/rack/middlewares.rb +++ b/lib/ddtrace/contrib/rack/middlewares.rb @@ -1,6 +1,7 @@ require 'ddtrace/ext/app_types' require 'ddtrace/ext/http' require 'ddtrace/propagation/http_propagator' +require 'ddtrace/contrib/rack/request_queue' module Datadog module Contrib @@ -19,10 +20,28 @@ def initialize(app) @app = app end + def compute_queue_time(env, tracer) + return unless Datadog.configuration[:rack][:request_queuing] + + # parse the request queue time + request_start = Datadog::Contrib::Rack::QueueTime.get_request_start(env) + return if request_start.nil? + + tracer.trace( + 'http_server.queue', + start_time: request_start, + service: Datadog.configuration[:rack][:web_service_name] + ) + end + def call(env) # retrieve integration settings tracer = Datadog.configuration[:rack][:tracer] + # [experimental] create a root Span to keep track of frontend web servers + # (i.e. Apache, nginx) if the header is properly set + frontend_span = compute_queue_time(env, tracer) + trace_options = { service: Datadog.configuration[:rack][:service_name], resource: nil, @@ -77,6 +96,7 @@ def call(env) # ensure the request_span is finished and the context reset; # this assumes that the Rack middleware creates a root span request_span.finish + frontend_span.finish unless frontend_span.nil? # TODO: Remove this once we change how context propagation works. This # ensures we clean thread-local variables on each HTTP request avoiding @@ -105,7 +125,8 @@ def set_request_tags!(request_span, env, status, headers, response, original_env # So when its not available, we want the original, unmutated PATH_INFO, which # is just the relative path without query strings. url = env['REQUEST_URI'] || original_env['PATH_INFO'] - request_id = get_request_id(headers, env) + request_headers = parse_request_headers(env) + response_headers = parse_response_headers(headers || {}) request_span.resource ||= resource_name_for(env, status) if request_span.get_tag(Datadog::Ext::HTTP::METHOD).nil? @@ -130,8 +151,15 @@ def set_request_tags!(request_span, env, status, headers, response, original_env if request_span.get_tag(Datadog::Ext::HTTP::STATUS_CODE).nil? && status request_span.set_tag(Datadog::Ext::HTTP::STATUS_CODE, status) end - if request_span.get_tag(Datadog::Ext::HTTP::REQUEST_ID).nil? && request_id - request_span.set_tag(Datadog::Ext::HTTP::REQUEST_ID, request_id) + + # Request headers + request_headers.each do |name, value| + request_span.set_tag(name, value) if request_span.get_tag(name).nil? + end + + # Response headers + response_headers.each do |name, value| + request_span.set_tag(name, value) if request_span.get_tag(name).nil? end # detect if the status code is a 5xx and flag the request span as an error @@ -141,14 +169,6 @@ def set_request_tags!(request_span, env, status, headers, response, original_env end end - # If Rails is present, it will sanitize & use the Request ID header, - # or generate a UUID if no request ID header is present, then set that as headers['X-Request-Id']. - # Othewise use whatever Rack variables are present (they should all be the same.) - def get_request_id(headers, env) - headers ||= {} - headers['X-Request-Id'] || headers['X-Request-ID'] || env['HTTP_X_REQUEST_ID'] - end - private REQUEST_SPAN_DEPRECATION_WARNING = %( @@ -176,6 +196,40 @@ def []=(key, value) end end end + + def parse_request_headers(env) + {}.tap do |result| + whitelist = Datadog.configuration[:rack][:headers][:request] || [] + whitelist.each do |header| + rack_header = header_to_rack_header(header) + if env.key?(rack_header) + result[Datadog::Ext::HTTP::RequestHeaders.to_tag(header)] = env[rack_header] + end + end + end + end + + def parse_response_headers(headers) + {}.tap do |result| + whitelist = Datadog.configuration[:rack][:headers][:response] || [] + whitelist.each do |header| + if headers.key?(header) + result[Datadog::Ext::HTTP::ResponseHeaders.to_tag(header)] = headers[header] + else + # Try a case-insensitive lookup + uppercased_header = header.to_s.upcase + matching_header = headers.keys.find { |h| h.upcase == uppercased_header } + if matching_header + result[Datadog::Ext::HTTP::ResponseHeaders.to_tag(header)] = headers[matching_header] + end + end + end + end + end + + def header_to_rack_header(name) + "HTTP_#{name.to_s.upcase.gsub(/[-\s]/, '_')}" + end end end end diff --git a/lib/ddtrace/contrib/rack/patcher.rb b/lib/ddtrace/contrib/rack/patcher.rb index 50649916be..9bde547e54 100644 --- a/lib/ddtrace/contrib/rack/patcher.rb +++ b/lib/ddtrace/contrib/rack/patcher.rb @@ -4,6 +4,14 @@ module Rack # Provides instrumentation for `rack` module Patcher include Base + + DEFAULT_HEADERS = { + response: [ + 'Content-Type', + 'X-Request-ID' + ] + }.freeze + register_as :rack option :tracer, default: Datadog.tracer option :distributed_tracing, default: false @@ -14,6 +22,14 @@ module Patcher get_option(:tracer).set_service_info(value, 'rack', Ext::AppTypes::WEB) value end + option :request_queuing, default: false + option :web_service_name, default: 'web-server', depends_on: [:tracer, :request_queuing] do |value| + if get_option(:request_queuing) + get_option(:tracer).set_service_info(value, 'webserver', Ext::AppTypes::WEB) + end + value + end + option :headers, default: DEFAULT_HEADERS module_function diff --git a/lib/ddtrace/contrib/rack/request_queue.rb b/lib/ddtrace/contrib/rack/request_queue.rb new file mode 100644 index 0000000000..4eb97be5a8 --- /dev/null +++ b/lib/ddtrace/contrib/rack/request_queue.rb @@ -0,0 +1,34 @@ +module Datadog + module Contrib + module Rack + # QueueTime simply... + module QueueTime + REQUEST_START = 'HTTP_X_REQUEST_START'.freeze + QUEUE_START = 'HTTP_X_QUEUE_START'.freeze + + module_function + + def get_request_start(env, now = Time.now.utc) + header = env[REQUEST_START] || env[QUEUE_START] + return unless header + + # nginx header is in the format "t=1512379167.574" + # TODO: this should be generic enough to work with any + # frontend web server or load balancer + time_string = header.split('t=')[1] + return if time_string.nil? + + # return the request_start only if it's lesser than + # current time, to avoid significant clock skew + request_start = Time.at(time_string.to_f) + request_start.utc > now ? nil : request_start + rescue StandardError => e + # in case of an Exception we don't create a + # `request.queuing` span + Datadog::Tracer.log.debug("[rack] unable to parse request queue headers: #{e}") + nil + end + end + end + end +end diff --git a/lib/ddtrace/contrib/rake/instrumentation.rb b/lib/ddtrace/contrib/rake/instrumentation.rb new file mode 100644 index 0000000000..fe6be8ec14 --- /dev/null +++ b/lib/ddtrace/contrib/rake/instrumentation.rb @@ -0,0 +1,70 @@ +module Datadog + module Contrib + module Rake + # Instrumentation for Rake tasks + module Instrumentation + SPAN_NAME_INVOKE = 'rake.invoke'.freeze + SPAN_NAME_EXECUTE = 'rake.execute'.freeze + + def self.included(base) + base.send(:prepend, InstanceMethods) + end + + # Instance methods for Rake instrumentation + module InstanceMethods + def invoke(*args) + return super unless enabled? + + tracer.trace(SPAN_NAME_INVOKE) do |span| + super + annotate_invoke!(span, args) + end + end + + def execute(args = nil) + return super unless enabled? + + tracer.trace(SPAN_NAME_EXECUTE) do |span| + super + annotate_execute!(span, args) + end + end + + private + + def annotate_invoke!(span, args) + span.resource = name + span.set_tag('rake.task.arg_names', arg_names) + span.set_tag('rake.invoke.args', quantize_args(args)) unless args.nil? + rescue StandardError => e + Datadog::Tracer.log.debug("Error while tracing Rake invoke: #{e.message}") + end + + def annotate_execute!(span, args) + span.resource = name + span.set_tag('rake.execute.args', quantize_args(args.to_hash)) unless args.nil? + rescue StandardError => e + Datadog::Tracer.log.debug("Error while tracing Rake execute: #{e.message}") + end + + def quantize_args(args) + quantize_options = Datadog.configuration[:rake][:quantize][:args] + Datadog::Quantization::Hash.format(args, quantize_options) + end + + def enabled? + configuration[:enabled] == true + end + + def tracer + configuration[:tracer] + end + + def configuration + Datadog.configuration[:rake] + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/rake/patcher.rb b/lib/ddtrace/contrib/rake/patcher.rb new file mode 100644 index 0000000000..aac1272897 --- /dev/null +++ b/lib/ddtrace/contrib/rake/patcher.rb @@ -0,0 +1,53 @@ +require 'ddtrace/ext/app_types' +require 'ddtrace/contrib/rake/instrumentation' + +module Datadog + module Contrib + module Rake + # Patcher for Rake instrumentation + module Patcher + include Base + + register_as :rake + option :service_name, default: 'rake' + option :tracer, default: Datadog.tracer + option :enabled, default: true + option :quantize, default: {} + + module_function + + def patch + return patched? if patched? || !compatible? + + patch_rake + + # Set service info + configuration[:tracer].set_service_info( + configuration[:service_name], + 'rake', + Ext::AppTypes::WORKER + ) + + @patched = true + end + + def patched? + return @patched if defined?(@patched) + @patched = false + end + + def patch_rake + ::Rake::Task.send(:include, Instrumentation) + end + + def compatible? + RUBY_VERSION >= '2.0.0' && defined?(::Rake) + end + + def configuration + Datadog.configuration[:rake] + end + end + end + end +end diff --git a/lib/ddtrace/contrib/sequel/database.rb b/lib/ddtrace/contrib/sequel/database.rb new file mode 100644 index 0000000000..3adbd5f103 --- /dev/null +++ b/lib/ddtrace/contrib/sequel/database.rb @@ -0,0 +1,58 @@ +require 'ddtrace/ext/sql' +require 'ddtrace/ext/app_types' +require 'ddtrace/contrib/sequel/utils' + +module Datadog + module Contrib + module Sequel + # Adds instrumentation to Sequel::Database + module Database + def self.included(base) + base.send(:prepend, InstanceMethods) + end + + # Instance methods for instrumenting Sequel::Database + module InstanceMethods + def run(sql, options = ::Sequel::OPTS) + opts = parse_opts(sql, options) + + response = nil + + datadog_pin.tracer.trace('sequel.query') do |span| + span.service = datadog_pin.service + span.resource = opts[:query] + span.span_type = Datadog::Ext::SQL::TYPE + span.set_tag('sequel.db.vendor', adapter_name) + response = super(sql, options) + end + response + end + + def datadog_pin + @pin ||= Datadog::Pin.new( + Datadog.configuration[:sequel][:service_name] || adapter_name, + app: Patcher::APP, + app_type: Datadog::Ext::AppTypes::DB, + tracer: Datadog.configuration[:sequel][:tracer] || Datadog.tracer + ) + end + + private + + def adapter_name + Utils.adapter_name(self) + end + + def parse_opts(sql, opts) + db_opts = if ::Sequel::VERSION < '3.41.0' && self.class.to_s !~ /Dataset$/ + @opts + elsif instance_variable_defined?(:@pool) && @pool + @pool.db.opts + end + Utils.parse_opts(sql, opts, db_opts) + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/sequel/dataset.rb b/lib/ddtrace/contrib/sequel/dataset.rb new file mode 100644 index 0000000000..4cde0a0c8f --- /dev/null +++ b/lib/ddtrace/contrib/sequel/dataset.rb @@ -0,0 +1,59 @@ +require 'ddtrace/ext/sql' +require 'ddtrace/ext/app_types' +require 'ddtrace/contrib/sequel/utils' + +module Datadog + module Contrib + module Sequel + # Adds instrumentation to Sequel::Dataset + module Dataset + def self.included(base) + base.send(:prepend, InstanceMethods) + end + + # Instance methods for instrumenting Sequel::Dataset + module InstanceMethods + def execute(sql, options = ::Sequel::OPTS, &block) + trace_execute(proc { super(sql, options, &block) }, sql, options, &block) + end + + def execute_ddl(sql, options = ::Sequel::OPTS, &block) + trace_execute(proc { super(sql, options, &block) }, sql, options, &block) + end + + def execute_dui(sql, options = ::Sequel::OPTS, &block) + trace_execute(proc { super(sql, options, &block) }, sql, options, &block) + end + + def execute_insert(sql, options = ::Sequel::OPTS, &block) + trace_execute(proc { super(sql, options, &block) }, sql, options, &block) + end + + def datadog_pin + Datadog::Pin.get_from(db) + end + + private + + def trace_execute(super_method, sql, options, &block) + opts = Utils.parse_opts(sql, options, db.opts) + response = nil + + datadog_pin.tracer.trace('sequel.query') do |span| + span.service = datadog_pin.service + span.resource = opts[:query] + span.span_type = Datadog::Ext::SQL::TYPE + span.set_tag('sequel.db.vendor', adapter_name) + response = super_method.call(sql, options, &block) + end + response + end + + def adapter_name + Utils.adapter_name(db) + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/sequel/patcher.rb b/lib/ddtrace/contrib/sequel/patcher.rb new file mode 100644 index 0000000000..978d2e2030 --- /dev/null +++ b/lib/ddtrace/contrib/sequel/patcher.rb @@ -0,0 +1,56 @@ +require 'ddtrace/contrib/sequel/database' +require 'ddtrace/contrib/sequel/dataset' + +module Datadog + module Contrib + module Sequel + # Patcher enables patching of 'sequel' module. + # This is used in monkey.rb to manually apply patches + module Patcher + include Base + + APP = 'sequel'.freeze + + register_as :sequel, auto_patch: false + option :service_name + option :tracer, default: Datadog.tracer + + @patched = false + + module_function + + # patched? tells whether patch has been successfully applied + def patched? + @patched + end + + def patch + if !@patched && compatible? + begin + patch_sequel_database + patch_sequel_dataset + + @patched = true + rescue StandardError => e + Datadog::Tracer.log.error("Unable to apply Sequel integration: #{e}") + end + end + + @patched + end + + def compatible? + RUBY_VERSION >= '2.0.0' && defined?(::Sequel) + end + + def patch_sequel_database + ::Sequel::Database.send(:include, Database) + end + + def patch_sequel_dataset + ::Sequel::Dataset.send(:include, Dataset) + end + end + end + end +end diff --git a/lib/ddtrace/contrib/sequel/utils.rb b/lib/ddtrace/contrib/sequel/utils.rb new file mode 100644 index 0000000000..8e1a3111ef --- /dev/null +++ b/lib/ddtrace/contrib/sequel/utils.rb @@ -0,0 +1,28 @@ +module Datadog + module Contrib + module Sequel + # General purpose functions for Sequel + module Utils + module_function + + def adapter_name(database) + Datadog::Utils::Database.normalize_vendor(database.adapter_scheme.to_s) + end + + def parse_opts(sql, opts, db_opts) + if ::Sequel::VERSION >= '4.37.0' && !sql.is_a?(String) + # In 4.37.0, sql was converted to a prepared statement object + sql = sql.prepared_sql unless sql.is_a?(Symbol) + end + + { + name: opts[:type], + query: sql, + database: db_opts[:database], + host: db_opts[:host] + } + end + end + end + end +end diff --git a/lib/ddtrace/contrib/sidekiq/tracer.rb b/lib/ddtrace/contrib/sidekiq/tracer.rb index b982ade1f9..267353c0bc 100644 --- a/lib/ddtrace/contrib/sidekiq/tracer.rb +++ b/lib/ddtrace/contrib/sidekiq/tracer.rb @@ -32,6 +32,7 @@ def call(worker, job, queue) span.set_tag('sidekiq.job.retry', job['retry']) span.set_tag('sidekiq.job.queue', job['queue']) span.set_tag('sidekiq.job.wrapper', job['class']) if job['wrapped'] + span.set_tag('sidekiq.job.delay', 1000.0 * (Time.now.utc.to_f - job['enqueued_at'].to_f)) yield end diff --git a/lib/ddtrace/contrib/sinatra/env.rb b/lib/ddtrace/contrib/sinatra/env.rb new file mode 100644 index 0000000000..097dc6040d --- /dev/null +++ b/lib/ddtrace/contrib/sinatra/env.rb @@ -0,0 +1,39 @@ +require 'ddtrace/ext/http' + +module Datadog + module Contrib + module Sinatra + # Gets and sets trace information from a Rack Env + module Env + ENV_SPAN = 'datadog.sinatra_request_span'.freeze + + module_function + + def datadog_span(env) + env[ENV_SPAN] + end + + def set_datadog_span(env, span) + env[ENV_SPAN] = span + end + + def request_header_tags(env, headers) + headers ||= [] + + {}.tap do |result| + headers.each do |header| + rack_header = header_to_rack_header(header) + if env.key?(rack_header) + result[Datadog::Ext::HTTP::RequestHeaders.to_tag(header)] = env[rack_header] + end + end + end + end + + def header_to_rack_header(name) + "HTTP_#{name.to_s.upcase.gsub(/[-\s]/, '_')}" + end + end + end + end +end diff --git a/lib/ddtrace/contrib/sinatra/headers.rb b/lib/ddtrace/contrib/sinatra/headers.rb new file mode 100644 index 0000000000..f77764f2d8 --- /dev/null +++ b/lib/ddtrace/contrib/sinatra/headers.rb @@ -0,0 +1,31 @@ +require 'ddtrace/ext/http' + +module Datadog + module Contrib + module Sinatra + # Gets and sets trace information from a Rack headers Hash + module Headers + module_function + + def response_header_tags(headers, target_headers) + target_headers ||= [] + + {}.tap do |result| + target_headers.each do |header| + if headers.key?(header) + result[Datadog::Ext::HTTP::ResponseHeaders.to_tag(header)] = headers[header] + else + # Try a case-insensitive lookup + uppercased_header = header.to_s.upcase + matching_header = headers.keys.find { |h| h.upcase == uppercased_header } + if matching_header + result[Datadog::Ext::HTTP::ResponseHeaders.to_tag(header)] = headers[matching_header] + end + end + end + end + end + end + end + end +end diff --git a/lib/ddtrace/contrib/sinatra/tracer.rb b/lib/ddtrace/contrib/sinatra/tracer.rb index a96354ef4f..ec27018c34 100644 --- a/lib/ddtrace/contrib/sinatra/tracer.rb +++ b/lib/ddtrace/contrib/sinatra/tracer.rb @@ -1,4 +1,3 @@ - require 'sinatra/base' require 'ddtrace/ext/app_types' @@ -6,11 +5,14 @@ require 'ddtrace/ext/http' require 'ddtrace/propagation/http_propagator' +require 'ddtrace/contrib/sinatra/tracer_middleware' +require 'ddtrace/contrib/sinatra/env' + sinatra_vs = Gem::Version.new(Sinatra::VERSION) sinatra_min_vs = Gem::Version.new('1.4.0') if sinatra_vs < sinatra_min_vs raise "sinatra version #{sinatra_vs} is not supported yet " \ - + "(supporting versions >=#{sinatra_min_vs})" + + "(supporting versions >=#{sinatra_min_vs})" end Datadog::Tracer.log.info("activating instrumentation for sinatra #{sinatra_vs}") @@ -21,6 +23,10 @@ module Sinatra # Datadog::Contrib::Sinatra::Tracer is a Sinatra extension which traces # requests. module Tracer + DEFAULT_HEADERS = { + response: %w[Content-Type X-Request-ID] + }.freeze + include Base register_as :sinatra @@ -32,6 +38,7 @@ module Tracer option :tracer, default: Datadog.tracer option :resource_script_names, default: false option :distributed_tracing, default: false + option :headers, default: DEFAULT_HEADERS def route(verb, action, *) # Keep track of the route name when the app is instantiated for an @@ -49,8 +56,6 @@ def route(verb, action, *) super end - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/MethodLength def self.registered(app) ::Sinatra::Base.module_eval do def render(engine, data, *) @@ -71,52 +76,30 @@ def render(engine, data, *) end end + app.use TracerMiddleware + app.before do return unless Datadog.configuration[:sinatra][:tracer].enabled - if instance_variable_defined? :@datadog_request_span - if @datadog_request_span - Datadog::Tracer.log.error('request span active in :before hook') - @datadog_request_span.finish() - @datadog_request_span = nil - end - end - - tracer = Datadog.configuration[:sinatra][:tracer] - distributed_tracing = Datadog.configuration[:sinatra][:distributed_tracing] - - if distributed_tracing && tracer.provider.context.trace_id.nil? - context = HTTPPropagator.extract(request.env) - tracer.provider.context = context if context.trace_id - end - - span = tracer.trace('sinatra.request', - service: Datadog.configuration[:sinatra][:service_name], - span_type: Datadog::Ext::HTTP::TYPE) + span = Sinatra::Env.datadog_span(env) span.set_tag(Datadog::Ext::HTTP::URL, request.path) span.set_tag(Datadog::Ext::HTTP::METHOD, request.request_method) - - @datadog_request_span = span end app.after do return unless Datadog.configuration[:sinatra][:tracer].enabled - span = @datadog_request_span - begin - unless span - Datadog::Tracer.log.error('missing request span in :after hook') - return - end + span = Sinatra::Env.datadog_span(env) - span.resource = "#{request.request_method} #{@datadog_route}" - span.set_tag('sinatra.route.path', @datadog_route) - span.set_tag(Datadog::Ext::HTTP::STATUS_CODE, response.status) - span.set_error(env['sinatra.error']) if response.server_error? - span.finish() - ensure - @datadog_request_span = nil + unless span + Datadog::Tracer.log.error('missing request span in :after hook') + return end + + span.resource = "#{request.request_method} #{@datadog_route}" + span.set_tag('sinatra.route.path', @datadog_route) + span.set_tag(Datadog::Ext::HTTP::STATUS_CODE, response.status) + span.set_error(env['sinatra.error']) if response.server_error? end end end diff --git a/lib/ddtrace/contrib/sinatra/tracer_middleware.rb b/lib/ddtrace/contrib/sinatra/tracer_middleware.rb new file mode 100644 index 0000000000..5b72340849 --- /dev/null +++ b/lib/ddtrace/contrib/sinatra/tracer_middleware.rb @@ -0,0 +1,61 @@ +require 'ddtrace/contrib/sinatra/env' +require 'ddtrace/contrib/sinatra/headers' + +module Datadog + module Contrib + module Sinatra + # Middleware used for automatically tagging configured headers and handle request span + class TracerMiddleware + REQUEST_TRACE_NAME = 'sinatra.request'.freeze + + def initialize(app) + @app = app + end + + def call(env) + # Set the trace context (e.g. distributed tracing) + if configuration[:distributed_tracing] && tracer.provider.context.trace_id.nil? + context = HTTPPropagator.extract(env) + tracer.provider.context = context if context.trace_id + end + + # Begin the trace + tracer.trace( + REQUEST_TRACE_NAME, + service: configuration[:service_name], + span_type: Datadog::Ext::HTTP::TYPE + ) do |span| + Sinatra::Env.set_datadog_span(env, span) + + Sinatra::Env.request_header_tags(env, configuration[:headers][:request]).each do |name, value| + span.set_tag(name, value) if span.get_tag(name).nil? + end + + # Run application stack + status, headers, response_body = @app.call(env) + + Sinatra::Headers.response_header_tags(headers, configuration[:headers][:response]).each do |name, value| + span.set_tag(name, value) if span.get_tag(name).nil? + end + + [status, headers, response_body] + end + end + + private + + def tracer + configuration[:tracer] + end + + def configuration + Datadog.configuration[:sinatra] + end + + def header_to_rack_header(name) + "HTTP_#{name.to_s.upcase.gsub(/[-\s]/, '_')}" + end + end + end + end +end diff --git a/lib/ddtrace/ext/distributed.rb b/lib/ddtrace/ext/distributed.rb index 687a64d0e7..533ac921d4 100644 --- a/lib/ddtrace/ext/distributed.rb +++ b/lib/ddtrace/ext/distributed.rb @@ -7,6 +7,11 @@ module DistributedTracing HTTP_HEADER_PARENT_ID = 'x-datadog-parent-id'.freeze HTTP_HEADER_SAMPLING_PRIORITY = 'x-datadog-sampling-priority'.freeze SAMPLING_PRIORITY_KEY = '_sampling_priority_v1'.freeze + + # gRPC metadata keys for distributed tracing. https://github.com/grpc/grpc-go/blob/v1.10.x/Documentation/grpc-metadata.md + GRPC_METADATA_TRACE_ID = 'x-datadog-trace-id'.freeze + GRPC_METADATA_PARENT_ID = 'x-datadog-parent-id'.freeze + GRPC_METADATA_SAMPLING_PRIORITY = 'x-datadog-sampling-priority'.freeze end end end diff --git a/lib/ddtrace/ext/grpc.rb b/lib/ddtrace/ext/grpc.rb new file mode 100644 index 0000000000..e55131fc95 --- /dev/null +++ b/lib/ddtrace/ext/grpc.rb @@ -0,0 +1,7 @@ +module Datadog + module Ext + module GRPC + TYPE = 'grpc'.freeze + end + end +end diff --git a/lib/ddtrace/ext/http.rb b/lib/ddtrace/ext/http.rb index 503c3f4bdc..461160e13c 100644 --- a/lib/ddtrace/ext/http.rb +++ b/lib/ddtrace/ext/http.rb @@ -1,14 +1,44 @@ module Datadog module Ext module HTTP - TYPE = 'http'.freeze - TEMPLATE = 'template'.freeze - URL = 'http.url'.freeze BASE_URL = 'http.base_url'.freeze + ERROR_RANGE = 500...600 METHOD = 'http.method'.freeze - REQUEST_ID = 'http.request_id'.freeze STATUS_CODE = 'http.status_code'.freeze - ERROR_RANGE = 500...600 + TEMPLATE = 'template'.freeze + TYPE = 'http'.freeze + URL = 'http.url'.freeze + + # General header functionality + module Headers + module_function + + def to_tag(name) + name.to_s.downcase.gsub(/[-\s]/, '_') + end + end + + # Request headers + module RequestHeaders + PREFIX = 'http.request.headers'.freeze + + module_function + + def to_tag(name) + "#{PREFIX}.#{Headers.to_tag(name)}" + end + end + + # Response headers + module ResponseHeaders + PREFIX = 'http.response.headers'.freeze + + module_function + + def to_tag(name) + "#{PREFIX}.#{Headers.to_tag(name)}" + end + end end end end diff --git a/lib/ddtrace/propagation/grpc_propagator.rb b/lib/ddtrace/propagation/grpc_propagator.rb new file mode 100644 index 0000000000..725ae7c957 --- /dev/null +++ b/lib/ddtrace/propagation/grpc_propagator.rb @@ -0,0 +1,54 @@ +require 'ddtrace/context' +require 'ddtrace/ext/distributed' + +module Datadog + # opentracing.io compliant methods for distributing trace context + # between two or more distributed services. Note this is very close + # to the HTTPPropagator; the key difference is the way gRPC handles + # header information (called "metadata") as it operates over HTTP2 + module GRPCPropagator + include Ext::DistributedTracing + + def self.inject!(context, metadata) + metadata[GRPC_METADATA_TRACE_ID] = context.trace_id.to_s + metadata[GRPC_METADATA_PARENT_ID] = context.span_id.to_s + metadata[GRPC_METADATA_SAMPLING_PRIORITY] = context.sampling_priority.to_s if context.sampling_priority + end + + def self.extract(metadata) + metadata = Carrier.new(metadata) + return Datadog::Context.new unless metadata.valid? + Datadog::Context.new(trace_id: metadata.trace_id, + span_id: metadata.parent_id, + sampling_priority: metadata.sampling_priority) + end + + # opentracing.io compliant carrier object + class Carrier + include Ext::DistributedTracing + + def initialize(metadata = {}) + @metadata = metadata || {} + end + + def valid? + trace_id && parent_id + end + + def trace_id + value = @metadata[GRPC_METADATA_TRACE_ID].to_i + value if (1..Span::MAX_ID).cover? value + end + + def parent_id + value = @metadata[GRPC_METADATA_PARENT_ID].to_i + value if (1..Span::MAX_ID).cover? value + end + + def sampling_priority + value = @metadata[GRPC_METADATA_SAMPLING_PRIORITY] + value && value.to_i + end + end + end +end diff --git a/lib/ddtrace/quantization/hash.rb b/lib/ddtrace/quantization/hash.rb new file mode 100644 index 0000000000..92128e90c5 --- /dev/null +++ b/lib/ddtrace/quantization/hash.rb @@ -0,0 +1,103 @@ +module Datadog + module Quantization + # Quantization for HTTP resources + module Hash + PLACEHOLDER = '?'.freeze + EXCLUDE_KEYS = [].freeze + SHOW_KEYS = [].freeze + DEFAULT_OPTIONS = { + exclude: EXCLUDE_KEYS, + show: SHOW_KEYS, + placeholder: PLACEHOLDER + }.freeze + + module_function + + def format(hash_obj, options = {}) + options ||= {} + format!(hash_obj, options) + rescue StandardError + options[:placeholder] || PLACEHOLDER + end + + def format!(hash_obj, options = {}) + options ||= {} + options = merge_options(DEFAULT_OPTIONS, options) + format_hash(hash_obj, options) + end + + def format_hash(hash_obj, options = {}) + case hash_obj + when ::Hash + return {} if options[:exclude] == :all + return hash_obj if options[:show] == :all + + hash_obj.each_with_object({}) do |(key, value), quantized| + if options[:show].any?(&indifferent_equals(key)) + quantized[key] = value + elsif options[:exclude].none?(&indifferent_equals(key)) + quantized[key] = format_value(value, options) + end + end + else + format_value(hash_obj, options) + end + end + + def format_value(value, options = {}) + return value if options[:show] == :all + + case value + when ::Hash + format_hash(value, options) + when Array + # If any are objects, format them. + format_array(value, options) + else + options[:placeholder] + end + end + + def format_array(value, options) + if value.any? { |v| v.class <= ::Hash || v.class <= Array } + first_entry = format_value(value.first, options) + value.size > 1 ? [first_entry, options[:placeholder]] : [first_entry] + # Otherwise short-circuit and return single placeholder + else + [options[:placeholder]] + end + end + + def merge_options(original, additional) + {}.tap do |options| + # Show + # If either is :all, value becomes :all + options[:show] = if original[:show] == :all || additional[:show] == :all + :all + else + (original[:show] || []).dup.concat(additional[:show] || []).uniq + end + + # Exclude + # If either is :all, value becomes :all + options[:exclude] = if original[:exclude] == :all || additional[:exclude] == :all + :all + else + (original[:exclude] || []).dup.concat(additional[:exclude] || []).uniq + end + + options[:placeholder] = additional[:placeholder] || original[:placeholder] + end + end + + def indifferent_equals(value) + value = convert_value(value) + ->(compared_value) { value == convert_value(compared_value) } + end + + def convert_value(value) + value.is_a?(Symbol) ? value.to_s : value + end + end + end +end diff --git a/lib/ddtrace/utils.rb b/lib/ddtrace/utils.rb index 9c0c3cdc5b..67bd9611ff 100644 --- a/lib/ddtrace/utils.rb +++ b/lib/ddtrace/utils.rb @@ -1,3 +1,5 @@ +require 'ddtrace/utils/database' + module Datadog # Utils contains low-level utilities, typically to provide pseudo-random trace IDs. module Utils diff --git a/lib/ddtrace/utils/database.rb b/lib/ddtrace/utils/database.rb new file mode 100644 index 0000000000..107f900044 --- /dev/null +++ b/lib/ddtrace/utils/database.rb @@ -0,0 +1,21 @@ +module Datadog + module Utils + # Common database-related utility functions. + module Database + module_function + + def normalize_vendor(vendor) + case vendor + when nil + 'defaultdb' + when 'postgresql' + 'postgres' + when 'sqlite3' + 'sqlite' + else + vendor + end + end + end + end +end diff --git a/lib/ddtrace/version.rb b/lib/ddtrace/version.rb index c1c8bdd2e7..bd90ab18dd 100644 --- a/lib/ddtrace/version.rb +++ b/lib/ddtrace/version.rb @@ -1,8 +1,8 @@ module Datadog module VERSION MAJOR = 0 - MINOR = 12 - PATCH = 1 + MINOR = 13 + PATCH = 0 PRE = nil STRING = [MAJOR, MINOR, PATCH, PRE].compact.join('.') diff --git a/spec/ddtrace/configurable_spec.rb b/spec/ddtrace/configurable_spec.rb new file mode 100644 index 0000000000..eaab5eee3f --- /dev/null +++ b/spec/ddtrace/configurable_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Configurable do + shared_examples_for 'a configurable constant' do + describe '#option' do + let(:name) { :foo } + let(:options) { {} } + let(:block) { nil } + before(:each) { configurable.send(:option, name, options, &block) } + + context 'given a default option' do + let(:options) { { default: default_value } } + let(:default_value) { :bar } + it { expect(configurable.get_option(name)).to eq(default_value) } + end + + context 'given a custom setter' do + let(:name) { :shout } + before(:each) { configurable.set_option(name, 'loud') } + + context 'option' do + let(:options) { { setter: ->(v) { v.upcase } } } + it { expect(configurable.get_option(name)).to eq('LOUD') } + end + + context 'block' do + let(:block) { proc { |value| "#{value.upcase}!" } } + it { expect(configurable.get_option(name)).to eq('LOUD!') } + end + end + end + + describe '#get_option' do + subject(:result) { configurable.get_option(name) } + let(:name) { :foo } + let(:options) { {} } + + it { expect(configurable).to respond_to(:get_option) } + + context 'when the option doesn\'t exist' do + it { expect { result }.to raise_error(Datadog::InvalidOptionError) } + end + end + + describe '#set_option' do + let(:name) { :foo } + let(:options) { {} } + let(:value) { :bar } + + before(:each) do + configurable.send(:option, name, options) + configurable.set_option(name, value) + end + + it { expect(configurable).to respond_to(:set_option) } + + context 'when a default has been defined' do + let(:options) { { default: default_value } } + let(:default_value) { :bar } + let(:value) { 'baz!' } + it { expect(configurable.get_option(name)).to eq(value) } + + context 'and the value set is \'false\'' do + let(:default_value) { true } + let(:value) { false } + it { expect(configurable.get_option(name)).to eq(value) } + end + end + + context 'when the option doesn\'t exist' do + subject(:result) { configurable.set_option(:bad_option, value) } + it { expect { result }.to raise_error(Datadog::InvalidOptionError) } + end + end + + describe '#to_h' do + subject(:hash) { configurable.to_h } + + before(:each) do + configurable.send(:option, :x, default: 1) + configurable.send(:option, :y, default: 2) + configurable.set_option(:y, 100) + end + + it { is_expected.to eq(x: 1, y: 100) } + end + + describe '#sorted_options' do + subject(:sorted_options) { configurable.sorted_options } + + before(:each) do + configurable.send(:option, :foo, depends_on: [:bar]) + configurable.send(:option, :bar, depends_on: [:baz]) + configurable.send(:option, :baz) + end + + it { is_expected.to eq([:baz, :bar, :foo]) } + end + end + + describe 'implemented' do + describe 'class' do + subject(:configurable) { Class.new { include(Datadog::Configurable) } } + it_behaves_like 'a configurable constant' + end + + describe 'module' do + subject(:configurable) { Module.new { include(Datadog::Configurable) } } + it_behaves_like 'a configurable constant' + end + end +end diff --git a/spec/ddtrace/configuration/pin_setup_spec.rb b/spec/ddtrace/configuration/pin_setup_spec.rb new file mode 100644 index 0000000000..ab3bbf732f --- /dev/null +++ b/spec/ddtrace/configuration/pin_setup_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Configuration::PinSetup do + let(:target) { Object.new } + + before(:each) do + Datadog::Pin.new('original-service', app: 'original-app').onto(target) + end + + describe '#call' do + before(:each) { described_class.new(target, options).call } + + context 'given options' do + let(:options) do + { + service_name: 'my-service', + app: 'my-app', + app_type: :cache, + tracer: tracer, + tags: { env: :prod }, + distributed_tracing: true + } + end + + let(:tracer) { Datadog::Tracer.new(writer: FauxWriter.new) } + + it do + expect(target.datadog_pin.service).to eq('my-service') + expect(target.datadog_pin.app).to eq('my-app') + expect(target.datadog_pin.tags).to eq(env: :prod) + expect(target.datadog_pin.config).to eq(distributed_tracing: true) + expect(target.datadog_pin.tracer).to eq(tracer) + end + end + + context 'missing options' do + let(:options) { { app: 'custom-app' } } + + it do + expect(target.datadog_pin.app).to eq('custom-app') + expect(target.datadog_pin.service).to eq('original-service') + end + end + end + + describe 'Datadog#configure' do + before(:each) { Datadog.configure(target, service_name: :foo, extra: :bar) } + + it do + expect(target.datadog_pin.service).to eq(:foo) + expect(target.datadog_pin.config).to eq(extra: :bar) + end + end +end diff --git a/spec/ddtrace/configuration/proxy_spec.rb b/spec/ddtrace/configuration/proxy_spec.rb new file mode 100644 index 0000000000..b30ea1a02d --- /dev/null +++ b/spec/ddtrace/configuration/proxy_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Configuration::Proxy do + subject(:proxy) { described_class.new(configurable_module) } + + let(:configurable_module) do + Module.new do + include Datadog::Configurable + option :x, default: :a + option :y, default: :b + end + end + + describe '#[]' do + before(:each) do + proxy[:x] = 1 + proxy[:y] = 2 + end + + it do + expect(proxy[:x]).to eq(1) + expect(proxy[:y]).to eq(2) + end + end + + describe '#to_h' do + subject(:hash) { proxy.to_h } + it { is_expected.to eq(x: :a, y: :b) } + end + + describe '#to_hash' do + subject(:hash) { proxy.to_hash } + it { is_expected.to eq(x: :a, y: :b) } + end + + describe '#merge' do + subject(:result) { proxy.merge(hash) } + let(:hash) { { z: :c } } + it { is_expected.to eq(x: :a, y: :b, z: :c) } + end +end diff --git a/spec/ddtrace/configuration/resolver_spec.rb b/spec/ddtrace/configuration/resolver_spec.rb new file mode 100644 index 0000000000..300a81b0ed --- /dev/null +++ b/spec/ddtrace/configuration/resolver_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Configuration::Resolver do + subject(:resolver) { described_class.new(graph) } + + describe '#call' do + subject(:order) { resolver.call } + + context 'given a set of dependencies' do + let(:graph) { { 1 => [2], 2 => [3, 4], 3 => [], 4 => [3], 5 => [1] } } + it { expect(order).to eq([3, 4, 2, 1, 5]) } + end + + context 'given cyclic dependencies' do + let(:graph) { { 1 => [2], 2 => [1] } } + it { expect { order }.to raise_error(TSort::Cyclic) } + end + end +end diff --git a/spec/ddtrace/configuration_spec.rb b/spec/ddtrace/configuration_spec.rb new file mode 100644 index 0000000000..3eeebcfed1 --- /dev/null +++ b/spec/ddtrace/configuration_spec.rb @@ -0,0 +1,170 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Configuration do + let(:configuration) { described_class.new(registry: registry) } + let(:registry) { Datadog::Registry.new } + + describe '#use' do + subject(:result) { configuration.use name, options } + let(:name) { :example } + let(:integration) { double('integration') } + let(:options) { {} } + + before(:each) do + registry.add(name, integration) + end + + context 'for a generic integration' do + before(:each) do + expect(integration).to receive(:sorted_options).and_return([]) + expect(integration).to receive(:patch).and_return(true) + end + + it { expect { result }.to_not raise_error } + end + + context 'for an integration that includes Datadog::Contrib::Base' do + let(:options) { { option1: :foo!, option2: :bar! } } + let(:integration) do + Module.new do + include Datadog::Contrib::Base + option :option1 + option :option2 + end + end + + it do + expect { result }.to_not raise_error + expect(configuration[name][:option1]).to eq(:foo!) + expect(configuration[name][:option2]).to eq(:bar!) + end + + context 'and has a lazy option' do + let(:integration) do + Module.new do + include Datadog::Contrib::Base + option :option1, default: -> { 1 + 1 }, lazy: true + end + end + + it { expect(configuration[name][:option1]).to eq(2) } + end + + context 'and has dependencies' do + let(:options) { { multiply_by: 5, number: 5 } } + let(:integration) do + Module.new do + include Datadog::Contrib::Base + option :multiply_by, depends_on: [:number] do |value| + get_option(:number) * value + end + + option :number + end + end + + it do + expect { result }.to_not raise_error + expect(configuration[name][:number]).to eq(5) + expect(configuration[name][:multiply_by]).to eq(25) + end + end + + context 'and has a setter' do + let(:array) { [] } + let(:options) { { option1: :foo! } } + let(:integration) do + arr = array + Module.new do + include Datadog::Contrib::Base + option :option1 + option :option2, default: 10 do |value| + arr << value + value + end + end + end + + it 'pass through the setter' do + expect { result }.to_not raise_error + expect(configuration[name][:option1]).to eq(:foo!) + expect(configuration[name][:option2]).to eq(10) + expect(array).to include(10) + end + end + + context 'when a setting is changed' do + before(:each) { configuration[name][:option1] = :foo } + it { expect(configuration[:example][:option1]).to eq(:foo) } + end + + context 'when coerced to a hash' do + let(:integration) do + Module.new do + include Datadog::Contrib::Base + option :option1, default: :foo + option :option2, default: :bar + end + end + + it { expect(configuration[name].to_h).to eq(option1: :foo, option2: :bar) } + end + end + end + + describe '#tracer' do + let(:tracer) { Datadog::Tracer.new } + let(:debug_state) { tracer.class.debug_logging } + let(:custom_log) { Logger.new(STDOUT) } + + context 'given some settings' do + before(:each) do + @original_log = tracer.class.log + + configuration.tracer( + enabled: false, + debug: !debug_state, + log: custom_log, + hostname: 'tracer.host.com', + port: 1234, + env: :config_test, + tags: { foo: :bar }, + instance: tracer + ) + end + + after(:each) do + tracer.class.debug_logging = debug_state + tracer.class.log = @original_log + end + + it 'applies settings correctly' do + expect(tracer.enabled).to be false + expect(debug_state).to be false + expect(Datadog::Tracer.log).to eq(custom_log) + expect(tracer.writer.transport.hostname).to eq('tracer.host.com') + expect(tracer.writer.transport.port).to eq(1234) + expect(tracer.tags[:env]).to eq(:config_test) + expect(tracer.tags[:foo]).to eq(:bar) + end + end + + it 'acts on the default tracer' do + previous_state = Datadog.tracer.enabled + configuration.tracer(enabled: !previous_state) + expect(Datadog.tracer.enabled).to_not eq(previous_state) + configuration.tracer(enabled: previous_state) + expect(Datadog.tracer.enabled).to eq(previous_state) + end + end + + describe '#[]' do + context 'when the integration doesn\'t exist' do + it do + expect { configuration[:foobar] }.to raise_error(Datadog::Configuration::InvalidIntegrationError) + end + end + end +end diff --git a/spec/ddtrace/contrib/active_model_serializers/helpers.rb b/spec/ddtrace/contrib/active_model_serializers/helpers.rb new file mode 100644 index 0000000000..134efbbd7e --- /dev/null +++ b/spec/ddtrace/contrib/active_model_serializers/helpers.rb @@ -0,0 +1,61 @@ +module ActiveModelSerializersHelpers + class << self + def ams_0_10_or_newer? + Gem.loaded_specs['active_model_serializers'] \ + && Gem.loaded_specs['active_model_serializers'].version >= Gem::Version.new('0.10') + end + + def disable_logging + if ams_0_10_or_newer? + ActiveModelSerializers.logger.level = Logger::Severity::UNKNOWN + end + end + end +end + +RSpec.shared_context 'AMS serializer' do + let(:serializer_class) do + end + + if ActiveModelSerializersHelpers.ams_0_10_or_newer? + before(:each) do + stub_const('Model', Class.new(ActiveModelSerializers::Model) do + attr_writer :id + end) + + stub_const('TestModel', Class.new(Model) do + attributes :name + end) + + stub_const('TestModelSerializer', Class.new(ActiveModel::Serializer) do + attributes :name + end) + end + else + before(:each) do + stub_const('Model', Class.new do + attr_writer :id + + def initialize(hash = {}) + @attributes = hash + end + + def read_attribute_for_serialization(name) + if [:id, 'id'].include?(name) + object_id + elsif respond_to?(name) + send name + else + @attributes[name] + end + end + end) + + stub_const('TestModel', Class.new(Model)) + + stub_const('TestModelSerializer', Class.new(ActiveModel::Serializer) do + attributes :name + end) + end + end +end diff --git a/spec/ddtrace/contrib/active_model_serializers/patcher_spec.rb b/spec/ddtrace/contrib/active_model_serializers/patcher_spec.rb new file mode 100644 index 0000000000..eb17b8d37d --- /dev/null +++ b/spec/ddtrace/contrib/active_model_serializers/patcher_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' +require 'spec/ddtrace/contrib/active_model_serializers/helpers' + +require 'active_support/all' +require 'active_model_serializers' +require 'ddtrace' +require 'ddtrace/contrib/active_model_serializers/patcher' +require 'ddtrace/ext/http' + +RSpec.describe 'ActiveModelSerializers patcher' do + include_context 'AMS serializer' + + let(:tracer) { ::Datadog::Tracer.new(writer: FauxWriter.new) } + + def all_spans + tracer.writer.spans(:keep) + end + + before(:each) do + # Supress active_model_serializers log output in the test run + ActiveModelSerializersHelpers.disable_logging + + Datadog.configure do |c| + c.use :active_model_serializers, tracer: tracer + end + + # Make sure to update the subscription tracer, + # so we aren't writing to a stale tracer. + if Datadog::Contrib::ActiveModelSerializers::Patcher.patched? + Datadog::Contrib::ActiveModelSerializers::Events.subscriptions.each do |subscription| + allow(subscription).to receive(:tracer).and_return(tracer) + end + end + end + + describe 'on render' do + let(:test_obj) { TestModel.new(name: 'test object') } + let(:serializer) { 'TestModelSerializer' } + let(:adapter) { 'ActiveModelSerializers::Adapter::Attributes' } + let(:event) { Datadog::Contrib::ActiveModelSerializers::Patcher.send(:event_name) } + let(:name) do + if ActiveModelSerializersHelpers.ams_0_10_or_newer? + Datadog::Contrib::ActiveModelSerializers::Events::Render.span_name + else + Datadog::Contrib::ActiveModelSerializers::Events::Serialize.span_name + end + end + + let(:active_model_serializers_span) do + all_spans.select { |s| s.name == name }.first + end + + if ActiveModelSerializersHelpers.ams_0_10_or_newer? + context 'when adapter is set' do + it 'is expected to send a span' do + ActiveModelSerializers::SerializableResource.new(test_obj).serializable_hash + + active_model_serializers_span.tap do |span| + expect(span).to_not be_nil + expect(span.name).to eq(name) + expect(span.resource).to eq(serializer) + expect(span.service).to eq('active_model_serializers') + expect(span.span_type).to eq(Datadog::Ext::HTTP::TEMPLATE) + expect(span.get_tag('active_model_serializers.serializer')).to eq(serializer) + expect(span.get_tag('active_model_serializers.adapter')).to eq(adapter) + end + end + end + end + + context 'when adapter is nil' do + if ActiveModelSerializersHelpers.ams_0_10_or_newer? + it 'is expected to send a span with adapter tag equal to the model name' do + ActiveModelSerializers::SerializableResource.new(test_obj, adapter: nil).serializable_hash + + active_model_serializers_span.tap do |span| + expect(span).to_not be_nil + expect(span.name).to eq(name) + expect(span.resource).to eq(serializer) + expect(span.service).to eq('active_model_serializers') + expect(span.span_type).to eq(Datadog::Ext::HTTP::TEMPLATE) + expect(span.get_tag('active_model_serializers.serializer')).to eq(serializer) + expect(span.get_tag('active_model_serializers.adapter')).to eq(test_obj.class.to_s) + end + end + else + it 'is expected to send a span with no adapter tag' do + TestModelSerializer.new(test_obj).as_json + + active_model_serializers_span.tap do |span| + expect(span).to_not be_nil + expect(span.name).to eq(name) + expect(span.resource).to eq(serializer) + expect(span.service).to eq('active_model_serializers') + expect(span.span_type).to eq(Datadog::Ext::HTTP::TEMPLATE) + expect(span.get_tag('active_model_serializers.serializer')).to eq(serializer) + expect(span.get_tag('active_model_serializers.adapter')).to be_nil + end + end + end + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/spec/ddtrace/contrib/active_record/utils_spec.rb b/spec/ddtrace/contrib/active_record/utils_spec.rb index 7f8df051c6..de3cd77d6b 100644 --- a/spec/ddtrace/contrib/active_record/utils_spec.rb +++ b/spec/ddtrace/contrib/active_record/utils_spec.rb @@ -3,37 +3,6 @@ require 'ddtrace/contrib/active_record/utils' RSpec.describe Datadog::Contrib::ActiveRecord::Utils do - describe '#normalize_vendor' do - subject(:result) { described_class.normalize_vendor(value) } - - context 'when given' do - context 'nil' do - let(:value) { nil } - it { is_expected.to eq('defaultdb') } - end - - context 'sqlite3' do - let(:value) { 'sqlite3' } - it { is_expected.to eq('sqlite') } - end - - context 'mysql2' do - let(:value) { 'mysql2' } - it { is_expected.to eq('mysql2') } - end - - context 'postgresql' do - let(:value) { 'postgresql' } - it { is_expected.to eq('postgres') } - end - - context 'customdb' do - let(:value) { 'customdb' } - it { is_expected.to eq(value) } - end - end - end - describe 'regression: retrieving database without an active connection does not raise an error' do before(:each) do root_pw = ENV.fetch('TEST_MYSQL_ROOT_PASSWORD', 'root') diff --git a/spec/ddtrace/contrib/active_support/notifications/event_spec.rb b/spec/ddtrace/contrib/active_support/notifications/event_spec.rb new file mode 100644 index 0000000000..ce0107e2c6 --- /dev/null +++ b/spec/ddtrace/contrib/active_support/notifications/event_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' +require 'ddtrace' + +require 'active_support/notifications' +require 'ddtrace/contrib/active_support/notifications/event' + +RSpec.describe Datadog::Contrib::ActiveSupport::Notifications::Event do + describe 'implemented' do + subject(:test_class) do + test_event_name = event_name + test_span_name = span_name + + Class.new.tap do |klass| + klass.send(:include, described_class) + klass.send(:define_singleton_method, :event_name) { test_event_name } + klass.send(:define_singleton_method, :span_name) { test_span_name } + klass.send(:define_singleton_method, :process, &process_block) + end + end + + let(:event_name) { double('event_name') } + let(:span_name) { double('span_name') } + let(:process_block) { proc { spy.call } } + let(:spy) { double(:spy) } + + describe 'class' do + describe 'behavior' do + describe '#subscribe!' do + subject(:result) { test_class.subscribe! } + + it do + expect(ActiveSupport::Notifications).to receive(:subscribe) + .with(event_name, be_a_kind_of(Datadog::Contrib::ActiveSupport::Notifications::Subscription)) + is_expected.to be true + end + + context 'is called a second time' do + before(:each) do + allow(ActiveSupport::Notifications).to receive(:subscribe) + .with(event_name, be_a_kind_of(Datadog::Contrib::ActiveSupport::Notifications::Subscription)) + test_class.subscribe! + end + + it do + expect(ActiveSupport::Notifications).to_not receive(:subscribe) + is_expected.to be true + end + end + end + + describe '#subscribe' do + before(:each) do + expect(Datadog::Contrib::ActiveSupport::Notifications::Subscription).to receive(:new) + .with(test_class.tracer, test_class.span_name, test_class.span_options) + .and_call_original + end + + context 'when given no pattern' do + subject(:subscription) { test_class.subscribe } + + before(:each) do + expect_any_instance_of(Datadog::Contrib::ActiveSupport::Notifications::Subscription).to receive(:subscribe) + .with(event_name) + end + + it { is_expected.to be_a_kind_of(Datadog::Contrib::ActiveSupport::Notifications::Subscription) } + it { expect(test_class.subscriptions).to contain_exactly(subscription) } + end + + context 'when given a pattern' do + subject(:subscription) { test_class.subscribe(pattern) } + let(:pattern) { double('pattern') } + + before(:each) do + expect_any_instance_of(Datadog::Contrib::ActiveSupport::Notifications::Subscription).to receive(:subscribe) + .with(pattern) + end + + it { is_expected.to be_a_kind_of(Datadog::Contrib::ActiveSupport::Notifications::Subscription) } + it { expect(test_class.subscriptions).to contain_exactly(subscription) } + end + end + + describe '#subscription' do + context 'when given no options' do + subject(:subscription) { test_class.subscription } + + before(:each) do + expect(Datadog::Contrib::ActiveSupport::Notifications::Subscription).to receive(:new) + .with(test_class.tracer, test_class.span_name, test_class.span_options) + .and_call_original + end + + it { is_expected.to be_a_kind_of(Datadog::Contrib::ActiveSupport::Notifications::Subscription) } + it { expect(test_class.subscriptions).to contain_exactly(subscription) } + end + + context 'when given options' do + subject(:subscription) { test_class.subscription(span_name, options, tracer) } + + let(:span_name) { double('span name') } + let(:options) { double('options') } + let(:tracer) { double('tracer') } + + before(:each) do + expect(Datadog::Contrib::ActiveSupport::Notifications::Subscription).to receive(:new) + .with(tracer, span_name, options) + .and_call_original + end + + it { is_expected.to be_a_kind_of(Datadog::Contrib::ActiveSupport::Notifications::Subscription) } + it { expect(test_class.subscriptions).to contain_exactly(subscription) } + end + end + end + end + end +end diff --git a/spec/ddtrace/contrib/active_support/notifications/subscription_spec.rb b/spec/ddtrace/contrib/active_support/notifications/subscription_spec.rb index 46e67d20e4..357ff174a3 100644 --- a/spec/ddtrace/contrib/active_support/notifications/subscription_spec.rb +++ b/spec/ddtrace/contrib/active_support/notifications/subscription_spec.rb @@ -9,7 +9,7 @@ subject(:subscription) { described_class.new(tracer, span_name, options, &block) } let(:tracer) { ::Datadog::Tracer.new(writer: FauxWriter.new) } let(:span_name) { double('span_name') } - let(:options) { double('options') } + let(:options) { {} } let(:block) do proc do |span, name, id, payload| spy.call(span, name, id, payload) @@ -68,6 +68,11 @@ expect(tracer).to receive(:trace).with(span_name, options).and_return(span) is_expected.to be(span) end + + it 'sets the parent span' do + parent = tracer.trace('parent_span') + expect(subject.parent_id).to eq parent.span_id + end end describe '#finish' do diff --git a/spec/ddtrace/contrib/excon/instrumentation_spec.rb b/spec/ddtrace/contrib/excon/instrumentation_spec.rb new file mode 100644 index 0000000000..f06dc1409b --- /dev/null +++ b/spec/ddtrace/contrib/excon/instrumentation_spec.rb @@ -0,0 +1,265 @@ +require 'spec_helper' + +require 'excon' +require 'ddtrace' +require 'ddtrace/contrib/excon/middleware' + +RSpec.describe Datadog::Contrib::Excon::Middleware do + let(:tracer) { Datadog::Tracer.new(writer: FauxWriter.new) } + + let(:connection_options) { { mock: true } } + let(:middleware_options) { {} } + let(:configuration_options) { { tracer: tracer } } + + let(:request_span) do + tracer.writer.spans(:keep).find { |span| span.name == Datadog::Contrib::Excon::Middleware::SPAN_NAME } + end + + let(:all_request_spans) do + tracer.writer.spans(:keep).find_all { |span| span.name == Datadog::Contrib::Excon::Middleware::SPAN_NAME } + end + + before(:each) do + Datadog.configure do |c| + c.use :excon, configuration_options + end + end + + after(:each) do + Excon.stubs.clear + end + + let(:connection) do + Excon.new('http://example.com', connection_options).tap do + Excon.stub({ method: :get, path: '/success' }, body: 'OK', status: 200) + Excon.stub({ method: :post, path: '/failure' }, body: 'Boom!', status: 500) + Excon.stub({ method: :get, path: '/not_found' }, body: 'Not Found.', status: 404) + Excon.stub( + { method: :get, path: '/timeout' }, + lambda do |_request_params| + raise Excon::Errors::Timeout, 'READ TIMEOUT' + end + ) + end + end + + shared_context 'connection with custom middleware' do + let(:connection_options) do + super().merge( + middlewares: [ + Excon::Middleware::ResponseParser, + Datadog::Contrib::Excon::Middleware.with(middleware_options), + Excon::Middleware::Mock + ] + ) + end + end + + shared_context 'connection with default middleware' do + let(:connection_options) do + super().merge(middlewares: Datadog::Contrib::Excon::Middleware.with(middleware_options).around_default_stack) + end + end + + context 'when there is no interference' do + subject!(:response) { connection.get(path: '/success') } + + it do + expect(response).to be_a_kind_of(Excon::Response) + expect(response.body).to eq('OK') + expect(response.status).to eq(200) + end + end + + context 'when there is successful request' do + subject!(:response) { connection.get(path: '/success') } + + it do + expect(request_span).to_not be nil + expect(request_span.service).to eq(Datadog::Contrib::Excon::Patcher::DEFAULT_SERVICE) + expect(request_span.name).to eq(Datadog::Contrib::Excon::Middleware::SPAN_NAME) + expect(request_span.resource).to eq('GET') + expect(request_span.get_tag(Datadog::Ext::HTTP::METHOD)).to eq('GET') + expect(request_span.get_tag(Datadog::Ext::HTTP::STATUS_CODE)).to eq('200') + expect(request_span.get_tag(Datadog::Ext::HTTP::URL)).to eq('/success') + expect(request_span.get_tag(Datadog::Ext::NET::TARGET_HOST)).to eq('example.com') + expect(request_span.get_tag(Datadog::Ext::NET::TARGET_PORT)).to eq('80') + expect(request_span.span_type).to eq(Datadog::Ext::HTTP::TYPE) + expect(request_span.status).to_not eq(Datadog::Ext::Errors::STATUS) + end + end + + context 'when there is a failing request' do + subject!(:response) { connection.post(path: '/failure') } + + it do + expect(request_span.service).to eq(Datadog::Contrib::Excon::Patcher::DEFAULT_SERVICE) + expect(request_span.name).to eq(Datadog::Contrib::Excon::Middleware::SPAN_NAME) + expect(request_span.resource).to eq('POST') + expect(request_span.get_tag(Datadog::Ext::HTTP::METHOD)).to eq('POST') + expect(request_span.get_tag(Datadog::Ext::HTTP::URL)).to eq('/failure') + expect(request_span.get_tag(Datadog::Ext::HTTP::STATUS_CODE)).to eq('500') + expect(request_span.get_tag(Datadog::Ext::NET::TARGET_HOST)).to eq('example.com') + expect(request_span.get_tag(Datadog::Ext::NET::TARGET_PORT)).to eq('80') + expect(request_span.span_type).to eq(Datadog::Ext::HTTP::TYPE) + expect(request_span.status).to eq(Datadog::Ext::Errors::STATUS) + expect(request_span.get_tag(Datadog::Ext::Errors::TYPE)).to eq('Error 500') + expect(request_span.get_tag(Datadog::Ext::Errors::MSG)).to eq('Boom!') + end + end + + context 'when the path is not found' do + subject!(:response) { connection.get(path: '/not_found') } + it { expect(request_span.status).to_not eq(Datadog::Ext::Errors::STATUS) } + end + + context 'when the request times out' do + subject(:response) { connection.get(path: '/timeout') } + it do + expect { subject }.to raise_error + expect(request_span.finished?).to eq(true) + expect(request_span.status).to eq(Datadog::Ext::Errors::STATUS) + expect(request_span.get_tag('error.type')).to eq('Excon::Error::Timeout') + end + + context 'when the request is idempotent' do + subject(:response) { connection.get(path: '/timeout', idempotent: true, retry_limit: 4) } + it 'records separate spans' do + expect { subject }.to raise_error + expect(all_request_spans.size).to eq(4) + expect(all_request_spans.all?(&:finished?)).to eq(true) + end + end + end + + context 'when there is custom error handling' do + subject!(:response) { connection.get(path: 'not_found') } + let(:configuration_options) { super().merge(error_handler: custom_handler) } + let(:custom_handler) { ->(env) { (400...600).cover?(env[:status]) } } + after(:each) { Datadog.configuration[:excon][:error_handler] = nil } + it { expect(request_span.status).to eq(Datadog::Ext::Errors::STATUS) } + end + + context 'when split by domain' do + subject!(:response) { connection.get(path: '/success') } + let(:configuration_options) { super().merge(split_by_domain: true) } + after(:each) { Datadog.configuration[:excon][:split_by_domain] = false } + + it do + expect(request_span.name).to eq(Datadog::Contrib::Excon::Middleware::SPAN_NAME) + expect(request_span.service).to eq('example.com') + expect(request_span.resource).to eq('GET') + end + end + + context 'default request headers' do + subject!(:response) do + expect_any_instance_of(Datadog::Contrib::Excon::Middleware).to receive(:request_call) + .and_wrap_original do |m, *args| + m.call(*args).tap do |datum| + # Assert request headers + headers = datum[:headers] + expect(headers).to_not include(Datadog::Ext::DistributedTracing::HTTP_HEADER_TRACE_ID) + expect(headers).to_not include(Datadog::Ext::DistributedTracing::HTTP_HEADER_PARENT_ID) + end + end + + connection.get(path: '/success') + end + + it do + expect(response).to be_a_kind_of(::Excon::Response) + expect(response.body).to eq('OK') + expect(response.status).to eq(200) + end + end + + context 'when distributed tracing is enabled' do + subject!(:response) do + expect_any_instance_of(Datadog::Contrib::Excon::Middleware).to receive(:request_call) + .and_wrap_original do |m, *args| + m.call(*args).tap do |datum| + # Assert request headers + span = datum[:datadog_span] + headers = datum[:headers] + expect(headers).to include(Datadog::Ext::DistributedTracing::HTTP_HEADER_TRACE_ID => span.trace_id.to_s) + expect(headers).to include(Datadog::Ext::DistributedTracing::HTTP_HEADER_PARENT_ID => span.span_id.to_s) + end + end + + connection.get(path: '/success') + end + + let(:configuration_options) { super().merge(distributed_tracing: true) } + after(:each) { Datadog.configuration[:excon][:distributed_tracing] = false } + + it do + expect(response).to be_a_kind_of(::Excon::Response) + expect(response.body).to eq('OK') + expect(response.status).to eq(200) + end + + context 'but the tracer is disabled' do + subject!(:response) do + # Disable the tracer + tracer.enabled = false + + expect_any_instance_of(Datadog::Contrib::Excon::Middleware).to receive(:request_call) + .and_wrap_original do |m, *args| + m.call(*args).tap do |datum| + # Assert request headers + headers = datum[:headers] + expect(headers).to_not include(Datadog::Ext::DistributedTracing::HTTP_HEADER_TRACE_ID) + expect(headers).to_not include(Datadog::Ext::DistributedTracing::HTTP_HEADER_PARENT_ID) + end + end + + connection.get(path: '/success') + end + + it do + expect(response).to be_a_kind_of(::Excon::Response) + expect(response.body).to eq('OK') + expect(response.status).to eq(200) + end + end + end + + context 'global service name' do + let(:service_name) { 'excon-global' } + + before(:each) do + @old_service_name = Datadog.configuration[:excon][:service_name] + Datadog.configure { |c| c.use :excon, service_name: service_name } + end + + after(:each) { Datadog.configure { |c| c.use :excon, service_name: @old_service_name } } + + it do + Excon.stub({ method: :get, path: '/success' }, body: 'OK', status: 200) + connection.get(path: '/success') + expect(request_span.service).to eq(service_name) + end + end + + context 'service name per request' do + subject!(:response) do + Excon.stub({ method: :get, path: '/success' }, body: 'OK', status: 200) + connection.get(path: '/success') + end + + let(:middleware_options) { { service_name: service_name } } + + context 'with default middleware' do + include_context 'connection with default middleware' + let(:service_name) { 'request-with-default' } + it { expect(request_span.service).to eq(service_name) } + end + + context 'with custom middleware' do + include_context 'connection with custom middleware' + let(:service_name) { 'request-with-custom' } + it { expect(request_span.service).to eq(service_name) } + end + end +end diff --git a/spec/ddtrace/contrib/grpc/datadog_interceptor/client_spec.rb b/spec/ddtrace/contrib/grpc/datadog_interceptor/client_spec.rb new file mode 100644 index 0000000000..3d05c49b53 --- /dev/null +++ b/spec/ddtrace/contrib/grpc/datadog_interceptor/client_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' +require 'grpc' +require 'ddtrace' + +RSpec.describe 'tracing on the client connection' do + subject(:client) { Datadog::Contrib::GRPC::DatadogInterceptor::Client.new } + + let(:span) { subject.datadog_pin.tracer.writer.spans.first } + + before do + Datadog.configure do |c| + c.use :grpc, + tracer: get_test_tracer, + service_name: 'rspec' + end + end + + context 'using client-specific configurations' do + let(:keywords) do + { request: instance_double(Object), + call: instance_double('GRPC::ActiveCall'), + method: 'MyService.Endpoint', + metadata: { some: 'datum' } } + end + + let(:default_client_interceptor) do + Datadog::Contrib::GRPC::DatadogInterceptor::Client.new + end + + let(:configured_client_interceptor) do + Datadog::Contrib::GRPC::DatadogInterceptor::Client.new do |c| + c.service_name = 'cepsr' + end + end + + it 'replaces default service name' do + default_client_interceptor.request_response(keywords) {} + span = default_client_interceptor.datadog_pin.tracer.writer.spans.first + expect(span.service).to eq 'rspec' + + configured_client_interceptor.request_response(keywords) {} + span = configured_client_interceptor.datadog_pin.tracer.writer.spans.first + expect(span.service).to eq 'cepsr' + end + end + + shared_examples 'span data contents' do + specify { expect(span.name).to eq 'grpc.client' } + specify { expect(span.span_type).to eq 'grpc' } + specify { expect(span.service).to eq 'rspec' } + specify { expect(span.resource).to eq 'myservice.endpoint' } + specify { expect(span.get_tag('error.stack')).to be_nil } + specify { expect(span.get_tag(:some)).to eq 'datum' } + end + + describe '#request_response' do + let(:keywords) do + { request: instance_double(Object), + call: instance_double('GRPC::ActiveCall'), + method: 'MyService.Endpoint', + metadata: { some: 'datum' } } + end + + before do + subject.request_response(keywords) {} + end + + it_behaves_like 'span data contents' + end + + describe '#client_streamer' do + let(:keywords) do + { call: instance_double('GRPC::ActiveCall'), + method: 'MyService.Endpoint', + metadata: { some: 'datum' } } + end + + before do + subject.client_streamer(keywords) {} + end + + it_behaves_like 'span data contents' + end + + describe '#server_streamer' do + let(:keywords) do + { request: instance_double(Object), + call: instance_double('GRPC::ActiveCall'), + method: 'MyService.Endpoint', + metadata: { some: 'datum' } } + end + + before do + subject.server_streamer(keywords) {} + end + + it_behaves_like 'span data contents' + end + + describe '#bidi_streamer' do + let(:keywords) do + { requests: instance_double(Array), + call: instance_double('GRPC::ActiveCall'), + method: 'MyService.Endpoint', + metadata: { some: 'datum' } } + end + + before do + subject.bidi_streamer(keywords) {} + end + + it_behaves_like 'span data contents' + end +end diff --git a/spec/ddtrace/contrib/grpc/datadog_interceptor/server_spec.rb b/spec/ddtrace/contrib/grpc/datadog_interceptor/server_spec.rb new file mode 100644 index 0000000000..e56da24619 --- /dev/null +++ b/spec/ddtrace/contrib/grpc/datadog_interceptor/server_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' +require 'grpc' +require 'ddtrace' + +RSpec.describe 'tracing on the server connection' do + subject(:server) { Datadog::Contrib::GRPC::DatadogInterceptor::Server.new } + + before do + Datadog.configure do |c| + c.use :grpc, tracer: get_test_tracer, service_name: 'rspec' + end + end + + let(:span) { Datadog::Pin.get_from(::GRPC).tracer.writer.spans.first } + + shared_examples 'span data contents' do + specify { expect(span.name).to eq 'grpc.service' } + specify { expect(span.span_type).to eq 'grpc' } + specify { expect(span.service).to eq 'rspec' } + specify { expect(span.resource).to eq 'my.server.endpoint' } + specify { expect(span.get_tag('error.stack')).to be_nil } + specify { expect(span.get_tag(:some)).to eq 'datum' } + end + + describe '#request_response' do + let(:keywords) do + { request: instance_double(Object), + call: instance_double('GRPC::ActiveCall', metadata: { some: 'datum' }), + method: instance_double(Method, owner: 'My::Server', name: 'endpoint') } + end + + before do + subject.request_response(keywords) {} + end + + it_behaves_like 'span data contents' + end + + describe '#client_streamer' do + let(:keywords) do + { call: instance_double('GRPC::ActiveCall', metadata: { some: 'datum' }), + method: instance_double(Method, owner: 'My::Server', name: 'endpoint') } + end + + before do + subject.client_streamer(keywords) {} + end + + it_behaves_like 'span data contents' + end + + describe '#server_streamer' do + let(:keywords) do + { request: instance_double(Object), + call: instance_double('GRPC::ActiveCall', metadata: { some: 'datum' }), + method: instance_double(Method, owner: 'My::Server', name: 'endpoint') } + end + + before do + subject.server_streamer(keywords) {} + end + + it_behaves_like 'span data contents' + end + + describe '#bidi_streamer' do + let(:keywords) do + { requests: instance_double(Array), + call: instance_double('GRPC::ActiveCall', metadata: { some: 'datum' }), + method: instance_double(Method, owner: 'My::Server', name: 'endpoint') } + end + + before do + subject.bidi_streamer(keywords) {} + end + + it_behaves_like 'span data contents' + end +end diff --git a/spec/ddtrace/contrib/grpc/integration_spec.rb b/spec/ddtrace/contrib/grpc/integration_spec.rb new file mode 100644 index 0000000000..8371ff9bbb --- /dev/null +++ b/spec/ddtrace/contrib/grpc/integration_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' +require_relative 'support/grpc_helper' +require 'ddtrace' + +RSpec.describe 'gRPC integration test' do + include GRPCHelper + + let(:spans) do + Datadog::Pin.get_from(::GRPC).tracer.writer.spans + end + + before do + Datadog.configure do |c| + c.use :grpc, tracer: get_test_tracer, service_name: 'rspec' + end + end + + context 'multiple client configurations' do + let(:configured_interceptor) do + Datadog::Contrib::GRPC::DatadogInterceptor::Client.new do |c| + c.service_name = 'awesome sauce' + end + end + let(:alternate_client) do + GRPCHelper::TestService.rpc_stub_class.new( + '0.0.0.0:50051', + :this_channel_is_insecure, + interceptors: [configured_interceptor] + ) + end + + it 'uses the correct configuration information' do + run_request_reply + span = spans.first + expect(span.service).to eq 'rspec' + + run_request_reply('0.0.0.0:50051', alternate_client) + span = configured_interceptor.datadog_pin.tracer.writer.spans.first + expect(span.service).to eq 'awesome sauce' + end + end + + shared_examples 'associates child spans with the parent' do + let(:parent_span) { spans.first } + let(:child_span) { spans.last } + + specify do + expect(child_span.trace_id).to eq parent_span.trace_id + expect(child_span.parent_id).to eq parent_span.span_id + end + end + + context 'request reply' do + before { run_request_reply } + it_behaves_like 'associates child spans with the parent' + end + + context 'client stream' do + before { run_client_streamer } + it_behaves_like 'associates child spans with the parent' + end + + context 'server stream' do + before { run_server_streamer } + it_behaves_like 'associates child spans with the parent' + end + + context 'bidirectional stream' do + before { run_bidi_streamer } + it_behaves_like 'associates child spans with the parent' + end +end diff --git a/spec/ddtrace/contrib/grpc/interception_context_spec.rb b/spec/ddtrace/contrib/grpc/interception_context_spec.rb new file mode 100644 index 0000000000..bbe4307cdb --- /dev/null +++ b/spec/ddtrace/contrib/grpc/interception_context_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' +require 'grpc' +require 'ddtrace' + +RSpec.describe GRPC::InterceptionContext do + subject(:interception_context) { described_class.new } + + describe '#intercept!' do + let(:span) { Datadog::Pin.get_from(::GRPC).tracer.writer.spans.first } + + before do + Datadog.configure do |c| + c.use :grpc, tracer: get_test_tracer, service_name: 'rspec' + end + + subject.intercept!(type, keywords) {} + end + + context 'when intercepting on the client' do + shared_examples 'span data contents' do + specify { expect(span.name).to eq 'grpc.client' } + specify { expect(span.span_type).to eq 'grpc' } + specify { expect(span.service).to eq 'rspec' } + specify { expect(span.resource).to eq 'myservice.endpoint' } + specify { expect(span.get_tag('error.stack')).to be_nil } + specify { expect(span.get_tag(:some)).to eq 'datum' } + end + + context 'request response call type' do + let(:type) { :request_response } + let(:keywords) do + { request: instance_double(Object), + call: instance_double('GRPC::ActiveCall'), + method: 'MyService.Endpoint', + metadata: { some: 'datum' } } + end + + it_behaves_like 'span data contents' + end + + context 'client streaming call type' do + let(:type) { :client_streamer } + let(:keywords) do + { call: instance_double('GRPC::ActiveCall'), + method: 'MyService.Endpoint', + metadata: { some: 'datum' } } + end + + it_behaves_like 'span data contents' + end + + context 'server streaming call type' do + let(:type) { :server_streamer } + let(:keywords) do + { request: instance_double(Object), + call: instance_double('GRPC::ActiveCall'), + method: 'MyService.Endpoint', + metadata: { some: 'datum' } } + end + + specify do + expect(span.name).to eq 'grpc.client' + expect(span.span_type).to eq 'grpc' + expect(span.service).to eq 'rspec' + expect(span.resource).to eq 'myservice.endpoint' + expect(span.get_tag('error.stack')).to be_nil + expect(span.get_tag(:some)).to eq 'datum' + end + end + + context 'bidirectional streaming call type' do + let(:type) { :bidi_streamer } + let(:keywords) do + { requests: instance_double(Array), + call: instance_double('GRPC::ActiveCall'), + method: 'MyService.Endpoint', + metadata: { some: 'datum' } } + end + + it_behaves_like 'span data contents' + end + end + + context 'when intercepting on the server' do + shared_examples 'span data contents' do + specify { expect(span.name).to eq 'grpc.service' } + specify { expect(span.span_type).to eq 'grpc' } + specify { expect(span.service).to eq 'rspec' } + specify { expect(span.resource).to eq 'my.server.endpoint' } + specify { expect(span.get_tag('error.stack')).to be_nil } + specify { expect(span.get_tag(:some)).to eq 'datum' } + end + + context 'request response call type' do + let(:type) { :request_response } + let(:keywords) do + { request: instance_double(Object), + call: instance_double('GRPC::ActiveCall', metadata: { some: 'datum' }), + method: instance_double(Method, owner: 'My::Server', name: 'endpoint') } + end + + it_behaves_like 'span data contents' + end + + context 'client streaming call type' do + let(:type) { :client_streamer } + let(:keywords) do + { call: instance_double('GRPC::ActiveCall', metadata: { some: 'datum' }), + method: instance_double(Method, owner: 'My::Server', name: 'endpoint') } + end + + it_behaves_like 'span data contents' + end + + context 'server streaming call type' do + let(:type) { :server_streamer } + let(:keywords) do + { request: instance_double(Object), + call: instance_double('GRPC::ActiveCall', metadata: { some: 'datum' }), + method: instance_double(Method, owner: 'My::Server', name: 'endpoint') } + end + + it_behaves_like 'span data contents' + end + + context 'bidirectional streaming call type do' do + let(:type) { :bidi_streamer } + let(:keywords) do + { requests: instance_double(Array), + call: instance_double('GRPC::ActiveCall', metadata: { some: 'datum' }), + method: instance_double(Method, owner: 'My::Server', name: 'endpoint') } + end + + it_behaves_like 'span data contents' + end + end + end +end diff --git a/spec/ddtrace/contrib/grpc/support/grpc_helper.rb b/spec/ddtrace/contrib/grpc/support/grpc_helper.rb new file mode 100644 index 0000000000..1f665d95f9 --- /dev/null +++ b/spec/ddtrace/contrib/grpc/support/grpc_helper.rb @@ -0,0 +1,94 @@ +require 'grpc' + +module GRPCHelper + def run_request_reply(address = '0.0.0.0:50052', client = nil) + runner(address, client) { |c| c.basic(TestMessage.new) } + end + + def run_client_streamer(address = '0.0.0.0:50053', client = nil) + runner(address, client) { |c| c.stream_from_client([TestMessage.new]) } + end + + def run_server_streamer(address = '0.0.0.0:50054', client = nil) + runner(address, client) do |c| + c.stream_from_server(TestMessage.new) + sleep 0.05 + end + end + + def run_bidi_streamer(address = '0.0.0.0:50055', client = nil) + runner(address, client) do |c| + c.stream_both_ways([TestMessage.new]) + sleep 0.05 + end + end + + def runner(address, client) + server = GRPC::RpcServer.new + server.add_http2_port(address, :this_port_is_insecure) + server.handle(TestService) + + t = Thread.new { server.run } + server.wait_till_running + + client ||= TestService.rpc_stub_class.new(address, :this_channel_is_insecure) + + yield client + + server.stop + until server.stopped?; end + t.join + end + + class TestMessage + class << self + def marshal(_o) + '' + end + + def unmarshal(_o) + new + end + end + end + + class TestService + include GRPC::GenericService + + rpc :basic, TestMessage, TestMessage + rpc :stream_from_client, stream(TestMessage), TestMessage + rpc :stream_from_server, TestMessage, stream(TestMessage) + rpc :stream_both_ways, stream(TestMessage), stream(TestMessage) + + attr_reader :received_metadata + + def initialize(**keywords) + @trailing_metadata = keywords + @received_metadata = [] + end + + # provide implementations for each registered rpc interface + def basic(request, call) + call.output_metadata.update(@trailing_metadata) + @received_metadata << call.metadata unless call.metadata.nil? + TestMessage.new + end + + def stream_from_client(call) + call.output_metadata.update(@trailing_metadata) + call.each_remote_read.each { |r| r } + TestMessage.new + end + + def stream_from_server(_request, call) + call.output_metadata.update(@trailing_metadata) + [TestMessage.new, TestMessage.new] + end + + def stream_both_ways(requests, call) + call.output_metadata.update(@trailing_metadata) + call.each_remote_read.each { |r| r } + [TestMessage.new, TestMessage.new] + end + end +end diff --git a/spec/ddtrace/contrib/mongodb/client_spec.rb b/spec/ddtrace/contrib/mongodb/client_spec.rb new file mode 100644 index 0000000000..04d83b8864 --- /dev/null +++ b/spec/ddtrace/contrib/mongodb/client_spec.rb @@ -0,0 +1,323 @@ +require 'spec_helper' + +require 'ddtrace' +require 'mongo' + +RSpec.describe 'Mongo::Client instrumentation' do + let(:tracer) { Datadog::Tracer.new(writer: FauxWriter.new) } + + let(:client) { Mongo::Client.new(*client_options) } + let(:client_options) { [["#{host}:#{port}"], { database: database }] } + let(:host) { ENV.fetch('TEST_MONGODB_HOST', '127.0.0.1') } + let(:port) { ENV.fetch('TEST_MONGODB_PORT', 27017) } + let(:database) { 'test' } + let(:collection) { :artists } + + let(:pin) { Datadog::Pin.get_from(client) } + let(:spans) { tracer.writer.spans(:keep) } + let(:span) { spans.first } + + def discard_spans! + tracer.writer.spans + end + + before(:each) do + # Disable Mongo logging + Mongo::Logger.logger.level = ::Logger::WARN + + Datadog.configure do |c| + c.use :mongo + end + + # Have to manually update this because its still + # using global pin instead of configuration. + # Remove this when we remove the pin. + pin.tracer = tracer + end + + # Clear data between tests + after(:each) do + client.database.drop + end + + it 'evaluates the block given to the constructor' do + expect { |b| Mongo::Client.new(*client_options, &b) }.to yield_control + end + + context 'pin' do + it 'has the correct attributes' do + expect(pin.service).to eq('mongodb') + expect(pin.app).to eq('mongodb') + expect(pin.app_type).to eq('db') + end + + context 'when the service is changed' do + let(:service) { 'mongodb-primary' } + before(:each) { pin.service = service } + + it 'produces spans with the correct service' do + client[collection].insert_one(name: 'FKA Twigs') + expect(spans).to have(1).items + expect(spans.first.service).to eq(service) + end + end + + context 'when the tracer is disabled' do + before(:each) { pin.tracer.enabled = false } + + it 'produces spans with the correct service' do + client[collection].insert_one(name: 'FKA Twigs') + expect(spans).to be_empty + end + end + end + + # rubocop:disable Metrics/LineLength + describe 'tracing' do + shared_examples_for 'a MongoDB trace' do + it 'has basic properties' do + expect(spans).to have(1).items + expect(span.service).to eq(pin.service) + expect(span.span_type).to eq('mongodb') + expect(span.get_tag('mongodb.db')).to eq(database) + expect(span.get_tag('mongodb.collection')).to eq(collection.to_s) + expect(span.get_tag('out.host')).to eq(host) + expect(span.get_tag('out.port')).to eq(port.to_s) + end + end + + describe '#insert_one operation' do + before(:each) { client[collection].insert_one(params) } + + context 'for a basic document' do + let(:params) { { name: 'FKA Twigs' } } + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>:insert, \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\", \"documents\"=>[{:name=>\"?\"}], \"ordered\"=>\"?\"}") + expect(span.get_tag('mongodb.rows')).to eq('1') + end + end + + context 'for a document with an array' do + let(:params) { { name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing'] } } + let(:collection) { :people } + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>:insert, \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\", \"documents\"=>[{:name=>\"?\", :hobbies=>[\"?\"]}], \"ordered\"=>\"?\"}") + expect(span.get_tag('mongodb.rows')).to eq('1') + end + end + end + + describe '#insert_many operation' do + before(:each) { client[collection].insert_many(params) } + + context 'for documents with arrays' do + let(:params) do + [ + { name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing'] }, + { name: 'Sally', hobbies: ['skiing', 'stamp collecting'] } + ] + end + + let(:collection) { :people } + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>:insert, \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\", \"documents\"=>[{:name=>\"?\", :hobbies=>[\"?\"]}, \"?\"], \"ordered\"=>\"?\"}") + expect(span.get_tag('mongodb.rows')).to eq('2') + end + end + end + + describe '#find_all operation' do + let(:collection) { :people } + + before(:each) do + # Insert a document + client[collection].insert_one(name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing']) + discard_spans! + + # Do #find_all operation + client[collection].find.each do |document| + # => Yields a BSON::Document. + end + end + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>\"find\", \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\", \"filter\"=>{}}") + expect(span.get_tag('mongodb.rows')).to be nil + end + end + + describe '#find operation' do + let(:collection) { :people } + + before(:each) do + # Insert a document + client[collection].insert_one(name: 'Steve', hobbies: ['hiking']) + discard_spans! + + # Do #find operation + result = client[collection].find(name: 'Steve').first[:hobbies] + expect(result).to eq(['hiking']) + end + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>\"find\", \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\", \"filter\"=>{\"name\"=>\"?\"}}") + expect(span.get_tag('mongodb.rows')).to be nil + end + end + + describe '#update_one operation' do + let(:collection) { :people } + + before(:each) do + # Insert a document + client[collection].insert_one(name: 'Sally', hobbies: ['skiing', 'stamp collecting']) + discard_spans! + + # Do #update_one operation + client[collection].update_one({ name: 'Sally' }, '$set' => { 'phone_number' => '555-555-5555' }) + end + + after(:each) do + # Verify correctness of the operation + expect(client[collection].find(name: 'Sally').first[:phone_number]).to eq('555-555-5555') + end + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>:update, \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\", \"updates\"=>[{\"q\"=>{\"name\"=>\"?\"}, \"u\"=>{\"$set\"=>{\"phone_number\"=>\"?\"}}, \"multi\"=>\"?\", \"upsert\"=>\"?\"}], \"ordered\"=>\"?\"}") + expect(span.get_tag('mongodb.rows')).to eq('1') + end + end + + describe '#update_many operation' do + let(:collection) { :people } + let(:documents) do + [ + { name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing'] }, + { name: 'Sally', hobbies: ['skiing', 'stamp collecting'] } + ] + end + + before(:each) do + # Insert documents + client[collection].insert_many(documents) + discard_spans! + + # Do #update_many operation + client[collection].update_many({}, '$set' => { 'phone_number' => '555-555-5555' }) + end + + after(:each) do + # Verify correctness of the operation + documents.each do |d| + expect(client[collection].find(name: d[:name]).first[:phone_number]).to eq('555-555-5555') + end + end + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>:update, \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\", \"updates\"=>[{\"q\"=>{}, \"u\"=>{\"$set\"=>{\"phone_number\"=>\"?\"}}, \"multi\"=>\"?\", \"upsert\"=>\"?\"}], \"ordered\"=>\"?\"}") + expect(span.get_tag('mongodb.rows')).to eq('2') + end + end + + describe '#delete_one operation' do + let(:collection) { :people } + + before(:each) do + # Insert a document + client[collection].insert_one(name: 'Sally', hobbies: ['skiing', 'stamp collecting']) + discard_spans! + + # Do #delete_one operation + client[collection].delete_one(name: 'Sally') + end + + after(:each) do + # Verify correctness of the operation + expect(client[collection].find(name: 'Sally').count).to eq(0) + end + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>:delete, \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\", \"deletes\"=>[{\"q\"=>{\"name\"=>\"?\"}, \"limit\"=>\"?\"}], \"ordered\"=>\"?\"}") + expect(span.get_tag('mongodb.rows')).to eq('1') + end + end + + describe '#delete_many operation' do + let(:collection) { :people } + let(:documents) do + [ + { name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing'] }, + { name: 'Sally', hobbies: ['skiing', 'stamp collecting'] } + ] + end + + before(:each) do + # Insert documents + client[collection].insert_many(documents) + discard_spans! + + # Do #delete_many operation + client[collection].delete_many(name: /$S*/) + end + + after(:each) do + # Verify correctness of the operation + documents.each do |d| + expect(client[collection].find(name: d[:name]).count).to eq(0) + end + end + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>:delete, \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\", \"deletes\"=>[{\"q\"=>{\"name\"=>\"?\"}, \"limit\"=>\"?\"}], \"ordered\"=>\"?\"}") + expect(span.get_tag('mongodb.rows')).to eq('2') + end + end + + describe '#drop operation' do + let(:collection) { 1 } # Because drop operation doesn't have a collection + + before(:each) { client.database.drop } + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>:dropDatabase, \"database\"=>\"#{database}\", \"collection\"=>1}") + expect(span.get_tag('mongodb.rows')).to be nil + end + end + + describe 'a failed query' do + before(:each) { client[:artists].drop } + + it_behaves_like 'a MongoDB trace' + + it 'has operation-specific properties' do + expect(span.resource).to eq("{\"operation\"=>:drop, \"database\"=>\"#{database}\", \"collection\"=>\"#{collection}\"}") + expect(span.get_tag('mongodb.rows')).to be nil + expect(span.status).to eq(1) + expect(span.get_tag('error.msg')).to eq('ns not found (26)') + end + end + end +end diff --git a/spec/ddtrace/contrib/mysql2/patcher_spec.rb b/spec/ddtrace/contrib/mysql2/patcher_spec.rb new file mode 100644 index 0000000000..8d883da6f3 --- /dev/null +++ b/spec/ddtrace/contrib/mysql2/patcher_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +require 'ddtrace' +require 'mysql2' + +RSpec.describe 'Mysql2::Client patcher' do + let(:tracer) { Datadog::Tracer.new(writer: FauxWriter.new) } + + let(:client) do + Mysql2::Client.new( + host: host, + port: port, + database: database, + username: username, + password: password + ) + end + + let(:host) { ENV.fetch('TEST_MYSQL_HOST') { '127.0.0.1' } } + let(:port) { ENV.fetch('TEST_MYSQL_PORT') { '3306' } } + let(:database) { ENV.fetch('TEST_MYSQL_DB') { 'mysql' } } + let(:username) { ENV.fetch('TEST_MYSQL_USERNAME') { 'root' } } + let(:password) { ENV.fetch('TEST_MYSQL_PASSWORD') { 'root' } } + + let(:pin) { client.datadog_pin } + let(:spans) { tracer.writer.spans(:keep) } + let(:span) { spans.first } + + before(:each) do + Datadog.configure do |c| + c.use :mysql2, service_name: 'my-sql', tracer: tracer + end + end + + context 'pin' do + it 'has the correct attributes' do + expect(pin.service).to eq('my-sql') + expect(pin.app).to eq('mysql2') + expect(pin.app_type).to eq('db') + end + end + + describe 'tracing' do + describe '#query' do + describe 'disabled tracer' do + before(:each) { tracer.enabled = false } + + it 'does not write spans' do + client.query('SELECT 1') + expect(spans).to be_empty + end + end + + it 'traces successful queries' do + client.query('SELECT 1') + expect(spans.count).to eq(1) + expect(span.get_tag('mysql2.db.name')).to eq(database) + expect(span.get_tag('out.host')).to eq(host) + expect(span.get_tag('out.port')).to eq(port) + end + + it 'traces failed queries' do + expect { client.query('SELECT INVALID') }.to raise_error(Mysql2::Error) + + expect(spans.count).to eq(1) + expect(span.status).to eq(1) + expect(span.get_tag('error.msg')) + .to eq("Unknown column 'INVALID' in 'field list'") + end + end + end +end diff --git a/spec/ddtrace/contrib/racecar/patcher_spec.rb b/spec/ddtrace/contrib/racecar/patcher_spec.rb index a3f2f4eaa3..9c2361d104 100644 --- a/spec/ddtrace/contrib/racecar/patcher_spec.rb +++ b/spec/ddtrace/contrib/racecar/patcher_spec.rb @@ -15,14 +15,6 @@ def all_spans Datadog.configure do |c| c.use :racecar, tracer: tracer end - - # Make sure to update the subscription tracer, - # so we aren't writing to a stale tracer. - if Datadog::Contrib::Racecar::Patcher.patched? - Datadog::Contrib::Racecar::Patcher.subscriptions.each do |subscription| - allow(subscription).to receive(:tracer).and_return(tracer) - end - end end describe 'for single message processing' do @@ -40,7 +32,7 @@ def all_spans end let(:racecar_span) do - all_spans.select { |s| s.name == Datadog::Contrib::Racecar::Patcher::NAME_MESSAGE }.first + all_spans.select { |s| s.name == Datadog::Contrib::Racecar::Events::Message::SPAN_NAME }.first end context 'that doesn\'t raise an error' do @@ -108,7 +100,7 @@ def all_spans end let(:racecar_span) do - all_spans.select { |s| s.name == Datadog::Contrib::Racecar::Patcher::NAME_BATCH }.first + all_spans.select { |s| s.name == Datadog::Contrib::Racecar::Events::Batch::SPAN_NAME }.first end context 'that doesn\'t raise an error' do diff --git a/spec/ddtrace/contrib/rake/instrumentation_spec.rb b/spec/ddtrace/contrib/rake/instrumentation_spec.rb new file mode 100644 index 0000000000..2de91fe49a --- /dev/null +++ b/spec/ddtrace/contrib/rake/instrumentation_spec.rb @@ -0,0 +1,230 @@ +require 'spec_helper' + +require 'securerandom' +require 'rake' +require 'rake/tasklib' +require 'ddtrace' +require 'ddtrace/contrib/rake/patcher' + +RSpec.describe Datadog::Contrib::Rake::Instrumentation do + let(:tracer) { Datadog::Tracer.new(writer: FauxWriter.new) } + let(:configuration_options) { { tracer: tracer, enabled: true } } + let(:spans) { tracer.writer.spans } + let(:span) { spans.first } + + before(:each) do + skip('Rake integration incompatible.') unless Datadog::Contrib::Rake::Patcher.compatible? + + # Reset options (that might linger from other tests) + Datadog.configuration[:rake].reset_options! + + # Patch Rake + Datadog.configure do |c| + c.use :rake, configuration_options + end + end + + after(:each) do + # We don't want instrumentation enabled during the rest of the test suite... + Datadog.configure do |c| + c.use :rake, enabled: false + end + end + + def reset_task!(task_name) + if Rake::Task.task_defined?(task_name) + Rake::Task[task_name].reenable + Rake::Task[task_name].clear + + # Rake prior to version 12.0 doesn't clear args when #clear is invoked. + # Perform a more invasive reset, to make sure its reusable. + if Gem::Version.new(Rake::VERSION) < Gem::Version.new('12.0') + Rake::Task[task_name].instance_variable_set(:@arg_names, nil) + end + end + end + + let(:task_name) { :test_rake_instrumentation } + let(:task_body) { proc { |task, args| spy.call(task, args) } } + let(:task_arg_names) { [] } + let(:task_class) do + stub_const('RakeInstrumentationTestTask', Class.new(Rake::TaskLib)).tap do |task_class| + tb = task_body + task_class.send(:define_method, :initialize) do |name = task_name, *args| + task(name, *args, &tb) + end + end + end + let(:task) { Rake::Task[task_name] } + let(:spy) { double('spy') } + + describe '#invoke' do + shared_examples_for 'a single task execution' do + before(:each) do + expect(spy).to receive(:call) do |invocation_task, invocation_args| + expect(invocation_task).to eq(task) + expect(invocation_args.to_hash).to eq(args_hash) + end + task.invoke(*args) + end + + let(:invoke_span) { spans.find { |s| s.name == described_class::SPAN_NAME_INVOKE } } + let(:execute_span) { spans.find { |s| s.name == described_class::SPAN_NAME_EXECUTE } } + + it do + expect(spans).to have(2).items + end + + describe '\'rake.invoke\' span' do + it do + expect(invoke_span.name).to eq(described_class::SPAN_NAME_INVOKE) + expect(invoke_span.resource).to eq(task_name.to_s) + expect(invoke_span.parent_id).to eq(0) + end + end + + describe '\'rake.execute\' span' do + it do + expect(execute_span.name).to eq(described_class::SPAN_NAME_EXECUTE) + expect(execute_span.resource).to eq(task_name.to_s) + expect(execute_span.parent_id).to eq(invoke_span.span_id) + end + end + end + + context 'for a task' do + let(:args_hash) { {} } + let(:task_arg_names) { args_hash.keys } + let(:args) { args_hash.values } + + let(:define_task!) do + reset_task!(task_name) + Rake::Task.define_task(task_name, *task_arg_names, &task_body) + end + + before(:each) { define_task! } + + context 'without args' do + it_behaves_like 'a single task execution' do + describe '\'rake.invoke\' span tags' do + it do + expect(invoke_span.get_tag('rake.task.arg_names')).to eq([].to_s) + expect(invoke_span.get_tag('rake.invoke.args')).to eq(['?'].to_s) + end + end + + describe '\'rake.execute\' span tags' do + it do + expect(execute_span.get_tag('rake.task.arg_names')).to be nil + expect(execute_span.get_tag('rake.execute.args')).to eq({}.to_s) + end + end + end + end + + context 'with args' do + let(:args_hash) { { one: 1, two: 2, three: 3 } } + it_behaves_like 'a single task execution' do + describe '\'rake.invoke\' span tags' do + it do + expect(invoke_span.get_tag('rake.task.arg_names')).to eq([:one, :two, :three].to_s) + expect(invoke_span.get_tag('rake.invoke.args')).to eq(['?'].to_s) + end + end + + describe '\'rake.execute\' span tags' do + it do + expect(execute_span.get_tag('rake.arg_names')).to be nil + expect(execute_span.get_tag('rake.execute.args')).to eq({ one: '?', two: '?', three: '?' }.to_s) + end + end + end + end + + context 'with a prerequisite task' do + let(:prerequisite_task_name) { :test_rake_instrumentation_prerequisite } + let(:prerequisite_task_body) { proc { |task, args| prerequisite_spy.call(task, args) } } + let(:prerequisite_spy) { double('prerequisite spy') } + let(:prerequisite_task) { Rake::Task[prerequisite_task_name] } + + let(:define_task!) do + reset_task!(task_name) + reset_task!(prerequisite_task_name) + Rake::Task.define_task(prerequisite_task_name, &prerequisite_task_body) + Rake::Task.define_task(task_name => prerequisite_task_name, &task_body) + end + + before(:each) do + expect(prerequisite_spy).to receive(:call) do |invocation_task, invocation_args| + expect(invocation_task).to eq(prerequisite_task) + expect(invocation_args.to_hash).to eq({}) + end.ordered + + expect(spy).to receive(:call) do |invocation_task, invocation_args| + expect(invocation_task).to eq(task) + expect(invocation_args.to_hash).to eq(args_hash) + end.ordered + + task.invoke(*args) + end + + let(:invoke_span) { spans.find { |s| s.name == described_class::SPAN_NAME_INVOKE } } + let(:prerequisite_task_execute_span) do + spans.find do |s| + s.name == described_class::SPAN_NAME_EXECUTE \ + && s.resource == prerequisite_task_name.to_s + end + end + let(:task_execute_span) do + spans.find do |s| + s.name == described_class::SPAN_NAME_EXECUTE \ + && s.resource == task_name.to_s + end + end + + it do + expect(spans).to have(3).items + end + + describe '\'rake.invoke\' span' do + it do + expect(invoke_span.name).to eq(described_class::SPAN_NAME_INVOKE) + expect(invoke_span.resource).to eq(task_name.to_s) + expect(invoke_span.parent_id).to eq(0) + expect(invoke_span.get_tag('rake.task.arg_names')).to eq([].to_s) + expect(invoke_span.get_tag('rake.invoke.args')).to eq(['?'].to_s) + end + end + + describe 'prerequisite \'rake.execute\' span' do + it do + expect(prerequisite_task_execute_span.name).to eq(described_class::SPAN_NAME_EXECUTE) + expect(prerequisite_task_execute_span.resource).to eq(prerequisite_task_name.to_s) + expect(prerequisite_task_execute_span.parent_id).to eq(invoke_span.span_id) + expect(prerequisite_task_execute_span.get_tag('rake.task.arg_names')).to be nil + expect(prerequisite_task_execute_span.get_tag('rake.execute.args')).to eq({}.to_s) + end + end + + describe 'task \'rake.execute\' span' do + it do + expect(task_execute_span.name).to eq(described_class::SPAN_NAME_EXECUTE) + expect(task_execute_span.resource).to eq(task_name.to_s) + expect(task_execute_span.parent_id).to eq(invoke_span.span_id) + expect(task_execute_span.get_tag('rake.task.arg_names')).to be nil + expect(task_execute_span.get_tag('rake.execute.args')).to eq({}.to_s) + end + end + end + + context 'defined by a class' do + let(:define_task!) do + reset_task!(task_name) + task_class.new(task_name, *task_arg_names) + end + + it_behaves_like 'a single task execution' + end + end + end +end diff --git a/spec/ddtrace/contrib/resque/instrumentation_spec.rb b/spec/ddtrace/contrib/resque/instrumentation_spec.rb new file mode 100644 index 0000000000..c708844edc --- /dev/null +++ b/spec/ddtrace/contrib/resque/instrumentation_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' +require_relative 'job' + +require 'ddtrace' + +RSpec.describe 'Resque instrumentation' do + include_context 'Resque job' + + let(:tracer) { ::Datadog::Tracer.new(writer: FauxWriter.new) } + let(:pin) { ::Resque.datadog_pin } + let(:spans) { tracer.writer.spans } + let(:span) { spans.first } + + let(:url) { "redis://#{host}:#{port}" } + let(:host) { ENV.fetch('TEST_REDIS_HOST', '127.0.0.1') } + let(:port) { ENV.fetch('TEST_REDIS_PORT', 6379) } + + before(:each) do + # Setup Resque to use Redis + ::Resque.redis = url + ::Resque::Failure.clear + + # Patch Resque + Datadog.configure do |c| + c.use :resque + end + + # Update the Resque pin with the tracer + pin.tracer = tracer + end + + describe 'for a job' do + context 'that succeeds' do + before(:each) { perform_job(job_class) } + + it 'is traced' do + expect(spans).to have(1).items + expect(Resque::Failure.count).to be(0) + expect(span.name).to eq('resque.job') + expect(span.resource).to eq(job_class.name) + expect(span.span_type).to eq(Datadog::Ext::AppTypes::WORKER) + expect(span.service).to eq('resque') + expect(span.status).to_not eq(Datadog::Ext::Errors::STATUS) + end + end + + context 'that fails' do + before(:each) do + # Rig the job to fail + expect(job_class).to receive(:perform) do + raise error_class, error_message + end + + # Perform it + perform_job(job_class) + end + + let(:error_class_name) { 'TestJobFailError' } + let(:error_class) { stub_const(error_class_name, Class.new(StandardError)) } + let(:error_message) { 'TestJob failed' } + + it 'is traced' do + expect(spans).to have(1).items + expect(Resque::Failure.count).to be(1) + expect(Resque::Failure.all['error']).to eq(error_message) + expect(span.name).to eq('resque.job') + expect(span.resource).to eq(job_class.name) + expect(span.span_type).to eq(Datadog::Ext::AppTypes::WORKER) + expect(span.service).to eq('resque') + expect(span.get_tag(Datadog::Ext::Errors::MSG)).to eq(error_message) + expect(span.status).to eq(Datadog::Ext::Errors::STATUS) + expect(span.get_tag(Datadog::Ext::Errors::TYPE)).to eq(error_class_name) + end + end + + context 'trace context' do + before(:each) do + expect(job_class).to receive(:perform) do + expect(tracer.active_span).to be_a_kind_of(Datadog::Span) + expect(tracer.active_span.parent_id).to eq(0) + end + + tracer.trace('main.process') do + perform_job(job_class) + end + end + + let(:main_span) { spans.first } + let(:job_span) { spans.last } + + it 'is clean' do + expect(spans).to have(2).items + expect(Resque::Failure.count).to be(0) + expect(main_span.name).to eq('main.process') + expect(job_span.name).to eq('resque.job') + expect(main_span.trace_id).to_not eq(job_span.trace_id) + end + end + end + + describe 'patching for workers' do + let(:worker_class_1) { Class.new } + let(:worker_class_2) { Class.new } + + before(:each) do + # Remove the patch so it applies new patch + remove_patch!(:resque) + + # Re-apply patch, to workers + Datadog.configure do |c| + c.use(:resque, workers: [worker_class_1, worker_class_2]) + end + end + + it 'adds the instrumentation module' do + expect(worker_class_1.singleton_class.included_modules).to include(Datadog::Contrib::Resque::ResqueJob) + expect(worker_class_2.singleton_class.included_modules).to include(Datadog::Contrib::Resque::ResqueJob) + end + end +end diff --git a/spec/ddtrace/contrib/resque/job.rb b/spec/ddtrace/contrib/resque/job.rb new file mode 100644 index 0000000000..8718a3e501 --- /dev/null +++ b/spec/ddtrace/contrib/resque/job.rb @@ -0,0 +1,28 @@ +LogHelpers.without_warnings do + require 'resque' +end + +require 'ddtrace/contrib/resque/resque_job' + +RSpec.shared_context 'Resque job' do + def perform_job(klass, *args) + job = Resque::Job.new(queue_name, 'class' => klass, 'args' => args) + worker.perform(job) + end + + let(:queue_name) { :test_queue } + let(:worker) { Resque::Worker.new(queue_name) } + let(:job_class) do + stub_const('TestJob', Module.new).tap do |mod| + mod.send(:extend, Datadog::Contrib::Resque::ResqueJob) + mod.send(:define_singleton_method, :perform) do + # Do nothing by default. + end + end + end + + before(:each) do + Resque.after_fork { Datadog::Pin.get_from(Resque).tracer.writer = FauxWriter.new } + Resque.before_first_fork.each(&:call) + end +end diff --git a/spec/ddtrace/contrib/sequel/configuration_spec.rb b/spec/ddtrace/contrib/sequel/configuration_spec.rb new file mode 100644 index 0000000000..60b9961b51 --- /dev/null +++ b/spec/ddtrace/contrib/sequel/configuration_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +require 'time' +require 'sequel' +require 'ddtrace' +require 'ddtrace/contrib/sequel/patcher' + +RSpec.describe 'Sequel configuration' do + let(:tracer) { Datadog::Tracer.new(writer: FauxWriter.new) } + let(:spans) { tracer.writer.spans } + let(:span) { spans.first } + + before(:each) do + skip unless Datadog::Contrib::Sequel::Patcher.compatible? + end + + describe 'for a SQLite database' do + let(:sequel) do + Sequel.sqlite(':memory:').tap do |db| + db.create_table(:table) do + String :name + end + end + end + + def perform_query! + sequel[:table].insert(name: 'data1') + end + + describe 'when configured' do + after(:each) { Datadog.configuration[:sequel].reset_options! } + + context 'only with defaults' do + # Expect it to be the normalized adapter name. + it do + Datadog.configure { |c| c.use :sequel, tracer: tracer } + perform_query! + expect(span.service).to eq('sqlite') + end + end + + context 'with options set via #use' do + let(:service_name) { 'my-sequel' } + + it do + Datadog.configure { |c| c.use :sequel, tracer: tracer, service_name: service_name } + perform_query! + expect(span.service).to eq(service_name) + end + end + + context 'with options set on Sequel::Database' do + let(:service_name) { 'custom-sequel' } + + it do + Datadog.configure { |c| c.use :sequel, tracer: tracer } + Datadog.configure(sequel, service_name: service_name) + perform_query! + expect(span.service).to eq(service_name) + end + end + + context 'after the database has been initialized' do + # NOTE: This test really only works when run in isolation. + # It relies on Sequel not being patched, and there's + # no way to unpatch it once its happened in other tests. + it do + sequel + Datadog.configure { |c| c.use :sequel, tracer: tracer } + perform_query! + expect(span.service).to eq('sqlite') + end + end + end + end +end diff --git a/spec/ddtrace/contrib/sequel/instrumentation_spec.rb b/spec/ddtrace/contrib/sequel/instrumentation_spec.rb new file mode 100644 index 0000000000..ed4f0266c5 --- /dev/null +++ b/spec/ddtrace/contrib/sequel/instrumentation_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +require 'time' +require 'sequel' +require 'ddtrace' +require 'ddtrace/contrib/sequel/patcher' + +RSpec.describe 'Sequel instrumentation' do + let(:tracer) { Datadog::Tracer.new(writer: FauxWriter.new) } + let(:configuration_options) { { tracer: tracer } } + let(:sequel) do + Sequel.sqlite(':memory:').tap do |s| + Datadog.configure(s, tracer: tracer) + end + end + + let(:spans) { tracer.writer.spans } + + before(:each) do + skip unless Datadog::Contrib::Sequel::Patcher.compatible? + + # Reset options (that might linger from other tests) + Datadog.configuration[:sequel].reset_options! + + # Patch Sequel + Datadog.configure do |c| + c.use :sequel, configuration_options + end + end + + describe 'for a SQLite database' do + before(:each) do + sequel.create_table(:table) do + String :name + end + end + + describe 'when queried through a Sequel::Database object' do + before(:each) { sequel.run(query) } + let(:query) { 'SELECT * FROM \'table\' WHERE `name` = \'John Doe\'' } + let(:span) { spans.first } + + it 'traces the command' do + expect(span.name).to eq('sequel.query') + # Expect it to be the normalized adapter name. + expect(span.service).to eq('sqlite') + expect(span.span_type).to eq('sql') + expect(span.get_tag('sequel.db.vendor')).to eq('sqlite') + # Expect non-quantized query: agent does SQL quantization. + expect(span.resource).to eq(query) + expect(span.status).to eq(0) + expect(span.parent_id).to eq(0) + end + end + + describe 'when queried through a Sequel::Dataset' do + let(:process_span) { spans[0] } + let(:publish_span) { spans[1] } + let(:sequel_cmd1_span) { spans[2] } + let(:sequel_cmd2_span) { spans[3] } + let(:sequel_cmd3_span) { spans[4] } + let(:sequel_cmd4_span) { spans[5] } + + before(:each) do + tracer.trace('publish') do |span| + span.service = 'webapp' + span.resource = '/index' + tracer.trace('process') do |subspan| + subspan.service = 'datalayer' + subspan.resource = 'home' + sequel[:table].insert(name: 'data1') + sequel[:table].insert(name: 'data2') + data = sequel[:table].select.to_a + expect(data.length).to eq(2) + data.each do |row| + expect(row[:name]).to match(/^data.$/) + end + end + end + end + + it do + expect(spans).to have(6).items + + # Check publish span + expect(publish_span.name).to eq('publish') + expect(publish_span.service).to eq('webapp') + expect(publish_span.resource).to eq('/index') + expect(publish_span.span_id).to_not eq(publish_span.trace_id) + expect(publish_span.parent_id).to eq(0) + + # Check process span + expect(process_span.name).to eq('process') + expect(process_span.service).to eq('datalayer') + expect(process_span.resource).to eq('home') + expect(process_span.parent_id).to eq(publish_span.span_id) + expect(process_span.trace_id).to eq(publish_span.trace_id) + + # Check each command span + [ + [sequel_cmd1_span, 'INSERT INTO `table` (`name`) VALUES (\'data1\')'], + [sequel_cmd2_span, 'INSERT INTO `table` (`name`) VALUES (\'data2\')'], + [sequel_cmd3_span, 'SELECT * FROM `table`'], + [sequel_cmd4_span, 'SELECT sqlite_version()'] + ].each do |command_span, query| + expect(command_span.name).to eq('sequel.query') + # Expect it to be the normalized adapter name. + expect(command_span.service).to eq('sqlite') + expect(command_span.span_type).to eq('sql') + expect(command_span.get_tag('sequel.db.vendor')).to eq('sqlite') + # Expect non-quantized query: agent does SQL quantization. + expect(command_span.resource).to eq(query) + expect(command_span.status).to eq(0) + expect(command_span.parent_id).to eq(process_span.span_id) + expect(command_span.trace_id).to eq(publish_span.trace_id) + end + end + end + end +end diff --git a/spec/ddtrace/pin_spec.rb b/spec/ddtrace/pin_spec.rb new file mode 100644 index 0000000000..c6d1d80182 --- /dev/null +++ b/spec/ddtrace/pin_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' + +require 'ddtrace' + +RSpec.describe Datadog::Pin do + subject(:pin) { described_class.new(service_name, options) } + + let(:service_name) { 'test-service' } + let(:options) { {} } + let(:target) { Object.new } + + describe '#initialize' do + before(:each) { pin } + + context 'when given some options' do + let(:options) { { app: 'anapp' } } + + it do + expect(pin.service).to eq(service_name) + expect(pin.app).to eq(options[:app]) + end + end + + context 'when given sufficient info' do + let(:options) { { app: 'test-app', app_type: 'test-type', tracer: tracer } } + let(:tracer) { Datadog::Tracer.new(writer: FauxWriter.new) } + + it 'sets the service info' do + expect(tracer.services.key?(service_name)).to be true + expect(tracer.services[service_name]).to eq( + 'app' => 'test-app', 'app_type' => 'test-type' + ) + end + end + + context 'when given insufficient info' do + let(:options) { { app_type: 'test-type', tracer: tracer } } + let(:tracer) { Datadog::Tracer.new(writer: FauxWriter.new) } + + it 'does not sets the service info' do + expect(tracer.services).to be_empty + end + end + end + + describe '#onto' do + let(:options) { { app: 'anapp' } } + let(:returned_pin) { described_class.get_from(target) } + + before(:each) { pin.onto(target) } + + it 'attaches the pin to the target' do + expect(returned_pin.service).to eq(service_name) + expect(returned_pin.app).to eq(options[:app]) + end + end + + describe '#get_from' do + subject(:returned_pin) { described_class.get_from(target) } + + context 'called against' do + context '0' do + let(:target) { 0 } + it { is_expected.to be nil } + end + + context 'nil' do + let(:target) { nil } + it { is_expected.to be nil } + end + + context 'self' do + let(:target) { self } + it { is_expected.to be nil } + end + end + + context 'when a custom pin has already been defined' do + let(:target_class) do + Class.new do + def datadog_pin + @custom_attribute + end + + def datadog_pin=(pin) + @custom_attribute = 'The PIN is set!' + end + end + end + + let(:target) { target_class.new } + before(:each) { pin.onto(target) } + + it 'returns the custom pin' do + is_expected.to eq('The PIN is set!') + end + end + end + + describe '#to_s' do + subject(:string) { pin.to_s } + let(:service_name) { 'abc' } + let(:options) { { app: 'anapp', app_type: 'db' } } + it { is_expected.to eq('Pin(service:abc,app:anapp,app_type:db,name:)') } + end + + describe '#datadog_pin' do + let(:returned_pin) { target.datadog_pin } + before(:each) { pin.onto(target) } + it { expect(returned_pin.service).to eq(service_name) } + end + + describe '#enabled?' do + subject(:enabled) { pin.enabled? } + it { is_expected.to be true } + + context 'when the tracer is disabled' do + let(:options) { { tracer: Datadog::Tracer.new(writer: FauxWriter.new) } } + before(:each) { pin.tracer.enabled = false } + it { is_expected.to be false } + end + end +end diff --git a/spec/ddtrace/propagation/grpc_propagator_spec.rb b/spec/ddtrace/propagation/grpc_propagator_spec.rb new file mode 100644 index 0000000000..d6e6d3444a --- /dev/null +++ b/spec/ddtrace/propagation/grpc_propagator_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' +require 'ddtrace/context' +require 'ddtrace/propagation/grpc_propagator' + +RSpec.describe Datadog::GRPCPropagator do + describe '.inject!' do + subject { described_class } + + let(:span_context) do + Datadog::Context.new(trace_id: 1234567890, + span_id: 9876543210, + sampling_priority: sampling_priority) + end + + let(:sampling_priority) { nil } + + let(:metadata) { {} } + + before { subject.inject!(span_context, metadata) } + + it 'injects the context trace id into the gRPC metadata' do + expect(metadata).to include('x-datadog-trace-id' => '1234567890') + end + + it 'injects the context parent span id into the gRPC metadata' do + expect(metadata).to include('x-datadog-parent-id' => '9876543210') + end + + context 'when sampling priority set on context' do + let(:sampling_priority) { 0 } + + it 'injects the sampling priority into the gRPC metadata' do + expect(metadata).to include('x-datadog-sampling-priority' => '0') + end + end + + context 'when sampling priority not set on context' do + it 'leaves the sampling priority blank in the gRPC metadata' do + expect(metadata).not_to include('x-datadog-sampling-priority') + end + end + end + + describe '.extract' do + subject { described_class.extract(metadata) } + + context 'given empty metadata' do + let(:metadata) { {} } + + it 'returns an empty context' do + expect(subject.trace_id).to be_nil + expect(subject.span_id).to be_nil + expect(subject.sampling_priority).to be_nil + end + end + + context 'given populated metadata' do + let(:metadata) do + { 'x-datadog-trace-id' => '1234567890', + 'x-datadog-parent-id' => '9876543210', + 'x-datadog-sampling-priority' => '0' } + end + + it 'returns a populated context' do + expect(subject.trace_id).to eq 1234567890 + expect(subject.span_id).to eq 9876543210 + expect(subject.sampling_priority).to be_zero + end + end + end +end diff --git a/spec/ddtrace/quantization/hash_spec.rb b/spec/ddtrace/quantization/hash_spec.rb new file mode 100644 index 0000000000..cdf6a73f36 --- /dev/null +++ b/spec/ddtrace/quantization/hash_spec.rb @@ -0,0 +1,84 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'ddtrace/quantization/hash' + +RSpec.describe Datadog::Quantization::Hash do + describe '#format' do + subject(:result) { described_class.format(hash, options) } + let(:options) { {} } + + context 'given a Hash' do + let(:hash) { { one: 'foo', two: 'bar', three: 'baz' } } + + context 'default behavior' do + it { is_expected.to eq(one: '?', two: '?', three: '?') } + end + + context 'with show: value' do + let(:options) { { show: [:two] } } + it { is_expected.to eq(one: '?', two: 'bar', three: '?') } + end + + context 'with show: :all' do + let(:options) { { show: :all } } + it { is_expected.to eq(hash) } + end + + context 'with exclude: value' do + let(:options) { { exclude: [:three] } } + it { is_expected.to eq(one: '?', two: '?') } + end + + context 'with exclude: value with indifferent key matching' do + let(:options) { { exclude: ['three'] } } + it { is_expected.to eq(one: '?', two: '?') } + end + + context 'with exclude: :all' do + let(:options) { { exclude: :all } } + it { is_expected.to eq({}) } + end + end + + context 'given an Array' do + let(:hash) { %w[foo bar baz] } + + context 'default behavior' do + it { is_expected.to eq(['?']) } + end + + context 'with show: value' do + let(:options) { { show: [:two] } } + it { is_expected.to eq(['?']) } + end + + context 'with show: :all' do + let(:options) { { show: :all } } + it { is_expected.to eq(hash) } + end + + context 'with exclude: value' do + let(:options) { { exclude: [:three] } } + it { is_expected.to eq(['?']) } + end + + context 'with exclude: :all' do + let(:options) { { exclude: :all } } + it { is_expected.to eq(['?']) } + end + end + + context 'given a Array with nested arrays' do + let(:hash) { [%w[foo bar baz], %w[foo], %w[bar], %w[baz]] } + + it { is_expected.to eq([['?'], '?']) } + end + + context 'given a Array with nested hashes' do + let(:hash) { [{ foo: { bar: 1 } }, { foo: { bar: 2 } }] } + + it { is_expected.to eq([{ foo: { bar: '?' } }, '?']) } + end + end +end diff --git a/spec/ddtrace/utils/database_spec.rb b/spec/ddtrace/utils/database_spec.rb new file mode 100644 index 0000000000..59418eb702 --- /dev/null +++ b/spec/ddtrace/utils/database_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +require 'ddtrace/utils/database' + +RSpec.describe Datadog::Utils::Database do + describe '#normalize_vendor' do + subject(:result) { described_class.normalize_vendor(value) } + + context 'when given' do + context 'nil' do + let(:value) { nil } + it { is_expected.to eq('defaultdb') } + end + + context 'sqlite3' do + let(:value) { 'sqlite3' } + it { is_expected.to eq('sqlite') } + end + + context 'mysql2' do + let(:value) { 'mysql2' } + it { is_expected.to eq('mysql2') } + end + + context 'postgresql' do + let(:value) { 'postgresql' } + it { is_expected.to eq('postgres') } + end + + context 'customdb' do + let(:value) { 'customdb' } + it { is_expected.to eq(value) } + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e734f9f68d..67f86bcabd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,7 +15,7 @@ # require 'support/spy_transport' require 'support/tracer_helpers' # require 'support/rails_active_record_helpers' -# require 'support/configuration_helpers' +require 'support/configuration_helpers' require 'support/synchronization_helpers' require 'support/log_helpers' require 'support/http_helpers' @@ -26,7 +26,7 @@ RSpec.configure do |config| config.include TracerHelpers # config.include RailsActiveRecordHelpers - # config.include ConfigurationHelpers + config.include ConfigurationHelpers config.include SynchronizationHelpers config.include LogHelpers diff --git a/spec/support/configuration_helpers.rb b/spec/support/configuration_helpers.rb index 12810b7b63..3419176b1c 100644 --- a/spec/support/configuration_helpers.rb +++ b/spec/support/configuration_helpers.rb @@ -1,24 +1,4 @@ module ConfigurationHelpers - # update Datadog user configuration; you should pass: - # - # * +key+: the key that should be updated - # * +value+: the value of the key - def update_config(key, value) - Datadog.configuration[:rails][key] = value - Datadog::Contrib::Rails::Framework.setup - end - - # reset default configuration and replace any dummy tracer - # with the global one - def reset_config - Datadog.configure do |c| - c.use :rails - c.use :redis - end - - Datadog::Contrib::Rails::Framework.setup - end - def remove_patch!(integration) Datadog .registry[integration] diff --git a/test/configurable_test.rb b/test/configurable_test.rb deleted file mode 100644 index e7bb041718..0000000000 --- a/test/configurable_test.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'ddtrace/configurable' - -module Datadog - class ConfigurableTest < Minitest::Test - def setup - @module = Module.new { include(Configurable) } - end - - def test_option_methods - assert_respond_to(@module, :set_option) - assert_respond_to(@module, :get_option) - end - - def test_option_default - @module.class_eval do - option :foo, default: :bar - end - - assert_equal(:bar, @module.get_option(:foo)) - end - - def test_setting_an_option - @module.class_eval do - option :foo, default: :bar - end - - @module.set_option(:foo, 'baz!') - assert_equal('baz!', @module.get_option(:foo)) - end - - def test_custom_setter - @module.class_eval do - option :shout, setter: ->(v) { v.upcase } - end - - @module.set_option(:shout, 'loud') - assert_equal('LOUD', @module.get_option(:shout)) - end - - def test_custom_setter_block - @module.class_eval do - option(:shout) { |value| "#{value.upcase}!" } - end - - @module.set_option(:shout, 'ouch') - assert_equal('OUCH!', @module.get_option(:shout)) - end - - def test_invalid_option - assert_raises(InvalidOptionError) do - @module.set_option(:bad_option, 'foo') - end - - assert_raises(InvalidOptionError) do - @module.get_option(:bad_option) - end - end - - def test_to_h - @module.class_eval do - option :x, default: 1 - option :y, default: 2 - end - - @module.set_option(:y, 100) - assert_equal({ x: 1, y: 100 }, @module.to_h) - end - - def test_false_options - @module.class_eval do - option :boolean, default: true - end - - @module.set_option(:boolean, false) - refute(@module.get_option(:boolean)) - end - - def test_dependency_solving - @module.class_eval do - option :foo, depends_on: [:bar] - option :bar, depends_on: [:baz] - option :baz - end - - assert_equal([:baz, :bar, :foo], @module.sorted_options) - end - end -end diff --git a/test/configuration/pin_setup_test.rb b/test/configuration/pin_setup_test.rb deleted file mode 100644 index 19a2e449b2..0000000000 --- a/test/configuration/pin_setup_test.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'ddtrace/configuration' - -module Datadog - class Configuration - class PinSetupTest < Minitest::Test - def setup - @target = Object.new - - Pin - .new('original-service', app: 'original-app') - .onto(@target) - end - - def test_setting_options - custom_tracer = get_test_tracer - - custom_options = { - service_name: 'my-service', - app: 'my-app', - app_type: :cache, - tracer: custom_tracer, - tags: { env: :prod }, - distributed_tracing: true - } - - PinSetup.new(@target, custom_options).call - - assert_equal('my-service', @target.datadog_pin.service) - assert_equal('my-app', @target.datadog_pin.app) - assert_equal({ env: :prod }, @target.datadog_pin.tags) - assert_equal({ distributed_tracing: true }, @target.datadog_pin.config) - assert_equal(custom_tracer, @target.datadog_pin.tracer) - end - - def test_missing_options_are_not_set - PinSetup.new(@target, app: 'custom-app').call - - assert_equal('custom-app', @target.datadog_pin.app) - assert_equal('original-service', @target.datadog_pin.service) - end - - def test_configure_api - Datadog.configure(@target, service_name: :foo, extra: :bar) - - assert_equal(:foo, @target.datadog_pin.service) - assert_equal({ extra: :bar }, @target.datadog_pin.config) - end - end - end -end diff --git a/test/configuration/proxy_test.rb b/test/configuration/proxy_test.rb deleted file mode 100644 index ea2903f621..0000000000 --- a/test/configuration/proxy_test.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'ddtrace/configurable' - -module Datadog - class Configuration - class ProxyTest < Minitest::Test - def setup - @module = Module.new do - include Configurable - option :x, default: :a - option :y, default: :b - end - - @proxy = Proxy.new(@module) - end - - def test_hash_syntax - @proxy[:x] = 1 - @proxy[:y] = 2 - - assert_equal(1, @proxy[:x]) - assert_equal(2, @proxy[:y]) - end - - def test_hash_coercion - assert_equal({ x: :a, y: :b }, @proxy.to_h) - assert_equal({ x: :a, y: :b }, @proxy.to_hash) - end - - def test_merge - assert_equal({ x: :a, y: :b, z: :c }, @proxy.merge(z: :c)) - end - end - end -end diff --git a/test/configuration/resolver_test.rb b/test/configuration/resolver_test.rb deleted file mode 100644 index 2b2900edf8..0000000000 --- a/test/configuration/resolver_test.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'ddtrace/configuration' - -module Datadog - class Configuration - class ResolverTest < Minitest::Test - def test_dependency_solving - graph = { 1 => [2], 2 => [3, 4], 3 => [], 4 => [3], 5 => [1] } - tsort = Resolver.new(graph).call - - assert_equal([3, 4, 2, 1, 5], tsort) - end - - def test_cyclic_dependecy - graph = { 1 => [2], 2 => [1] } - - assert_raises(TSort::Cyclic) do - Resolver.new(graph).call - end - end - end - end -end diff --git a/test/configuration_test.rb b/test/configuration_test.rb deleted file mode 100644 index 141e60894e..0000000000 --- a/test/configuration_test.rb +++ /dev/null @@ -1,150 +0,0 @@ -require 'ddtrace/configuration' -require 'ddtrace/configurable' -require 'logger' - -module Datadog - class ConfigurationTest < Minitest::Test - def setup - @registry = Registry.new - @configuration = Configuration.new(registry: @registry) - end - - def test_use_method - contrib = Minitest::Mock.new - contrib.expect(:patch, true) - contrib.expect(:sorted_options, []) - - @registry.add(:example, contrib) - @configuration.use(:example) - - assert_mock(contrib) - end - - def test_module_configuration - integration = Module.new do - include Contrib::Base - option :option1 - option :option2 - end - - @registry.add(:example, integration) - - @configuration.use(:example, option1: :foo!, option2: :bar!) - - assert_equal(:foo!, @configuration[:example][:option1]) - assert_equal(:bar!, @configuration[:example][:option2]) - end - - def test_setting_a_configuration_param - integration = Module.new do - include Contrib::Base - option :option1 - end - - @registry.add(:example, integration) - @configuration[:example][:option1] = :foo - assert_equal(:foo, @configuration[:example][:option1]) - end - - def test_invalid_integration - assert_raises(Configuration::InvalidIntegrationError) do - @configuration[:foobar] - end - end - - def test_lazy_option - integration = Module.new do - include Contrib::Base - option :option1, default: -> { 1 + 1 }, lazy: true - end - - @registry.add(:example, integration) - - assert_equal(2, @configuration[:example][:option1]) - end - - def test_hash_coercion - integration = Module.new do - include Contrib::Base - option :option1, default: :foo - option :option2, default: :bar - end - - @registry.add(:example, integration) - assert_equal({ option1: :foo, option2: :bar }, @configuration[:example].to_h) - end - - def test_dependency_solving - integration = Module.new do - include Contrib::Base - option :multiply_by, depends_on: [:number] do |value| - get_option(:number) * value - end - - option :number - end - - @registry.add(:example, integration) - @configuration.use(:example, multiply_by: 5, number: 5) - assert_equal(5, @configuration[:example][:number]) - assert_equal(25, @configuration[:example][:multiply_by]) - end - - def test_default_also_passes_through_setter - array = [] - - integration = Module.new do - include Contrib::Base - option :option1 - option :option2, default: 10 do |value| - array << value - value - end - end - - @registry.add(:example, integration) - @configuration.use(:example, option1: :foo!) - - assert_equal(:foo!, @configuration[:example][:option1]) - assert_equal(10, @configuration[:example][:option2]) - assert_includes(array, 10) - end - - def test_tracer_configuration - tracer = Datadog::Tracer.new - debug_state = tracer.class.debug_logging - original_log = tracer.class.log - custom_log = Logger.new(STDOUT) - - @configuration.tracer( - enabled: false, - debug: !debug_state, - log: custom_log, - hostname: 'tracer.host.com', - port: 1234, - env: :config_test, - tags: { foo: :bar }, - instance: tracer - ) - - refute(tracer.enabled) - refute(debug_state) - assert_equal(custom_log, Datadog::Tracer.log) - assert_equal('tracer.host.com', tracer.writer.transport.hostname) - assert_equal(1234, tracer.writer.transport.port) - assert_equal(:config_test, tracer.tags[:env]) - assert_equal(:bar, tracer.tags[:foo]) - tracer.class.debug_logging = debug_state - tracer.class.log = original_log - end - - def test_configuration_acts_on_default_tracer - previous_state = Datadog.tracer.enabled - - @configuration.tracer(enabled: !previous_state) - refute_equal(previous_state, Datadog.tracer.enabled) - @configuration.tracer(enabled: previous_state) - assert_equal(previous_state, Datadog.tracer.enabled) - end - end -end diff --git a/test/contrib/elasticsearch/quantize_test.rb b/test/contrib/elasticsearch/quantize_test.rb index 8c51004b11..8c68c0f4fa 100644 --- a/test/contrib/elasticsearch/quantize_test.rb +++ b/test/contrib/elasticsearch/quantize_test.rb @@ -9,7 +9,17 @@ def test_id assert_equal('/my/thing/?/', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1/')) assert_equal('/my/thing/?/is/cool', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1/is/cool')) assert_equal('/my/thing/??is=cool', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1?is=cool')) - assert_equal('/my/thing/1two3/z', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1two3/z')) + assert_equal('/my/thing/?/z', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1two3/z')) + assert_equal('/my/thing/?/z/', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1two3/z/')) + assert_equal('/my/thing/?/z?a=b', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1two3/z?a=b')) + assert_equal('/my/thing/?/z?a=b?', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1two3/z?a=b123')) + assert_equal('/my/thing/?/abc', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1two3/abc')) + assert_equal('/my/thing/?/abc/', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing/1two3/abc/')) + assert_equal('/my/thing?/?/abc/', Datadog::Contrib::Elasticsearch::Quantize.format_url('/my/thing231/1two3/abc/')) + assert_equal('/my/thing/?/_termvector', Datadog::Contrib::Elasticsearch::Quantize + .format_url('/my/thing/1447990c-811a-4a83-b7e2-c3e8a4a6ff54/_termvector')) + assert_equal('app_prod/user/?/_termvector', + Datadog::Contrib::Elasticsearch::Quantize.format_url('app_prod/user/1fff2c9dc2f3e/_termvector')) end def test_index @@ -27,7 +37,7 @@ def test_combine def test_body # MGet format body = "{\"ids\":[\"1\",\"2\",\"3\"]}" - quantized_body = "{\"ids\":\"?\"}" + quantized_body = "{\"ids\":[\"?\"]}" assert_equal(quantized_body, Datadog::Contrib::Elasticsearch::Quantize.format_body(body)) # Search format diff --git a/test/contrib/mongodb/client_test.rb b/test/contrib/mongodb/client_test.rb deleted file mode 100644 index d908656069..0000000000 --- a/test/contrib/mongodb/client_test.rb +++ /dev/null @@ -1,312 +0,0 @@ -require 'contrib/mongodb/test_helper' -require 'helper' - -# rubocop:disable Metrics/ClassLength -# rubocop:disable Metrics/LineLength -class MongoDBTest < Minitest::Test - MONGO_HOST = ENV.fetch('TEST_MONGODB_HOST', '127.0.0.1').freeze - MONGO_PORT = ENV.fetch('TEST_MONGODB_PORT', 27017).freeze - MONGO_DB = 'test'.freeze - - def setup - Datadog.configure do |c| - c.use :mongo - end - - # disable Mongo logging - Mongo::Logger.logger.level = ::Logger::WARN - - # initialize the client and overwrite the default tracer - @client = Mongo::Client.new(["#{MONGO_HOST}:#{MONGO_PORT}"], database: MONGO_DB) - @tracer = get_test_tracer() - pin = Datadog::Pin.get_from(@client) - pin.tracer = @tracer - end - - def teardown - # clear intermediate data - @client.database.drop - end - - def test_constructor_signature - executed = false - Mongo::Client.new(["#{MONGO_HOST}:#{MONGO_PORT}"], database: MONGO_DB) do |_self| - # be sure that the block is evaluated - executed = true - end - assert(executed, 'the constructor block is not executed') - end - - def test_pin_attributes - # the client must have a PIN set - pin = Datadog::Pin.get_from(@client) - assert_equal('mongodb', pin.service) - assert_equal('mongodb', pin.app) - assert_equal('db', pin.app_type) - end - - def test_pin_service_change - pin = Datadog::Pin.get_from(@client) - pin.service = 'mongodb-primary' - @client[:artists].insert_one(name: 'FKA Twigs') - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('mongodb-primary', span.service) - end - - def test_pin_disabled - pin = Datadog::Pin.get_from(@client) - pin.tracer.enabled = false - @client[:artists].insert_one(name: 'FKA Twigs') - spans = @tracer.writer.spans() - assert_equal(0, spans.length) - end - - def test_insert_operation - @client[:artists].insert_one(name: 'FKA Twigs') - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>:insert, :database=>"test", :collection=>"artists", "documents"=>{:name=>"?"}, "ordered"=>"?"}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('artists', span.get_tag('mongodb.collection')) - assert_equal('1', span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - end - - def test_drop_operation - @client.database.drop - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>:dropDatabase, :database=>"test", :collection=>1}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('1', span.get_tag('mongodb.collection')) - assert_nil(span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - end - - def test_insert_array_operation - @client[:people].insert_one(name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing']) - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>:insert, :database=>"test", :collection=>"people", "documents"=>{:name=>"?", :hobbies=>"?"}, "ordered"=>"?"}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('people', span.get_tag('mongodb.collection')) - assert_equal('1', span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - end - - def test_insert_many_array_operation - docs = [ - { name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing'] }, - { name: 'Sally', hobbies: ['skiing', 'stamp collecting'] } - ] - - @client[:people].insert_many(docs) - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>:insert, :database=>"test", :collection=>"people", "documents"=>{:name=>"?", :hobbies=>"?"}, "ordered"=>"?"}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('people', span.get_tag('mongodb.collection')) - assert_equal('2', span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - end - - def test_find_all - # prepare the test case - doc = { name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing'] } - @client[:people].insert_one(doc) - @tracer.writer.spans() - - # do a find in all and consume the database - collection = @client[:people] - collection.find.each do |document| - # => Yields a BSON::Document. - end - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>"find", :database=>"test", :collection=>"people", "filter"=>{}}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('people', span.get_tag('mongodb.collection')) - assert_nil(span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - end - - def test_find_matching_document - # prepare the test case - doc = { name: 'Steve', hobbies: ['hiking'] } - @client[:people].insert_one(doc) - @tracer.writer.spans() - - # find and check the correct result - collection = @client[:people] - assert_equal(['hiking'], collection.find(name: 'Steve').first[:hobbies]) - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>"find", :database=>"test", :collection=>"people", "filter"=>{"name"=>"?"}}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('people', span.get_tag('mongodb.collection')) - assert_nil(span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - end - - def test_update_one_document - # prepare the test case - doc = { name: 'Sally', hobbies: ['skiing', 'stamp collecting'] } - collection = @client[:people] - collection.insert_one(doc) - @tracer.writer.spans() - - # update with a new field - collection.update_one({ name: 'Sally' }, '$set' => { 'phone_number' => '555-555-5555' }) - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>:update, :database=>"test", :collection=>"people", "updates"=>{"q"=>{"name"=>"?"}, "u"=>{"$set"=>{"phone_number"=>"?"}}, "multi"=>"?", "upsert"=>"?"}, "ordered"=>"?"}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('people', span.get_tag('mongodb.collection')) - assert_equal('1', span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - # validity check - assert_equal('555-555-5555', collection.find(name: 'Sally').first[:phone_number]) - end - - def test_update_many_documents - # prepare the test case - docs = [ - { name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing'] }, - { name: 'Sally', hobbies: ['skiing', 'stamp collecting'] } - ] - - collection = @client[:people] - collection.insert_many(docs) - @tracer.writer.spans() - - # update with a new field - collection.update_many({}, '$set' => { 'phone_number' => '555-555-5555' }) - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>:update, :database=>"test", :collection=>"people", "updates"=>{"q"=>{}, "u"=>{"$set"=>{"phone_number"=>"?"}}, "multi"=>"?", "upsert"=>"?"}, "ordered"=>"?"}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('people', span.get_tag('mongodb.collection')) - assert_equal('2', span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - # validity checks - assert_equal('555-555-5555', collection.find(name: 'Sally').first[:phone_number]) - assert_equal('555-555-5555', collection.find(name: 'Steve').first[:phone_number]) - end - - def test_delete_one_document - # prepare the test case - doc = { name: 'Sally', hobbies: ['skiing', 'stamp collecting'] } - collection = @client[:people] - collection.insert_one(doc) - @tracer.writer.spans() - - # update with a new field - collection.delete_one(name: 'Sally') - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>:delete, :database=>"test", :collection=>"people", "deletes"=>{"q"=>{"name"=>"?"}, "limit"=>"?"}, "ordered"=>"?"}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('people', span.get_tag('mongodb.collection')) - assert_equal('1', span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - # validity check - assert_equal(0, collection.find(name: 'Sally').count) - end - - def test_delete_many_documents - # prepare the test case - docs = [ - { name: 'Steve', hobbies: ['hiking', 'tennis', 'fly fishing'] }, - { name: 'Sally', hobbies: ['skiing', 'stamp collecting'] } - ] - - collection = @client[:people] - collection.insert_many(docs) - @tracer.writer.spans() - - # update with a new field - collection.delete_many(name: /$S*/) - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>:delete, :database=>"test", :collection=>"people", "deletes"=>{"q"=>{"name"=>"?"}, "limit"=>"?"}, "ordered"=>"?"}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('people', span.get_tag('mongodb.collection')) - assert_equal('2', span.get_tag('mongodb.rows')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - # validity check - assert_equal(0, collection.find(name: 'Sally').count) - assert_equal(0, collection.find(name: 'Steve').count) - end - - def test_failed_queries - # do an invalid operation that results with a failed command - @client[:artists].drop - spans = @tracer.writer.spans() - assert_equal(1, spans.length) - span = spans[0] - # check fields - assert_equal('{:operation=>:drop, :database=>"test", :collection=>"artists"}', span.resource) - assert_equal('mongodb', span.service) - assert_equal('mongodb', span.span_type) - assert_equal('test', span.get_tag('mongodb.db')) - assert_equal('artists', span.get_tag('mongodb.collection')) - assert_nil(span.get_tag('mongodb.rows')) - assert_equal(1, span.status) - assert_equal('ns not found (26)', span.get_tag('error.msg')) - assert_equal(MONGO_HOST, span.get_tag('out.host')) - assert_equal(MONGO_PORT.to_s, span.get_tag('out.port')) - end -end diff --git a/test/contrib/mongodb/test_helper.rb b/test/contrib/mongodb/test_helper.rb deleted file mode 100644 index 88ebdf231b..0000000000 --- a/test/contrib/mongodb/test_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -require 'ddtrace' -require 'mongo' diff --git a/test/contrib/rack/helpers.rb b/test/contrib/rack/helpers.rb index 2b3893133c..54da2cc2da 100644 --- a/test/contrib/rack/helpers.rb +++ b/test/contrib/rack/helpers.rb @@ -82,6 +82,21 @@ def app run(handler) end + + map '/headers/' do + run(proc do |_env| + response_headers = { + 'Content-Type' => 'text/html', + 'Cache-Control' => 'max-age=3600', + 'ETag' => '"737060cd8c284d8af7ad3082f209582d"', + 'Expires' => 'Thu, 01 Dec 1994 16:00:00 GMT', + 'Last-Modified' => 'Tue, 15 Nov 1994 12:45:26 GMT', + 'X-Request-ID' => 'f058ebd6-02f7-4d3f-942e-904344e8cde5', + 'X-Fake-Response' => 'Don\'t tag me.' + } + [200, response_headers, 'OK'] + end) + end end.to_app end diff --git a/test/contrib/rack/middleware_test.rb b/test/contrib/rack/middleware_test.rb index c8221a7ad7..7acbe33b29 100644 --- a/test/contrib/rack/middleware_test.rb +++ b/test/contrib/rack/middleware_test.rb @@ -360,9 +360,36 @@ def test_request_middleware_custom_service assert_nil(span.parent) end - def test_request_middleware_request_id - request_id = SecureRandom.uuid - get '/success/', {}, 'HTTP_X_REQUEST_ID' => request_id + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize + def test_request_middleware_headers + # Configure to tag headers + Datadog.configure do |c| + c.use :rack, headers: { + request: [ + 'Cache-Control' + ], + response: [ + 'Content-Type', + 'Cache-Control', + 'Content-Type', + 'ETag', + 'Expires', + 'Last-Modified', + # This lowercase 'Id' header doesn't match. + # Ensure middleware allows for case-insensitive matching. + 'X-Request-Id' + ] + } + end + + request_headers = { + 'HTTP_CACHE_CONTROL' => 'no-cache', + 'HTTP_X_REQUEST_ID' => SecureRandom.uuid, + 'HTTP_X_FAKE_REQUEST' => 'Don\'t tag me.' + } + + get '/headers/', {}, request_headers assert last_response.ok? spans = @tracer.writer.spans @@ -375,10 +402,24 @@ def test_request_middleware_request_id assert_equal('GET 200', span.resource) assert_equal('GET', span.get_tag('http.method')) assert_equal('200', span.get_tag('http.status_code')) - assert_equal('/success/', span.get_tag('http.url')) + assert_equal('/headers/', span.get_tag('http.url')) assert_equal('http://example.org', span.get_tag('http.base_url')) - assert_equal(request_id, span.get_tag('http.request_id')) assert_equal(0, span.status) assert_nil(span.parent) + + # Request headers + assert_equal('no-cache', span.get_tag('http.request.headers.cache_control')) + # Make sure non-whitelisted headers don't become tags. + assert_nil(span.get_tag('http.request.headers.x_request_id')) + assert_nil(span.get_tag('http.request.headers.x_fake_request')) + + # Response headers + assert_equal('text/html', span.get_tag('http.response.headers.content_type')) + assert_equal('max-age=3600', span.get_tag('http.response.headers.cache_control')) + assert_equal('"737060cd8c284d8af7ad3082f209582d"', span.get_tag('http.response.headers.etag')) + assert_equal('Tue, 15 Nov 1994 12:45:26 GMT', span.get_tag('http.response.headers.last_modified')) + assert_equal('f058ebd6-02f7-4d3f-942e-904344e8cde5', span.get_tag('http.response.headers.x_request_id')) + # Make sure non-whitelisted headers don't become tags. + assert_nil(span.get_tag('http.request.headers.x_fake_response')) end end diff --git a/test/contrib/rack/request_parser_test.rb b/test/contrib/rack/request_parser_test.rb new file mode 100644 index 0000000000..d1a90a66b9 --- /dev/null +++ b/test/contrib/rack/request_parser_test.rb @@ -0,0 +1,13 @@ +require 'ddtrace/contrib/rack/request_queue' + +class QueueTimeParserTest < Minitest::Test + include Rack::Test::Methods + + def test_nginx_header + # ensure nginx headers are properly parsed + headers = {} + headers['HTTP_X_REQUEST_START'] = 't=1512379167.574' + request_start = Datadog::Contrib::Rack::QueueTime.get_request_start(headers) + assert_equal(1512379167.574, request_start.to_f) + end +end diff --git a/test/contrib/rack/request_queuing_test.rb b/test/contrib/rack/request_queuing_test.rb new file mode 100644 index 0000000000..f77c89db15 --- /dev/null +++ b/test/contrib/rack/request_queuing_test.rb @@ -0,0 +1,124 @@ +require 'contrib/rack/helpers' + +class RequestQueuingTest < RackBaseTest + def setup + super + # enable request_queuing + Datadog.configuration[:rack][:request_queuing] = true + end + + def test_request_queuing_header + # ensure a queuing Span is created if the header is available + request_start = (Time.now.utc - 5).to_i + header 'x-request-start', "t=#{request_start}" + get '/success/' + assert last_response.ok? + + spans = @tracer.writer.spans + assert_equal(2, spans.length) + + rack_span = spans.find { |s| s.name == 'rack.request' } + frontend_span = spans.find { |s| s.name == 'http_server.queue' } + refute_nil(rack_span) + refute_nil(frontend_span) + + assert_equal('http', rack_span.span_type) + assert_equal('rack', rack_span.service) + assert_equal('GET 200', rack_span.resource) + assert_equal('GET', rack_span.get_tag('http.method')) + assert_equal('200', rack_span.get_tag('http.status_code')) + assert_equal('/success/', rack_span.get_tag('http.url')) + assert_equal(0, rack_span.status) + assert_equal(frontend_span.span_id, rack_span.parent_id) + + assert_equal('web-server', frontend_span.service) + assert_equal(request_start, frontend_span.start_time.to_i) + end + + def test_request_alternative_queuing_header + # ensure a queuing Span is created if the header is available + request_start = (Time.now.utc - 5).to_i + header 'x-queue-start', "t=#{request_start}" + get '/success/' + assert last_response.ok? + + spans = @tracer.writer.spans + assert_equal(2, spans.length) + + rack_span = spans.find { |s| s.name == 'rack.request' } + frontend_span = spans.find { |s| s.name == 'http_server.queue' } + refute_nil(rack_span) + refute_nil(frontend_span) + + assert_equal('http', rack_span.span_type) + assert_equal('rack', rack_span.service) + assert_equal('GET 200', rack_span.resource) + assert_equal('GET', rack_span.get_tag('http.method')) + assert_equal('200', rack_span.get_tag('http.status_code')) + assert_equal('/success/', rack_span.get_tag('http.url')) + assert_equal(0, rack_span.status) + assert_equal(frontend_span.span_id, rack_span.parent_id) + + assert_equal('web-server', frontend_span.service) + assert_equal(request_start, frontend_span.start_time.to_i) + end + + def test_request_queuing_service_name + # ensure a queuing Span is created if the header is available + Datadog.configuration[:rack][:web_service_name] = 'nginx' + request_start = (Time.now.utc - 5).to_i + header 'x-request-start', "t=#{request_start}" + get '/success/' + assert last_response.ok? + + spans = @tracer.writer.spans + assert_equal(2, spans.length) + + rack_span = spans.find { |s| s.name == 'rack.request' } + frontend_span = spans.find { |s| s.name == 'http_server.queue' } + refute_nil(rack_span) + refute_nil(frontend_span) + + assert_equal('nginx', frontend_span.service) + end + + def test_clock_skew + # ensure a queuing Span is NOT created if there is a clock skew + # where the starting time is greater than current host Time.now + request_start = (Time.now.utc + 5).to_i + header 'x-request-start', "t=#{request_start}" + get '/success/' + assert last_response.ok? + + spans = @tracer.writer.spans + assert_equal(1, spans.length) + + rack_span = spans[0] + assert_equal('rack.request', rack_span.name) + end + + def test_wrong_header + # ensure a queuing Span is NOT created if the header is wrong + header 'x-request-start', 'something_weird' + get '/success/' + assert last_response.ok? + + spans = @tracer.writer.spans + assert_equal(1, spans.length) + + rack_span = spans[0] + assert_equal('rack.request', rack_span.name) + end + + def test_enabled_missing_header + # ensure a queuing Span is NOT created if the header is missing + get '/success/' + assert last_response.ok? + + spans = @tracer.writer.spans + assert_equal(1, spans.length) + + rack_span = spans[0] + assert_equal('rack.request', rack_span.name) + end +end diff --git a/test/contrib/rails/controller_test.rb b/test/contrib/rails/controller_test.rb index c129c9f77f..c2d3d97a87 100644 --- a/test/contrib/rails/controller_test.rb +++ b/test/contrib/rails/controller_test.rb @@ -117,7 +117,7 @@ class TracingControllerTest < ActionController::TestCase spans = @tracer.writer.spans # rubocop:disable Style/IdenticalConditionalBranches - if Datadog::Contrib::ActiveRecord::Patcher.instantiation_tracing_supported? + if Datadog::Contrib::ActiveRecord::Events::Instantiation.supported? assert_equal(spans.length, 5) span_instantiation, span_database, span_request, span_cache, span_template = spans diff --git a/test/contrib/rails/database_test.rb b/test/contrib/rails/database_test.rb index 9daf2dec3a..549f8f36ce 100644 --- a/test/contrib/rails/database_test.rb +++ b/test/contrib/rails/database_test.rb @@ -44,7 +44,7 @@ class DatabaseTracingTest < ActiveSupport::TestCase end test 'active record traces instantiation' do - if Datadog::Contrib::ActiveRecord::Patcher.instantiation_tracing_supported? + if Datadog::Contrib::ActiveRecord::Events::Instantiation.supported? begin Article.create(title: 'Instantiation test') @tracer.writer.spans # Clear spans @@ -69,7 +69,7 @@ class DatabaseTracingTest < ActiveSupport::TestCase end test 'active record traces instantiation inside parent trace' do - if Datadog::Contrib::ActiveRecord::Patcher.instantiation_tracing_supported? + if Datadog::Contrib::ActiveRecord::Events::Instantiation.supported? begin Article.create(title: 'Instantiation test') @tracer.writer.spans # Clear spans diff --git a/test/contrib/rails/rack_middleware_test.rb b/test/contrib/rails/rack_middleware_test.rb index 27f095db61..31c1db53cd 100644 --- a/test/contrib/rails/rack_middleware_test.rb +++ b/test/contrib/rails/rack_middleware_test.rb @@ -35,7 +35,7 @@ class FullStackTest < ActionDispatch::IntegrationTest # spans are sorted alphabetically, and ... controller names start # either by m or p (MySQL or PostGreSQL) so the database span is always # the first one. Would fail with an adapter named z-something. - if Datadog::Contrib::ActiveRecord::Patcher.instantiation_tracing_supported? + if Datadog::Contrib::ActiveRecord::Events::Instantiation.supported? assert_equal(spans.length, 6) instantiation_span, database_span, request_span, controller_span, cache_span, render_span = spans else @@ -71,7 +71,7 @@ class FullStackTest < ActionDispatch::IntegrationTest assert_includes(database_span.resource, 'FROM') assert_includes(database_span.resource, 'articles') - if Datadog::Contrib::ActiveRecord::Patcher.instantiation_tracing_supported? + if Datadog::Contrib::ActiveRecord::Events::Instantiation.supported? assert_equal(instantiation_span.name, 'active_record.instantiation') assert_equal(instantiation_span.span_type, 'custom') assert_equal(instantiation_span.service, Datadog.configuration[:rails][:service_name]) diff --git a/test/contrib/resque/hooks_test.rb b/test/contrib/resque/hooks_test.rb deleted file mode 100644 index 9630e277ff..0000000000 --- a/test/contrib/resque/hooks_test.rb +++ /dev/null @@ -1,113 +0,0 @@ -require 'helper' -require 'resque' -require 'ddtrace' -require_relative 'test_helper' - -module Datadog - module Contrib - module Resque - class HooksTest < Minitest::Test - REDIS_HOST = ENV.fetch('TEST_REDIS_HOST', '127.0.0.1').freeze - REDIS_PORT = ENV.fetch('TEST_REDIS_PORT', 6379) - - def setup - Datadog.configure do |c| - c.use :resque - end - - redis_url = "redis://#{REDIS_HOST}:#{REDIS_PORT}" - ::Resque.redis = redis_url - @tracer = enable_test_tracer! - ::Resque::Failure.clear - end - - def test_successful_job - perform_job(TestJob) - spans = @tracer.writer.spans - span = spans.first - - assert_equal(1, spans.length, 'created wrong number of spans') - assert_equal('resque.job', span.name, 'wrong span name set') - assert_equal(TestJob.name, span.resource, 'span resource should match job name') - assert_equal(Ext::AppTypes::WORKER, span.span_type, 'span should be of worker span type') - assert_equal('resque', span.service, 'wrong service stored in span') - refute_equal(Ext::Errors::STATUS, span.status, 'wrong span status') - end - - def test_clean_state - @tracer.trace('main.process') do - perform_job(TestCleanStateJob, @tracer) - end - - spans = @tracer.writer.spans - assert_equal(2, spans.length) - assert_equal(0, ::Resque::Failure.count) - - main_span = spans[0] - job_span = spans[1] - assert_equal('main.process', main_span.name, 'wrong span name set') - assert_equal('resque.job', job_span.name, 'wrong span name set') - refute_equal(main_span.trace_id, job_span.trace_id, 'main process and resque job must not be in the same trace') - end - - def test_service_change - pin = Datadog::Pin.get_from(::Resque) - pin.service = 'test_service_change' - perform_job(TestJob) - spans = @tracer.writer.spans - span = spans.first - - pin.service = 'resque' # reset pin - assert_equal(1, spans.length, 'created wrong number of spans') - assert_equal('resque.job', span.name, 'wrong span name set') - assert_equal(TestJob.name, span.resource, 'span resource should match job name') - assert_equal(Ext::AppTypes::WORKER, span.span_type, 'span should be of worker span type') - assert_equal('test_service_change', span.service, 'wrong service stored in span') - refute_equal(Ext::Errors::STATUS, span.status, 'wrong span status') - end - - def test_failed_job - perform_job(TestJob, false) - spans = @tracer.writer.spans - span = spans.first - - # retrieve error from Resque backend - assert_equal(1, ::Resque::Failure.count) - error_message = ::Resque::Failure.all['error'] - - assert_equal('TestJob failed', error_message, 'unplanned error occured') - assert_equal(1, spans.length, 'created wrong number of spans') - assert_equal('resque.job', span.name, 'wrong span name set') - assert_equal(TestJob.name, span.resource, 'span resource should match job name') - assert_equal(Ext::AppTypes::WORKER, span.span_type, 'span should be of worker span type') - assert_equal('resque', span.service, 'wrong service stored in span') - assert_equal(error_message, span.get_tag(Ext::Errors::MSG), 'wrong error message populated') - assert_equal(Ext::Errors::STATUS, span.status, 'wrong status in span') - assert_equal('StandardError', span.get_tag(Ext::Errors::TYPE), 'wrong type of error stored in span') - end - - def test_workers_patch - worker_class1 = Class.new - worker_class2 = Class.new - - remove_patch!(:resque) - - Datadog.configure do |c| - c.use(:resque, workers: [worker_class1, worker_class2]) - end - - assert_includes(worker_class1.singleton_class.included_modules, ResqueJob) - assert_includes(worker_class2.singleton_class.included_modules, ResqueJob) - end - - def enable_test_tracer! - get_test_tracer.tap { |tracer| pin.tracer = tracer } - end - - def pin - ::Resque.datadog_pin - end - end - end - end -end diff --git a/test/contrib/resque/test_helper.rb b/test/contrib/resque/test_helper.rb deleted file mode 100644 index 5131cb01c2..0000000000 --- a/test/contrib/resque/test_helper.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'resque' - -require 'ddtrace' -require 'ddtrace/contrib/resque/resque_job' - -def perform_job(klass, *args) - worker = Resque::Worker.new(:test_queue) - job = Resque::Job.new(:test_queue, 'class' => klass, 'args' => args) - worker.perform(job) -end - -module TestJob - extend Datadog::Contrib::Resque::ResqueJob - - def self.perform(pass = true) - return true if pass - raise StandardError, 'TestJob failed' - end -end - -module TestCleanStateJob - extend Datadog::Contrib::Resque::ResqueJob - - def self.perform(tracer) - # the perform ensures no Context is propagated - pin = Datadog::Pin.get_from(Resque) - spans = pin.tracer.provider.context.trace.length - raise StandardError if spans != 1 - end -end - -Resque.after_fork { Datadog::Pin.get_from(Resque).tracer.writer = FauxWriter.new } -Resque.before_first_fork.each(&:call) diff --git a/test/contrib/sidekiq/tracer_test.rb b/test/contrib/sidekiq/tracer_test.rb index 0a293d33a5..3b527785f9 100644 --- a/test/contrib/sidekiq/tracer_test.rb +++ b/test/contrib/sidekiq/tracer_test.rb @@ -50,6 +50,7 @@ def test_empty assert_equal('sidekiq', span.service) assert_equal('TracerTest::EmptyWorker', span.resource) assert_equal('default', span.get_tag('sidekiq.job.queue')) + refute_nil(span.get_tag('sidekiq.job.delay')) assert_equal(0, span.status) assert_nil(span.parent) end @@ -71,6 +72,7 @@ def test_error assert_equal('sidekiq', span.service) assert_equal('TracerTest::ErrorWorker', span.resource) assert_equal('default', span.get_tag('sidekiq.job.queue')) + refute_nil(span.get_tag('sidekiq.job.delay')) assert_equal(1, span.status) assert_equal('job error', span.get_tag(Datadog::Ext::Errors::MSG)) assert_equal('TracerTest::TestError', span.get_tag(Datadog::Ext::Errors::TYPE)) @@ -92,6 +94,7 @@ def test_custom assert_equal('sidekiq', empty.service) assert_equal('TracerTest::EmptyWorker', empty.resource) assert_equal('default', empty.get_tag('sidekiq.job.queue')) + refute_nil(empty.get_tag('sidekiq.job.delay')) assert_equal(0, empty.status) assert_nil(empty.parent) diff --git a/test/contrib/sinatra/tracer_activerecord_test.rb b/test/contrib/sinatra/tracer_activerecord_test.rb index 1c6401d6a6..acd1dfb027 100644 --- a/test/contrib/sinatra/tracer_activerecord_test.rb +++ b/test/contrib/sinatra/tracer_activerecord_test.rb @@ -123,7 +123,7 @@ def test_cached_tag def test_instantiation_tracing # Only supported in Rails 4.2+ - skip unless Datadog::Contrib::ActiveRecord::Patcher.instantiation_tracing_supported? + skip unless Datadog::Contrib::ActiveRecord::Events::Instantiation.supported? # Make sure Article table exists migrate_db diff --git a/test/contrib/sinatra/tracer_test.rb b/test/contrib/sinatra/tracer_test.rb index ec2f327e3e..53225c92db 100644 --- a/test/contrib/sinatra/tracer_test.rb +++ b/test/contrib/sinatra/tracer_test.rb @@ -4,6 +4,7 @@ class TracerTest < TracerTestBase class TracerTestApp < Sinatra::Application get '/request' do + headers['X-Request-Id'] = request.env['HTTP_X_REQUEST_ID'] 'hello world' end @@ -83,10 +84,10 @@ def test_distributed_request # Enable distributed tracing Datadog.configuration.use(:sinatra, distributed_tracing: true) - response = get '/request', {}, - 'HTTP_X_DATADOG_TRACE_ID' => '1', - 'HTTP_X_DATADOG_PARENT_ID' => '2', - 'HTTP_X_DATADOG_SAMPLING_PRIORITY' => Datadog::Ext::Priority::USER_KEEP.to_s + response = get '/request', {}, + 'HTTP_X_DATADOG_TRACE_ID' => '1', + 'HTTP_X_DATADOG_PARENT_ID' => '2', + 'HTTP_X_DATADOG_SAMPLING_PRIORITY' => Datadog::Ext::Priority::USER_KEEP.to_s assert_equal(200, response.status) @@ -237,4 +238,61 @@ def test_literal_template assert_equal(0, root.status) assert_nil(root.parent) end + + def test_tagging_default_connection_headers + request_id = SecureRandom.uuid + get '/request', {}, 'HTTP_X_REQUEST_ID' => request_id + + assert_equal(200, last_response.status) + + spans = @writer.spans + assert_equal(1, spans.length) + + span = spans[0] + assert_equal('sinatra', span.service) + assert_equal('GET /request', span.resource) + assert_equal('GET', span.get_tag(Datadog::Ext::HTTP::METHOD)) + assert_equal('/request', span.get_tag(Datadog::Ext::HTTP::URL)) + assert_equal(Datadog::Ext::HTTP::TYPE, span.span_type) + assert_equal(request_id, span.get_tag('http.response.headers.x_request_id')) + assert_equal('text/html;charset=utf-8', span.get_tag('http.response.headers.content_type')) + + assert_equal(0, span.status) + assert_nil(span.parent) + end + + def test_tagging_configured_connection_headers + Datadog.configuration.use(:sinatra, + headers: { + response: ['Content-Type'], + request: ['X-Request-Header'] + }) + + request_headers = { + 'HTTP_X_REQUEST_HEADER' => 'header_value', + 'HTTP_X_HEADER' => "don't tag me" + } + + get '/request#foo?a=1', {}, request_headers + + assert_equal(200, last_response.status) + + spans = @writer.spans + assert_equal(1, spans.length) + + span = spans[0] + assert_equal('sinatra', span.service) + assert_equal('GET /request', span.resource) + assert_equal('GET', span.get_tag(Datadog::Ext::HTTP::METHOD)) + assert_equal('/request', span.get_tag(Datadog::Ext::HTTP::URL)) + assert_equal(Datadog::Ext::HTTP::TYPE, span.span_type) + assert_equal('header_value', span.get_tag('http.request.headers.x_request_header')) + assert_equal('text/html;charset=utf-8', span.get_tag('http.response.headers.content_type')) + assert_nil(span.get_tag('http.request.headers.x_header')) + + assert_equal(0, span.status) + assert_nil(span.parent) + ensure + Datadog.configuration.use(:sinatra, headers: Datadog::Contrib::Sinatra::Tracer::DEFAULT_HEADERS) + end end diff --git a/test/pin_test.rb b/test/pin_test.rb deleted file mode 100644 index 18c74937a8..0000000000 --- a/test/pin_test.rb +++ /dev/null @@ -1,90 +0,0 @@ -require 'helper' -require 'ddtrace' -require 'ddtrace/pin' -require 'ddtrace/tracer' - -class CustomPinSetGet - def datadog_pin - @custom_attribute - end - - def datadog_pin=(pin) - @custom_attribute = 'The PIN is set!' - end -end - -class PinTest < Minitest::Test - def test_pin_onto - a = '' # using String, but really, any object should fit - - pin = Datadog::Pin.new('abc', app: 'anapp') - assert_equal('abc', pin.service) - assert_equal('anapp', pin.app) - pin.onto(a) - - got = Datadog::Pin.get_from(a) - assert_equal('abc', got.service) - assert_equal('anapp', got.app) - end - - def test_pin_get_from - a = [0, nil, self] # get_from should be callable on anything - - a.each do |x| - assert_nil(Datadog::Pin.get_from(x)) - end - end - - def test_to_s - pin = Datadog::Pin.new('abc', app: 'anapp', app_type: 'db') - assert_equal('abc', pin.service) - assert_equal('anapp', pin.app) - assert_equal('db', pin.app_type) - repr = pin.to_s - assert_equal('Pin(service:abc,app:anapp,app_type:db,name:)', repr) - end - - def test_pin_accessor - a = '' # using String, but really, any object should fit - - pin = Datadog::Pin.new('abc') - pin.onto(a) - - got = a.datadog_pin - assert_equal('abc', got.service) - end - - def test_enabled - pin = Datadog::Pin.new('abc') - assert_equal(true, pin.enabled?) - end - - def test_custom_getter_setter - # ensures that if datadog_pin is available in the class, it will - # be used instead of the default datadog_pin - obj = CustomPinSetGet.new - pin = Datadog::Pin.new('pin', app: 'app', app_type: 'db') - pin.onto(obj) - assert_equal('The PIN is set!', Datadog::Pin.get_from(obj)) - end - - def test_service_info_update - tracer = get_test_tracer - Datadog::Pin.new('test-service', app: 'test-app', app_type: 'test-type', tracer: tracer) - - assert(tracer.services.key?('test-service')) - assert_equal( - { 'app' => 'test-app', 'app_type' => 'test-type' }, - tracer.services['test-service'] - ) - end - - def test_service_info_update_with_missing_params - tracer = get_test_tracer - - # instantiating `Pin` without an `app` param (which is necessary for service info) - Datadog::Pin.new('test-service', app_type: 'test-type', tracer: tracer) - - assert(tracer.services.empty?) - end -end diff --git a/test/registry_test.rb b/test/registry_test.rb deleted file mode 100644 index 97649c9f2c..0000000000 --- a/test/registry_test.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'minitest/autorun' -require 'ddtrace' -require 'ddtrace/registry' - -module Datadog - class RegistryTest < Minitest::Test - def test_object_retrieval - registry = Registry.new - - object1 = Object.new - object2 = Object.new - - registry.add(:object1, object1) - registry.add(:object2, object2) - - assert_same(object1, registry[:object1]) - assert_same(object2, registry[:object2]) - end - - def test_hash_coercion - registry = Registry.new - - object1 = Object.new - object2 = Object.new - - registry.add(:object1, object1, true) - registry.add(:object2, object2, false) - - assert_equal({ object1: true, object2: false }, registry.to_h) - end - - def test_enumeration - registry = Registry.new - - object1 = Object.new - object2 = Object.new - - registry.add(:object1, object1, true) - registry.add(:object2, object2, false) - - assert(registry.respond_to?(:each)) - assert_kind_of(Enumerable, registry) - - # Enumerable#map - objects = registry.map(&:klass) - assert_kind_of(Array, objects) - assert_equal(2, objects.size) - assert_includes(objects, object1) - assert_includes(objects, object2) - end - - def test_registry_entry - entry = Registry::Entry.new(:array, Array, true) - - assert_equal(:array, entry.name) - assert_equal(Array, entry.klass) - assert_equal(true, entry.auto_patch) - end - end -end