newrelic_rpm 3.15.1.316 → 3.15.2.317

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +41 -0
  3. data/lib/new_relic/agent/agent.rb +1 -1
  4. data/lib/new_relic/agent/autostart.rb +1 -1
  5. data/lib/new_relic/agent/database.rb +72 -165
  6. data/lib/new_relic/agent/database/explain_plan_helpers.rb +127 -0
  7. data/lib/new_relic/agent/database/obfuscation_helpers.rb +2 -1
  8. data/lib/new_relic/agent/database/postgres_explain_obfuscator.rb +0 -2
  9. data/lib/new_relic/agent/instrumentation/active_record.rb +5 -5
  10. data/lib/new_relic/agent/instrumentation/active_record_subscriber.rb +10 -8
  11. data/lib/new_relic/agent/instrumentation/data_mapper.rb +2 -1
  12. data/lib/new_relic/agent/instrumentation/grape.rb +23 -9
  13. data/lib/new_relic/agent/instrumentation/mongodb_command_subscriber.rb +1 -6
  14. data/lib/new_relic/agent/instrumentation/padrino.rb +36 -0
  15. data/lib/new_relic/agent/method_tracer.rb +20 -18
  16. data/lib/new_relic/agent/new_relic_service.rb +3 -33
  17. data/lib/new_relic/agent/pipe_service.rb +0 -4
  18. data/lib/new_relic/agent/sql_sampler.rb +3 -3
  19. data/lib/new_relic/agent/transaction/trace_node.rb +2 -5
  20. data/lib/new_relic/agent/transaction_sampler.rb +2 -2
  21. data/lib/new_relic/metric_data.rb +6 -14
  22. data/lib/new_relic/version.rb +1 -1
  23. data/newrelic_rpm.gemspec +1 -1
  24. data/test/environments/lib/environments/runner.rb +1 -1
  25. data/test/environments/rails40/Gemfile +9 -0
  26. data/test/environments/rails41/Gemfile +9 -0
  27. data/test/environments/rails42/Gemfile +9 -0
  28. data/test/multiverse/lib/multiverse/suite.rb +12 -0
  29. data/test/multiverse/suites/agent_only/harvest_timestamps_test.rb +0 -31
  30. data/test/multiverse/suites/grape/Envfile +1 -1
  31. data/test/multiverse/suites/mongo/Envfile +3 -0
  32. data/test/multiverse/suites/mongo/mongo2_instrumentation_test.rb +3 -2
  33. data/test/multiverse/suites/padrino/Envfile +8 -0
  34. data/test/multiverse/suites/typhoeus/Envfile +21 -0
  35. data/test/new_relic/agent/database_test.rb +216 -144
  36. data/test/new_relic/agent/new_relic_service_test.rb +0 -58
  37. data/test/new_relic/metric_data_test.rb +24 -71
  38. data/test/new_relic/metric_spec_test.rb +3 -4
  39. data/test/new_relic/rack/developer_mode_test.rb +5 -2
  40. metadata +4 -2
@@ -12,7 +12,7 @@ module NewRelic
12
12
 
13
13
  MAJOR = 3
14
14
  MINOR = 15
15
- TINY = 1
15
+ TINY = 2
16
16
 
17
17
  begin
18
18
  require File.join(File.dirname(__FILE__), 'build')
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.version = NewRelic::VERSION::STRING
11
11
  s.required_ruby_version = '>= 1.8.7'
12
12
  s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
13
- s.authors = [ "Tim Krajcar", "Matthew Wear", "Katherine Wu", "Karl Sandwich" ]
13
+ s.authors = [ "Tim Krajcar", "Matthew Wear", "Katherine Wu", "Karl Sandwich", "Caito Scherr" ]
14
14
  s.date = Time.now.strftime('%Y-%m-%d')
15
15
  s.licenses = ['New Relic', 'MIT', 'Ruby']
16
16
  s.description = <<-EOS
@@ -97,7 +97,7 @@ module Environments
97
97
  bundling = `cd #{dir} && bundle install --local`
98
98
  unless $?.success?
99
99
  puts "Failed local bundle, trying again with full bundle..."
100
- bundling = `cd #{dir} && bundle install`
100
+ bundling = `cd #{dir} && bundle install --retry 3`
101
101
  end
102
102
 
103
103
  bundling = red(bundling) unless $?.success?
@@ -4,6 +4,15 @@ gem 'rake', '< 11'
4
4
 
5
5
  gem 'rails', '~>4.0.13'
6
6
 
7
+ # mime-types 2.99.1 and 3.x introduce mime-types-data
8
+ # mime-types-data requires Ruby version >= 2.0
9
+ # mime-types 2.99 requires Ruby version >= 1.9.3
10
+ if RUBY_VERSION < '1.9.3'
11
+ gem 'mime-types', '1.25.1'
12
+ elsif RUBY_VERSION < '2'
13
+ gem 'mime-types', '< 3'
14
+ end
15
+
7
16
  # Do not automatically require minitest, since this will break with rbx-2.2.5.
8
17
  # rbx-2.2.5 helpfully bundles minitest-5.3.0.
9
18
  # Our tests don't work with minitest-5.3.0.
@@ -4,6 +4,15 @@ gem 'rake', '< 11'
4
4
 
5
5
  gem 'rails', '~>4.1.10'
6
6
 
7
+ # mime-types 2.99.1 and 3.x introduce mime-types-data
8
+ # mime-types-data requires Ruby version >= 2.0
9
+ # mime-types 2.99 requires Ruby version >= 1.9.3
10
+ if RUBY_VERSION < '1.9.3'
11
+ gem 'mime-types', '1.25.1'
12
+ elsif RUBY_VERSION < '2'
13
+ gem 'mime-types', '< 3'
14
+ end
15
+
7
16
  gem 'minitest', '5.2.3'
8
17
  gem 'mocha', :require => false
9
18
  gem 'rack'
@@ -4,6 +4,15 @@ gem 'rake', '< 11'
4
4
 
5
5
  gem 'rails', '~>4.2.1'
6
6
 
7
+ # mime-types 2.99.1 and 3.x introduce mime-types-data
8
+ # mime-types-data requires Ruby version >= 2.0
9
+ # mime-types 2.99 requires Ruby version >= 1.9.3
10
+ if RUBY_VERSION < '1.9.3'
11
+ gem 'mime-types', '1.25.1'
12
+ elsif RUBY_VERSION < '2'
13
+ gem 'mime-types', '< 3'
14
+ end
15
+
7
16
  gem 'minitest', '5.2.3'
8
17
  gem 'mocha', :require => false
9
18
  gem 'rack'
@@ -173,6 +173,7 @@ module Multiverse
173
173
  f.print gemfile_text
174
174
  f.puts newrelic_gemfile_line unless gemfile_text =~ /^\s*gem .newrelic_rpm./
175
175
  f.puts jruby_openssl_line unless gemfile_text =~ /^\s*gem .jruby-openssl./ || (defined?(JRUBY_VERSION) && JRUBY_VERSION > '1.7')
176
+ f.puts mime_types_line unless gemfile_text =~ /^\s*gem .mime-types[^_-]./
176
177
  f.puts minitest_line unless gemfile_text =~ /^\s*gem .minitest[^_]./
177
178
  f.puts rake_line unless gemfile_text =~ /^\s*gem .rake[^_]./ || suite == 'rake'
178
179
  if RUBY_VERSION == "1.8.7"
@@ -219,6 +220,17 @@ module Multiverse
219
220
  "gem 'jruby-openssl', '~> 0.9.10', :require => false, :platforms => [:jruby]"
220
221
  end
221
222
 
223
+ # mime-types got a new dependency after 2.99 that breaks compatibility with ruby < 2.0
224
+ def mime_types_line
225
+ if RUBY_VERSION <= '1.9.3'
226
+ "gem 'mime-types', '1.25.1'"
227
+ elsif RUBY_VERSION < '2'
228
+ "gem 'mime-types', '2.99'"
229
+ else
230
+ ''
231
+ end
232
+ end
233
+
222
234
  def minitest_line
223
235
  "gem 'minitest', '~> 4.7.5', :require => false"
224
236
  end
@@ -63,37 +63,6 @@ class HarvestTimestampsTest < Minitest::Test
63
63
  end
64
64
  end
65
65
 
66
- def test_timestamps_updated_even_if_filling_metric_id_cache_fails
67
- t0 = freeze_time.to_f
68
-
69
- simulate_fork
70
- NewRelic::Agent.after_fork
71
-
72
- # Induce a failure in filling the metric ID cache by handing back a bogus
73
- # response to the first metric_data post.
74
- $collector.stub('metric_data', [[[[]]]], 200)
75
- t1 = advance_time(10).to_f
76
- trigger_metric_data_post
77
- first_post = last_metric_data_post
78
-
79
- $collector.reset
80
- t2 = advance_time(10).to_f
81
- trigger_metric_data_post
82
- second_post = last_metric_data_post
83
-
84
- if RUBY_VERSION == '1.8.7'
85
- # 1.8 + JSON is finicky about comparing floats.
86
- # If the timestamps are within 0.001 seconds, it's Good Enough.
87
- assert_in_delta(t0, first_post[1], 0.001)
88
- assert_in_delta(t1, first_post[2], 0.001)
89
- assert_in_delta(t1, second_post[1], 0.001)
90
- assert_in_delta(t2, second_post[2], 0.001)
91
- else
92
- assert_equal([t0, t1], first_post[1..2])
93
- assert_equal([t1, t2], second_post[1..2])
94
- end
95
- end
96
-
97
66
  def trigger_metric_data_post
98
67
  NewRelic::Agent.agent.send(:transmit_data)
99
68
  end
@@ -2,7 +2,7 @@ suite_condition("Grape is only supported for versions >= 1.9.3") do
2
2
  RUBY_VERSION >= '1.9.3'
3
3
  end
4
4
 
5
- versions = %w(0.14.0 0.13.0 0.12.0 0.11.0 0.10.0 0.9.0 0.8.0 0.7.0 0.6.1 0.5.0 0.4.1 0.3.2 0.2.6 0.2.0 0.1.5)
5
+ versions = %w(0.16.1 0.15.0 0.14.0 0.13.0 0.12.0 0.11.0 0.10.0 0.9.0 0.8.0 0.7.0 0.6.1 0.5.0 0.4.1 0.3.2 0.2.6 0.2.0 0.1.5)
6
6
 
7
7
  versions.each do |version|
8
8
  gemfile <<-RB
@@ -1,4 +1,7 @@
1
1
  if RUBY_VERSION >= '1.9.3'
2
+ gemfile <<-RB
3
+ gem 'mongo', '~>2.2.0'
4
+ RB
2
5
  gemfile <<-RB
3
6
  gem 'mongo', '~>2.1.0'
4
7
  RB
@@ -270,12 +270,13 @@ if NewRelic::Agent::Datastores::Mongo.is_supported_version? &&
270
270
  :collection => @collection_name,
271
271
  'insert' => @collection_name,
272
272
  :operation => :insert,
273
- 'writeConcern' => { :w => 1 },
274
273
  'ordered' => true
275
274
  }
276
275
 
277
276
  result = node.params[:statement]
278
-
277
+ # the writeConcern is added by some, but not all versions of the mongo driver,
278
+ # we don't care if it's present or not, just that the statement is noticed
279
+ result.delete('writeConcern')
279
280
  assert_equal expected, result
280
281
  end
281
282
 
@@ -1,3 +1,11 @@
1
+ if RUBY_VERSION >= '1.9.3' # padrino v0.12.0 dropped support for ruby 1.8.7 and requres ruby 1.9.3 or greater
2
+ gemfile <<-RB
3
+ gem 'activesupport', '~> 3'
4
+ gem 'padrino', '~> 0.13.0'
5
+ gem 'rack-test', :require => 'rack/test'
6
+ RB
7
+ end
8
+
1
9
  if RUBY_VERSION > '1.8.7' # padrino-core 0.11.0 requires http_router 0.11.0 which has syntax errors in 1.8.7.
2
10
  gemfile <<-RB
3
11
  gem 'activesupport', '~> 3'
@@ -31,6 +31,13 @@ gemfile <<-RB
31
31
  # Compatibility issues with ethon 0.5.12 https://github.com/typhoeus/ethon/issues/51
32
32
  gem 'ethon', '0.5.11'
33
33
 
34
+ # ethon 0.5.11 requires mime-types ~> 1.18
35
+ # normally, this wouldn't be a problem, but
36
+ # we have other checks to work around other
37
+ # mime-type dependency issues, so we need
38
+ # to specify it here.
39
+ gem 'mime-types', '~> 1.18'
40
+
34
41
  gem 'typhoeus', '~> 0.5.4'
35
42
  gem 'rack'
36
43
  gem 'json', :platforms => [:rbx, :mri_18]
@@ -41,6 +48,13 @@ gemfile <<-RB
41
48
  # Compatibility issues with ethon 0.5.12 https://github.com/typhoeus/ethon/issues/51
42
49
  gem 'ethon', '0.5.11'
43
50
 
51
+ # ethon 0.5.11 requires mime-types ~> 1.18
52
+ # normally, this wouldn't be a problem, but
53
+ # we have other checks to work around other
54
+ # mime-type dependency issues, so we need
55
+ # to specify it here.
56
+ gem 'mime-types', '~> 1.18'
57
+
44
58
  gem 'typhoeus', '0.5.3'
45
59
  gem 'rack'
46
60
  gem 'json', :platforms => [:rbx, :mri_18]
@@ -51,6 +65,13 @@ gemfile <<-RB
51
65
  # Compatibility issues with ethon 0.5.12 https://github.com/typhoeus/ethon/issues/51
52
66
  gem 'ethon', '0.5.11'
53
67
 
68
+ # ethon 0.5.11 requires mime-types ~> 1.18
69
+ # normally, this wouldn't be a problem, but
70
+ # we have other checks to work around other
71
+ # mime-type dependency issues, so we need
72
+ # to specify it here.
73
+ gem 'mime-types', '~> 1.18'
74
+
54
75
  gem 'typhoeus', '0.5.2'
55
76
  gem 'rack'
56
77
  gem 'json', :platforms => [:rbx, :mri_18]
@@ -6,85 +6,113 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..',
6
6
  'test_helper'))
7
7
  require 'new_relic/agent/database'
8
8
  class NewRelic::Agent::DatabaseTest < Minitest::Test
9
- def setup
10
- @explainer = NewRelic::Agent::Instrumentation::ActiveRecord::EXPLAINER
11
- end
12
-
13
9
  def teardown
14
10
  NewRelic::Agent::Database::Obfuscator.instance.reset
15
11
  end
16
12
 
17
13
  def test_adapter_from_config_string
18
14
  config = { :adapter => 'mysql' }
19
- assert_equal('mysql', NewRelic::Agent::Database.adapter_from_config(config))
15
+ statement = NewRelic::Agent::Database::Statement.new('some query', config)
16
+ assert_equal(:mysql, statement.adapter)
20
17
  end
21
18
 
22
19
  def test_adapter_from_config_symbol
23
20
  config = { :adapter => :mysql }
24
- assert_equal('mysql', NewRelic::Agent::Database.adapter_from_config(config))
21
+ statement = NewRelic::Agent::Database::Statement.new('some query', config)
22
+ assert_equal(:mysql, statement.adapter)
25
23
  end
26
24
 
27
25
  def test_adapter_from_config_uri_jdbc_postgresql
28
26
  config = { :uri=>"jdbc:postgresql://host/database?user=posgres" }
29
- assert_equal('postgresql', NewRelic::Agent::Database.adapter_from_config(config))
27
+ statement = NewRelic::Agent::Database::Statement.new('some query', config)
28
+ assert_equal(:postgres, statement.adapter)
30
29
  end
31
30
 
32
31
  def test_adapter_from_config_uri_jdbc_mysql
33
32
  config = { :uri=>"jdbc:mysql://host/database" }
34
- assert_equal('mysql', NewRelic::Agent::Database.adapter_from_config(config))
33
+ statement = NewRelic::Agent::Database::Statement.new('some query', config)
34
+ assert_equal(:mysql, statement.adapter)
35
35
  end
36
36
 
37
37
  def test_adapter_from_config_uri_jdbc_sqlite
38
38
  config = { :uri => "jdbc:sqlite::memory" }
39
- assert_equal('sqlite', NewRelic::Agent::Database.adapter_from_config(config))
39
+ statement = NewRelic::Agent::Database::Statement.new('some query', config)
40
+ assert_equal(:sqlite, statement.adapter)
41
+ end
42
+
43
+ # An ActiveRecord::Result is what you get back when executing a
44
+ # query using exec_query on the connection, which is what we're
45
+ # doing now for explain plans in AR4 instrumentation
46
+ def test_explain_sql_with_mysql2_activerecord_result
47
+ return unless defined?(::ActiveRecord::Result)
48
+ config = {:adapter => 'mysql2'}
49
+ sql = 'SELECT * FROM spells where id=1'
50
+
51
+ columns = ["id", "select_type", "table", "type", "possible_keys", "key", "key_len", "ref", "rows", "Extra"]
52
+ rows = [["1", "SIMPLE", "spells", "const", "PRIMARY", "PRIMARY", "4", "const", "1", ""]]
53
+ activerecord_result = ::ActiveRecord::Result.new(columns, rows)
54
+ explainer = lambda { |statement| activerecord_result}
55
+
56
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
57
+ result = NewRelic::Agent::Database.explain_sql(statement)
58
+
59
+ assert_equal([columns, rows], result)
60
+ end
61
+
62
+ def test_explain_sql_obfuscates_for_postgres_activerecord_result
63
+ return unless defined?(::ActiveRecord::Result)
64
+ config = {:adapter => 'postgres'}
65
+ sql = "SELECT * FROM blogs WHERE blogs.id=1234 AND blogs.title='sensitive text'"
66
+
67
+ columns = ["stuffs"]
68
+ rows = [[" Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540)"],
69
+ [" Index Cond: (id = 1234)"],
70
+ [" Filter: ((title)::text = 'sensitive text'::text)"]]
71
+ activerecord_result = ::ActiveRecord::Result.new(columns, rows)
72
+ explainer = lambda { |statement| activerecord_result}
73
+
74
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
75
+ expected_result = [['QUERY PLAN'],
76
+ [[" Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540)"],
77
+ [" Index Cond: ?"],
78
+ [" Filter: ?"]
79
+ ]]
80
+
81
+ with_config(:'transaction_tracer.record_sql' => 'obfuscated') do
82
+ result = NewRelic::Agent::Database.explain_sql(statement)
83
+ assert_equal(expected_result, result)
84
+ end
40
85
  end
41
86
 
42
- def test_explain_sql_select_with_mysql_connection
87
+ # The following tests in the format _with_##_explain_result go
88
+ # through the different kinds of results when using an explainer
89
+ # that calls .execute on the connection
90
+
91
+ def test_explain_sql_select_with_mysql_explain_result
43
92
  config = {:adapter => 'mysql'}
44
- config.default('val')
45
93
  sql = 'SELECT foo'
46
- connection = mock('mysql connection')
94
+
47
95
  plan = {
48
96
  "select_type"=>"SIMPLE", "key_len"=>nil, "table"=>"blogs", "id"=>"1",
49
97
  "possible_keys"=>nil, "type"=>"ALL", "Extra"=>"", "rows"=>"2",
50
98
  "ref"=>nil, "key"=>nil
51
99
  }
52
- result = mock('explain plan')
53
- result.expects(:each_hash).yields(plan)
54
- # two rows, two columns
55
- connection.expects(:execute).with('EXPLAIN SELECT foo').returns(result)
56
- NewRelic::Agent::Database.stubs(:get_connection).returns(connection)
57
- result = NewRelic::Agent::Database.explain_sql(sql, config, @explainer)
58
- assert_equal(plan.keys.sort, result[0].sort)
59
- assert_equal(plan.values.compact.sort, result[1][0].compact.sort)
60
- end
100
+ explainer_result = mock('explain plan')
101
+ explainer_result.expects(:each_hash).yields(plan)
102
+ explainer = lambda { |statement| explainer_result}
61
103
 
62
- def test_explain_sql_select_with_mysql2_connection_sequel
63
- config = { :adapter => 'mysql2' }
64
- config.default('val')
65
- sql = 'SELECT * FROM items'
66
-
67
- # Sequel returns explain plans to us as one giant preformatted string rather
68
- # than individual rows.
69
- plan_string = [
70
- "+--+-----------+-----+----+-------------+---+-------+---+----+-----+",
71
- "|id|select_type|table|type|possible_keys|key|key_len|ref|rows|Extra|",
72
- "+--+-----------+-----+----+-------------+---+-------+---+----+-----+",
73
- "| 1|SIMPLE |items|ALL | | | | | 3| |",
74
- "+--+-----------+-----+----+-------------+---+-------+---+----+-----+"
75
- ].join("\n")
104
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
105
+ result = NewRelic::Agent::Database.explain_sql(statement)
106
+ expected_result = [["select_type", "key_len", "table", "id", "possible_keys", "type",
107
+ "Extra", "rows", "ref", "key"],
108
+ [["SIMPLE", nil, "blogs", "1", nil, "ALL", "", "2", nil, nil]]]
76
109
 
77
- connection = mock('mysql connection')
78
- connection.expects(:execute).with('EXPLAIN SELECT * FROM items').returns(plan_string)
79
- NewRelic::Agent::Database.stubs(:get_connection).returns(connection)
80
- result = NewRelic::Agent::Database.explain_sql(sql, config, @explainer)
81
- assert_nil(result[0])
82
- assert_equal([plan_string], result[1])
110
+ assert_equal(expected_result[0].sort, result[0].sort, "Headers don't match")
111
+ assert_equal(expected_result[1][0].compact.sort, result[1][0].compact.sort, "Values don't match")
83
112
  end
84
113
 
85
- def test_explain_sql_select_with_mysql_connection_sequel
86
- config = { :adapter => 'mysql' }
87
- config.default('val')
114
+ def test_explain_sql_select_with_sequel
115
+ config = { :adapter => 'mysql2' }
88
116
  sql = 'SELECT * FROM items'
89
117
 
90
118
  # Sequel returns explain plans to us as one giant preformatted string rather
@@ -96,114 +124,124 @@ class NewRelic::Agent::DatabaseTest < Minitest::Test
96
124
  "| 1|SIMPLE |items|ALL | | | | | 3| |",
97
125
  "+--+-----------+-----+----+-------------+---+-------+---+----+-----+"
98
126
  ].join("\n")
127
+ explainer = lambda { |statement| plan_string}
128
+
129
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
130
+ result = NewRelic::Agent::Database.explain_sql(statement)
99
131
 
100
- connection = mock('mysql connection')
101
- connection.expects(:execute).with('EXPLAIN SELECT * FROM items').returns(plan_string)
102
- NewRelic::Agent::Database.stubs(:get_connection).returns(connection)
103
- result = NewRelic::Agent::Database.explain_sql(sql, config, @explainer)
104
132
  assert_nil(result[0])
105
133
  assert_equal([plan_string], result[1])
106
134
  end
107
135
 
108
- def test_explain_sql_select_with_mysql2_connection
136
+ def test_explain_sql_select_with_mysql2_explain_result
109
137
  config = {:adapter => 'mysql2'}
110
- config.default('val')
111
138
  sql = 'SELECT foo'
112
- connection = mock('mysql connection')
113
139
 
114
140
  plan_fields = ["select_type", "key_len", "table", "id", "possible_keys", "type", "Extra", "rows", "ref", "key"]
115
141
  plan_row = ["SIMPLE", nil, "blogs", "1", nil, "ALL", "", "2", nil, nil ]
142
+ explainer_result = mock('explain plan')
143
+ explainer_result.expects(:fields).returns(plan_fields)
144
+ explainer_result.expects(:each).yields(plan_row)
145
+ explainer = lambda { |statement| explainer_result}
116
146
 
117
- result = mock('explain plan')
118
- result.expects(:fields).returns(plan_fields)
119
- result.expects(:each).yields(plan_row)
147
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
148
+ result = NewRelic::Agent::Database.explain_sql(statement)
149
+ expected_result = [["select_type", "key_len", "table", "id", "possible_keys", "type",
150
+ "Extra", "rows", "ref","key"],
151
+ [["SIMPLE", nil, "blogs", "1", nil, "ALL", "", "2", nil, nil]]]
120
152
 
121
- # two rows, two columns
122
- connection.expects(:execute).with('EXPLAIN SELECT foo').returns(result)
123
- NewRelic::Agent::Database.stubs(:get_connection).returns(connection)
124
- result = NewRelic::Agent::Database.explain_sql(sql, config, @explainer)
125
- assert_equal(plan_fields.sort, result[0].sort)
126
- assert_equal(plan_row.compact.sort, result[1][0].compact.sort)
153
+ assert_equal(expected_result[0].sort, result[0].sort, "Headers don't match")
154
+ assert_equal(expected_result[1][0].compact.sort, result[1][0].compact.sort, "Values don't match")
127
155
  end
128
156
 
129
- def test_explain_sql_one_select_with_pg_connection
157
+ def test_explain_sql_one_select_with_pg_explain_result
130
158
  config = {:adapter => 'postgresql'}
131
- config.default('val')
132
159
  sql = 'select count(id) from blogs limit 1'
133
- connection = stub('pg connection', :disconnect! => true)
160
+
134
161
  plan = [{"QUERY PLAN"=>"Limit (cost=11.75..11.76 rows=1 width=4)"},
135
162
  {"QUERY PLAN"=>" -> Aggregate (cost=11.75..11.76 rows=1 width=4)"},
136
163
  {"QUERY PLAN"=>" -> Seq Scan on blogs (cost=0.00..11.40 rows=140 width=4)"}]
137
- connection.expects(:execute).returns(plan)
138
- NewRelic::Agent::Database.stubs(:get_connection).returns(connection)
139
- assert_equal([['QUERY PLAN'],
140
- [["Limit (cost=11.75..11.76 rows=1 width=4)"],
141
- [" -> Aggregate (cost=11.75..11.76 rows=1 width=4)"],
142
- [" -> Seq Scan on blogs (cost=0.00..11.40 rows=140 width=4)"]]],
143
- NewRelic::Agent::Database.explain_sql(sql, config, @explainer))
164
+ explainer = lambda { |statement| plan}
165
+
166
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
167
+ result = NewRelic::Agent::Database.explain_sql(statement)
168
+ expected_result = [['QUERY PLAN'],
169
+ [["Limit (cost=11.75..11.76 rows=1 width=4)"],
170
+ [" -> Aggregate (cost=11.75..11.76 rows=1 width=4)"],
171
+ [" -> Seq Scan on blogs (cost=0.00..11.40 rows=140 width=4)"]
172
+ ]]
173
+
174
+ assert_equal expected_result, result
144
175
  end
145
176
 
146
- def test_explain_sql_one_select_with_pg_connection_string
177
+ def test_explain_sql_one_select_with_pg_explain_string_result
147
178
  config = {:adapter => 'postgresql'}
148
- config.default('val')
149
179
  sql = 'select count(id) from blogs limit 1'
150
- connection = stub('pg connection', :disconnect! => true)
180
+
151
181
  plan = "Limit (cost=11.75..11.76 rows=1 width=4)
152
182
  -> Aggregate (cost=11.75..11.76 rows=1 width=4)
153
183
  -> Seq Scan on blogs (cost=0.00..11.40 rows=140 width=4)"
184
+ explainer = lambda { |statement| plan}
185
+
186
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
187
+ result = NewRelic::Agent::Database.explain_sql(statement)
188
+ expected_result = [['QUERY PLAN'],
189
+ [["Limit (cost=11.75..11.76 rows=1 width=4)"],
190
+ [" -> Aggregate (cost=11.75..11.76 rows=1 width=4)"],
191
+ [" -> Seq Scan on blogs (cost=0.00..11.40 rows=140 width=4)"]
192
+ ]]
154
193
 
155
- connection.expects(:execute).returns(plan)
156
- NewRelic::Agent::Database.stubs(:get_connection).returns(connection)
157
- assert_equal([['QUERY PLAN'],
158
- [["Limit (cost=11.75..11.76 rows=1 width=4)"],
159
- [" -> Aggregate (cost=11.75..11.76 rows=1 width=4)"],
160
- [" -> Seq Scan on blogs (cost=0.00..11.40 rows=140 width=4)"]]],
161
- NewRelic::Agent::Database.explain_sql(sql, config, @explainer))
194
+ assert_equal expected_result, result
162
195
  end
163
196
 
164
197
  def test_explain_sql_obfuscates_for_postgres
165
198
  config = {:adapter => 'postgresql'}
166
- config.default('val')
167
199
  sql = "SELECT * FROM blogs WHERE blogs.id=1234 AND blogs.title='sensitive text'"
168
- connection = stub('pg connection', :disconnect! => true)
200
+
169
201
  plan = [{"QUERY PLAN"=>" Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540)"},
170
202
  {"QUERY PLAN"=>" Index Cond: (id = 1234)"},
171
203
  {"QUERY PLAN"=>" Filter: ((title)::text = 'sensitive text'::text)"}]
172
- connection.expects(:execute).returns(plan)
173
- NewRelic::Agent::Database.stubs(:get_connection).returns(connection)
204
+ explainer = lambda { |statement| plan}
205
+
206
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
207
+ expected_result = [['QUERY PLAN'],
208
+ [[" Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540)"],
209
+ [" Index Cond: ?"],
210
+ [" Filter: ?"]
211
+ ]]
212
+
174
213
  with_config(:'transaction_tracer.record_sql' => 'obfuscated') do
175
- assert_equal([['QUERY PLAN'],
176
- [[" Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540)"],
177
- [" Index Cond: ?"],
178
- [" Filter: ?"]]],
179
- NewRelic::Agent::Database.explain_sql(sql, config, @explainer))
214
+ result = NewRelic::Agent::Database.explain_sql(statement)
215
+ assert_equal expected_result, result
180
216
  end
181
217
  end
182
218
 
183
219
  def test_explain_sql_does_not_obfuscate_if_record_sql_raw
184
220
  config = {:adapter => 'postgresql'}
185
- config.default('val')
186
221
  sql = "SELECT * FROM blogs WHERE blogs.id=1234 AND blogs.title='sensitive text'"
187
- connection = stub('pg connection', :disconnect! => true)
222
+
188
223
  plan = [{"QUERY PLAN"=>" Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540)"},
189
224
  {"QUERY PLAN"=>" Index Cond: (id = 1234)"},
190
225
  {"QUERY PLAN"=>" Filter: ((title)::text = 'sensitive text'::text)"}]
191
- connection.expects(:execute).returns(plan)
192
- NewRelic::Agent::Database.stubs(:get_connection).returns(connection)
226
+ explainer = lambda { |statement| plan}
227
+
228
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
229
+ expected_result = [['QUERY PLAN'],
230
+ [[" Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540)"],
231
+ [" Index Cond: (id = 1234)"],
232
+ [" Filter: ((title)::text = 'sensitive text'::text)"]
233
+ ]]
234
+
193
235
  with_config(:'transaction_tracer.record_sql' => 'raw') do
194
- assert_equal([['QUERY PLAN'],
195
- [[" Index Scan using blogs_pkey on blogs (cost=0.00..8.27 rows=1 width=540)"],
196
- [" Index Cond: (id = 1234)"],
197
- [" Filter: ((title)::text = 'sensitive text'::text)"]]],
198
- NewRelic::Agent::Database.explain_sql(sql, config, @explainer))
236
+ result = NewRelic::Agent::Database.explain_sql(statement)
237
+ assert_equal expected_result, result
199
238
  end
200
239
  end
201
240
 
202
- def test_explain_sql_select_with_sqlite_connection
241
+ def test_explain_sql_select_with_sqlite_explain_string_result
203
242
  config = {:adapter => 'sqlite'}
204
- config.default('val')
205
243
  sql = 'SELECT foo'
206
- connection = mock('sqlite connection')
244
+
207
245
  plan = [
208
246
  {"addr"=>0, "opcode"=>"Trace", "p1"=>0, "p2"=>0, "p3"=>0, "p4"=>"", "p5"=>"00", "comment"=>nil, 0=>0, 1=>"Trace", 2=>0, 3=>0, 4=>0, 5=>"", 6=>"00", 7=>nil},
209
247
  {"addr"=>1, "opcode"=>"Goto", "p1"=>0, "p2"=>5, "p3"=>0, "p4"=>"", "p5"=>"00", "comment"=>nil, 0=>1, 1=>"Goto", 2=>0, 3=>5, 4=>0, 5=>"", 6=>"00", 7=>nil},
@@ -212,9 +250,10 @@ class NewRelic::Agent::DatabaseTest < Minitest::Test
212
250
  {"addr"=>4, "opcode"=>"Halt", "p1"=>0, "p2"=>0, "p3"=>0, "p4"=>"", "p5"=>"00", "comment"=>nil, 0=>4, 1=>"Halt", 2=>0, 3=>0, 4=>0, 5=>"", 6=>"00", 7=>nil},
213
251
  {"addr"=>5, "opcode"=>"Goto", "p1"=>0, "p2"=>2, "p3"=>0, "p4"=>"", "p5"=>"00", "comment"=>nil, 0=>5, 1=>"Goto", 2=>0, 3=>2, 4=>0, 5=>"", 6=>"00", 7=>nil}
214
252
  ]
215
- connection.expects(:execute).with('EXPLAIN SELECT foo').returns(plan)
216
- NewRelic::Agent::Database.stubs(:get_connection).returns(connection)
217
- result = NewRelic::Agent::Database.explain_sql(sql, config, @explainer)
253
+ explainer = lambda { |statement| plan}
254
+
255
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
256
+ result = NewRelic::Agent::Database.explain_sql(statement)
218
257
 
219
258
  expected_headers = %w[addr opcode p1 p2 p3 p4 p5 comment]
220
259
  expected_values = plan.map do |row|
@@ -225,68 +264,96 @@ class NewRelic::Agent::DatabaseTest < Minitest::Test
225
264
  assert_equal(expected_values, result[1])
226
265
  end
227
266
 
228
- def test_dont_collect_explain_for_parameterized_query
267
+ def test_explain_sql_select_with_sqlite3_explain_string_result
268
+ config = {:adapter => 'sqlite3'}
269
+ sql = 'SELECT foo'
270
+
271
+ plan = [
272
+ {"addr"=>0, "opcode"=>"Trace", "p1"=>0, "p2"=>0, "p3"=>0, "p4"=>"", "p5"=>"00", "comment"=>nil, 0=>0, 1=>"Trace", 2=>0, 3=>0, 4=>0, 5=>"", 6=>"00", 7=>nil},
273
+ {"addr"=>1, "opcode"=>"Goto", "p1"=>0, "p2"=>5, "p3"=>0, "p4"=>"", "p5"=>"00", "comment"=>nil, 0=>1, 1=>"Goto", 2=>0, 3=>5, 4=>0, 5=>"", 6=>"00", 7=>nil},
274
+ {"addr"=>2, "opcode"=>"String8", "p1"=>0, "p2"=>1, "p3"=>0, "p4"=>"foo", "p5"=>"00", "comment"=>nil, 0=>2, 1=>"String8", 2=>0, 3=>1, 4=>0, 5=>"foo", 6=>"00", 7=>nil},
275
+ {"addr"=>3, "opcode"=>"ResultRow", "p1"=>1, "p2"=>1, "p3"=>0, "p4"=>"", "p5"=>"00", "comment"=>nil, 0=>3, 1=>"ResultRow", 2=>1, 3=>1, 4=>0, 5=>"", 6=>"00", 7=>nil},
276
+ {"addr"=>4, "opcode"=>"Halt", "p1"=>0, "p2"=>0, "p3"=>0, "p4"=>"", "p5"=>"00", "comment"=>nil, 0=>4, 1=>"Halt", 2=>0, 3=>0, 4=>0, 5=>"", 6=>"00", 7=>nil},
277
+ {"addr"=>5, "opcode"=>"Goto", "p1"=>0, "p2"=>2, "p3"=>0, "p4"=>"", "p5"=>"00", "comment"=>nil, 0=>5, 1=>"Goto", 2=>0, 3=>2, 4=>0, 5=>"", 6=>"00", 7=>nil}
278
+ ]
279
+ explainer = lambda { |statement| plan}
280
+
281
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
282
+ result = NewRelic::Agent::Database.explain_sql(statement)
283
+
284
+ expected_headers = %w[addr opcode p1 p2 p3 p4 p5 comment]
285
+ expected_values = plan.map do |row|
286
+ expected_headers.map { |h| row[h] }
287
+ end
288
+
289
+ assert_equal(expected_headers.sort, result[0].sort)
290
+ assert_equal(expected_values, result[1])
291
+ end
292
+
293
+ def test_explain_sql_no_sql
294
+ statement = NewRelic::Agent::Database::Statement.new('', nil)
295
+ assert_equal(nil, NewRelic::Agent::Database.explain_sql(statement))
296
+ end
297
+
298
+ def test_explain_sql_non_select
299
+ statement = NewRelic::Agent::Database::Statement.new('foo', mock('config'), mock('explainer'))
300
+ assert_equal([], NewRelic::Agent::Database.explain_sql(statement))
301
+ end
302
+
303
+ def test_dont_collect_explain_for_truncated_query
229
304
  config = {:adapter => 'postgresql'}
230
- config.default('val')
231
- connection = mock('param connection')
232
- connection.expects(:execute).never
233
- NewRelic::Agent::Database.stubs(:get_connection).with(config).returns(connection)
234
- expects_logging(:debug, 'Unable to collect explain plan for parameterized query.')
305
+ sql = 'SELECT * FROM table WHERE id IN (1,2,3,4,5...'
306
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, mock('explainer'))
307
+
308
+ expects_logging(:debug, 'Unable to collect explain plan for truncated query.')
309
+ assert_equal [], NewRelic::Agent::Database.explain_sql(statement)
310
+ end
235
311
 
312
+ def test_dont_collect_explain_for_parameterized_query
313
+ config = {:adapter => 'postgresql'}
236
314
  sql = 'SELECT * FROM table WHERE id = $1'
237
- assert_equal [], NewRelic::Agent::Database.explain_sql(sql, config, @explainer)
315
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, mock('explainer'))
316
+
317
+ expects_logging(:debug, 'Unable to collect explain plan for parameter-less parameterized query.')
318
+ assert_equal [], NewRelic::Agent::Database.explain_sql(statement)
238
319
  end
239
320
 
240
321
  def test_do_collect_explain_for_parameter_looking_literal
241
322
  config = {:adapter => 'postgresql'}
242
- config.default('val')
243
- connection = mock('literal connection')
244
- plan = [{"QUERY PLAN"=>"Some Jazz"}]
245
323
  sql = "SELECT * FROM table WHERE id = 'noise $11'"
246
- confirm_sql = Proc.new {|query| query.include?(sql) }
247
- connection.expects(:execute).with(&confirm_sql).returns(plan)
248
- NewRelic::Agent::Database.stubs(:get_connection).with(config).returns(connection)
324
+ plan = [{"QUERY PLAN"=>"Some Jazz"}]
325
+ explainer = lambda { |statement| plan}
326
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer)
249
327
 
250
328
  assert_equal([['QUERY PLAN'], [["Some Jazz"]]],
251
- NewRelic::Agent::Database.explain_sql(sql, config, @explainer))
329
+ NewRelic::Agent::Database.explain_sql(statement))
252
330
  end
253
331
 
254
- def test_dont_collect_explain_for_truncated_query
332
+ def test_do_collect_explain_for_parameterized_query_with_binds
255
333
  config = {:adapter => 'postgresql'}
256
- config.default('val')
257
- connection = mock('truncated connection')
258
- connection.expects(:execute).never
259
- NewRelic::Agent::Database.stubs(:get_connection).with(config).returns(connection)
260
- expects_logging(:debug, 'Unable to collect explain plan for truncated query.')
334
+ sql = 'SELECT * FROM table WHERE id = $1'
335
+ # binds objects don't actually look like this, just need non-blank for test
336
+ binds = "values for the parameters"
337
+ plan = [{"QUERY PLAN"=>"Some Jazz"}]
338
+ explainer = lambda { |statement| plan}
339
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, explainer, binds)
261
340
 
262
- sql = 'SELECT * FROM table WHERE id IN (1,2,3,4,5...'
263
- assert_equal [], NewRelic::Agent::Database.explain_sql(sql, config, @explainer)
341
+ assert_equal([['QUERY PLAN'], [["Some Jazz"]]],
342
+ NewRelic::Agent::Database.explain_sql(statement))
264
343
  end
265
344
 
266
345
  def test_dont_collect_explain_if_adapter_not_recognized
267
346
  config = {:adapter => 'dorkdb'}
268
- config.default('val')
269
- connection = mock('connection')
270
- connection.expects(:execute).never
271
- NewRelic::Agent::Database.stubs(:get_connection).with(config).returns(connection)
272
- expects_logging(:debug, "Not collecting explain plan because an unknown connection adapter ('dorkdb') was used.")
273
-
274
347
  sql = 'SELECT * FROM table WHERE id IN (1,2,3,4,5)'
275
- assert_equal [], NewRelic::Agent::Database.explain_sql(sql, config, @explainer)
276
- end
348
+ statement = NewRelic::Agent::Database::Statement.new(sql, config, mock('explainer'))
277
349
 
278
- def test_explain_sql_no_sql
279
- assert_equal(nil, NewRelic::Agent::Database.explain_sql(nil, nil))
350
+ expects_logging(:debug, "Not collecting explain plan because an unknown connection adapter ('dorkdb') was used.")
351
+ assert_equal [], NewRelic::Agent::Database.explain_sql(statement)
280
352
  end
281
353
 
282
354
  def test_explain_sql_no_connection_config
283
- assert_equal(nil, NewRelic::Agent::Database.explain_sql('select foo', nil))
284
- end
285
-
286
- def test_explain_sql_non_select
287
- assert_equal([], NewRelic::Agent::Database.explain_sql('foo',
288
- mock('config'),
289
- @explainer))
355
+ statement = NewRelic::Agent::Database::Statement.new('select foo', nil)
356
+ assert_equal(nil, NewRelic::Agent::Database.explain_sql(statement))
290
357
  end
291
358
 
292
359
  def test_explain_sql_one_select_no_connection
@@ -296,7 +363,12 @@ class NewRelic::Agent::DatabaseTest < Minitest::Test
296
363
  # errors to percolate up.
297
364
  config = mock('config')
298
365
  config.stubs(:[]).returns(nil)
299
- assert_equal([], NewRelic::Agent::Database.explain_sql('SELECT', config, @explainer))
366
+
367
+ # if you have an invalid config or no connection, the explainer returns nil
368
+ explainer = lambda { |statement| nil}
369
+ statement = NewRelic::Agent::Database::Statement.new('SELECT', config, explainer)
370
+
371
+ assert_equal([], NewRelic::Agent::Database.explain_sql(statement))
300
372
  end
301
373
 
302
374
  # See SqlObfuscationTest, which uses cross agent tests for the basic SQL