newrelic_rpm 3.14.2.312 → 3.14.3.313

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/.travis.yml +6 -5
  3. data/CHANGELOG +22 -0
  4. data/lib/new_relic/agent/agent.rb +0 -4
  5. data/lib/new_relic/agent/configuration/default_source.rb +114 -107
  6. data/lib/new_relic/agent/database.rb +17 -1
  7. data/lib/new_relic/agent/database/obfuscation_helpers.rb +68 -48
  8. data/lib/new_relic/agent/database/obfuscator.rb +4 -23
  9. data/lib/new_relic/agent/database/postgres_explain_obfuscator.rb +1 -1
  10. data/lib/new_relic/agent/instrumentation/data_mapper.rb +20 -1
  11. data/lib/new_relic/agent/instrumentation/evented_subscriber.rb +1 -1
  12. data/lib/new_relic/agent/rules_engine.rb +39 -2
  13. data/lib/new_relic/agent/rules_engine/segment_terms_rule.rb +27 -5
  14. data/lib/new_relic/agent/sql_sampler.rb +7 -3
  15. data/lib/new_relic/language_support.rb +8 -0
  16. data/lib/new_relic/version.rb +1 -1
  17. data/lib/tasks/config.html.erb +5 -1
  18. data/lib/tasks/config.rake +10 -2
  19. data/lib/tasks/config.text.erb +6 -5
  20. data/test/environments/rails32/Gemfile +6 -1
  21. data/test/fixtures/cross_agent_tests/aws.json +95 -1
  22. data/test/fixtures/cross_agent_tests/cat/README.md +28 -0
  23. data/test/fixtures/cross_agent_tests/cat/cat_map.json +595 -0
  24. data/test/fixtures/cross_agent_tests/cat/path_hashing.json +51 -0
  25. data/test/fixtures/cross_agent_tests/data_transport/data_transport.json +1441 -0
  26. data/test/fixtures/cross_agent_tests/data_transport/data_transport.md +35 -0
  27. data/test/fixtures/cross_agent_tests/sql_obfuscation/README.md +7 -2
  28. data/test/fixtures/cross_agent_tests/sql_obfuscation/sql_obfuscation.json +261 -35
  29. data/test/fixtures/cross_agent_tests/transaction_segment_terms.json +305 -17
  30. data/test/multiverse/suites/active_record/active_record_test.rb +1 -1
  31. data/test/multiverse/suites/agent_only/rename_rule_test.rb +12 -12
  32. data/test/multiverse/suites/datamapper/datamapper_test.rb +23 -0
  33. data/test/multiverse/suites/rails/Envfile +10 -2
  34. data/test/new_relic/agent/database/sql_obfuscation_test.rb +2 -7
  35. data/test/performance/README.md +3 -10
  36. data/test/performance/lib/performance/table.rb +1 -1
  37. data/test/performance/suites/rules_engine.rb +35 -0
  38. data/test/performance/suites/segment_terms_rule.rb +27 -0
  39. data/test/performance/suites/sql_obfuscation.rb +19 -0
  40. metadata +9 -2
@@ -327,7 +327,23 @@ module NewRelic
327
327
  end
328
328
 
329
329
  def adapter
330
- config && config[:adapter]
330
+ config && config[:adapter] && symbolized_adapter(config[:adapter].to_s.downcase)
331
+ end
332
+
333
+ POSTGRES_PREFIX = 'postgres'.freeze
334
+ MYSQL_PREFIX = 'mysql'.freeze
335
+ SQLITE_PREFIX = 'sqlite'.freeze
336
+
337
+ def symbolized_adapter(adapter)
338
+ if adapter.start_with? POSTGRES_PREFIX
339
+ :postgres
340
+ elsif adapter.start_with? MYSQL_PREFIX
341
+ :mysql
342
+ elsif adapter.start_with? SQLITE_PREFIX
343
+ :sqlite
344
+ else
345
+ adapter.to_sym
346
+ end
331
347
  end
332
348
  end
333
349
  end
@@ -6,71 +6,91 @@ module NewRelic
6
6
  module Agent
7
7
  module Database
8
8
  module ObfuscationHelpers
9
- # Note that the following two regexes are applied to a reversed version
10
- # of the query. This is why the backslash escape sequences (\' and \")
11
- # appear reversed within them.
12
- #
13
- # Note that some database adapters (notably, PostgreSQL with
14
- # standard_conforming_strings on and MySQL with NO_BACKSLASH_ESCAPES on)
15
- # do not apply special treatment to backslashes within quoted string
16
- # literals. We don't have an easy way of determining whether the
17
- # database connection from which a query was captured was operating in
18
- # one of these modes, but the obfuscation is done in such a way that it
19
- # should not matter.
20
- #
21
- # Reversing the query string before obfuscation allows us to get around
22
- # the fact that a \' appearing within a string may or may not terminate
23
- # the string, because we know that a string cannot *start* with a \'.
24
- REVERSE_SINGLE_QUOTES_REGEX = /'(?:''|'\\|[^'])*'/
25
- REVERSE_ANY_QUOTES_REGEX = /'(?:''|'\\|[^'])*'|"(?:""|"\\|[^"])*"/
9
+ COMPONENTS_REGEX_MAP = {
10
+ :single_quotes => /'(?:[^']|'')*?(?:\\'.*|'(?!'))/,
11
+ :double_quotes => /"(?:[^"]|"")*?(?:\\".*|"(?!"))/,
12
+ :dollar_quotes => /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/,
13
+ :uuids => /\{?(?:[0-9a-fA-F]\-*){32}\}?/,
14
+ :numeric_literals => /\b-?(?:[0-9]+\.)?[0-9]+([eE][+-]?[0-9]+)?\b/,
15
+ :boolean_literals => /\b(?:true|false|null)\b/i,
16
+ :hexadecimal_literals => /0x[0-9a-fA-F]+/,
17
+ :comments => /(?:#|--).*?(?=\r|\n|$)/i,
18
+ :multi_line_comments => /\/\*(?:[^\/]|\/[^*])*?(?:\*\/|\/\*.*)/,
19
+ :oracle_quoted_strings => /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'\<.*?(?:\>'|$)|q'\(.*?(?:\)'|$)/
20
+ }
26
21
 
27
- NUMERICS_REGEX = /\b\d+\b/
28
-
29
- # We take a conservative, overly-aggressive approach to obfuscating
30
- # comments, and drop everything from the query after encountering any
31
- # character sequence that could be a comment initiator. We do this after
32
- # removal of string literals to avoid accidentally over-obfuscating when
33
- # a string literal contains a comment initiator.
34
- SQL_COMMENT_REGEX = Regexp.new('(?:/\*|--|#).*', Regexp::MULTILINE).freeze
22
+ DIALECT_COMPONENTS = {
23
+ :fallback => COMPONENTS_REGEX_MAP.keys,
24
+ :mysql => [:single_quotes, :double_quotes, :numeric_literals, :boolean_literals,
25
+ :hexadecimal_literals, :comments, :multi_line_comments],
26
+ :postgres => [:single_quotes, :dollar_quotes, :uuids, :numeric_literals,
27
+ :boolean_literals, :comments, :multi_line_comments],
28
+ :sqlite => [:single_quotes, :numeric_literals, :boolean_literals, :hexadecimal_literals,
29
+ :comments, :multi_line_comments],
30
+ :oracle => [:single_quotes, :oracle_quoted_strings, :numeric_literals, :comments,
31
+ :multi_line_comments],
32
+ :cassandra => [:single_quotes, :uuids, :numeric_literals, :boolean_literals,
33
+ :hexadecimal_literals, :comments, :multi_line_comments]
34
+ }
35
35
 
36
36
  # We use these to check whether the query contains any quote characters
37
37
  # after obfuscation. If so, that's a good indication that the original
38
- # query was malformed, and so our obfuscation can't reliabily find
38
+ # query was malformed, and so our obfuscation can't reliably find
39
39
  # literals. In such a case, we'll replace the entire query with a
40
40
  # placeholder.
41
- LITERAL_SINGLE_QUOTE = "'".freeze
42
- LITERAL_DOUBLE_QUOTE = '"'.freeze
41
+ CLEANUP_REGEX = {
42
+ :mysql => /'|"|\/\*|\*\//,
43
+ :postgres => /'|\/\*|\*\/|\$(?!\?)/,
44
+ :sqlite => /'|\/\*|\*\/|\$/,
45
+ :cassandra => /'|\/\*|\*\//,
46
+ :oracle => /'|\/\*|\*\//
47
+ }
43
48
 
44
49
  PLACEHOLDER = '?'.freeze
50
+ FAILED_TO_OBFUSCATE_MESSAGE = "Failed to obfuscate SQL query - quote characters remained after obfuscation".freeze
45
51
 
46
52
  def obfuscate_single_quote_literals(sql)
47
- obfuscated = sql.reverse
48
- obfuscated.gsub!(REVERSE_SINGLE_QUOTES_REGEX, PLACEHOLDER)
49
- obfuscated.reverse!
50
- obfuscated
53
+ sql.gsub!(COMPONENTS_REGEX_MAP[:single_quotes], PLACEHOLDER) || sql
51
54
  end
52
55
 
53
- def obfuscate_quoted_literals(sql)
54
- obfuscated = sql.reverse
55
- obfuscated.gsub!(REVERSE_ANY_QUOTES_REGEX, PLACEHOLDER)
56
- obfuscated.reverse!
57
- obfuscated
56
+ def self.generate_regex(dialect)
57
+ components = DIALECT_COMPONENTS[dialect]
58
+ Regexp.union(components.map{|component| COMPONENTS_REGEX_MAP[component]})
58
59
  end
59
60
 
60
- def obfuscate_numeric_literals(sql)
61
- sql.gsub(NUMERICS_REGEX, PLACEHOLDER)
62
- end
61
+ MYSQL_COMPONENTS_REGEX = self.generate_regex(:mysql)
62
+ POSTGRES_COMPONENTS_REGEX = self.generate_regex(:postgres)
63
+ SQLITE_COMPONENTS_REGEX = self.generate_regex(:sqlite)
64
+ ORACLE_COMPONENTS_REGEX = self.generate_regex(:oracle)
65
+ CASSANDRA_COMPONENTS_REGEX = self.generate_regex(:cassandra)
66
+ FALLBACK_REGEX = self.generate_regex(:fallback)
63
67
 
64
- def remove_comments(sql)
65
- sql.gsub(SQL_COMMENT_REGEX, PLACEHOLDER)
66
- end
67
-
68
- def contains_single_quotes?(str)
69
- str.include?(LITERAL_SINGLE_QUOTE)
68
+ def obfuscate(sql, adapter)
69
+ case adapter
70
+ when :mysql
71
+ regex = MYSQL_COMPONENTS_REGEX
72
+ when :postgres
73
+ regex = POSTGRES_COMPONENTS_REGEX
74
+ when :sqlite
75
+ regex = SQLITE_COMPONENTS_REGEX
76
+ when :oracle
77
+ regex = ORACLE_COMPONENTS_REGEX
78
+ when :cassandra
79
+ regex = CASSANDRA_COMPONENTS_REGEX
80
+ else
81
+ regex = FALLBACK_REGEX
82
+ end
83
+ obfuscated = sql.gsub(regex, PLACEHOLDER)
84
+ obfuscated = FAILED_TO_OBFUSCATE_MESSAGE if detect_unmatched_pairs(obfuscated, adapter)
85
+ obfuscated
70
86
  end
71
87
 
72
- def contains_quotes?(str)
73
- str.include?(LITERAL_SINGLE_QUOTE) || str.include?(LITERAL_DOUBLE_QUOTE)
88
+ def detect_unmatched_pairs(obfuscated, adapter)
89
+ if CLEANUP_REGEX[adapter]
90
+ CLEANUP_REGEX[adapter].match(obfuscated)
91
+ else
92
+ CLEANUP_REGEX[:mysql].match(obfuscated)
93
+ end
74
94
  end
75
95
  end
76
96
  end
@@ -13,8 +13,8 @@ module NewRelic
13
13
 
14
14
  attr_reader :obfuscator
15
15
 
16
- QUERY_TOO_LARGE_MESSAGE = "Query too large (over 16k characters) to safely obfuscate"
17
- FAILED_TO_OBFUSCATE_MESSAGE = "Failed to obfuscate SQL query - quote characters remained after obfuscation"
16
+ QUERY_TOO_LARGE_MESSAGE = "Query too large (over 16k characters) to safely obfuscate".freeze
17
+ ELLIPSIS = "...".freeze
18
18
 
19
19
  def initialize
20
20
  reset
@@ -50,30 +50,11 @@ module NewRelic
50
50
  def default_sql_obfuscator(sql)
51
51
  stmt = sql.kind_of?(Statement) ? sql : Statement.new(sql)
52
52
 
53
- if stmt.sql[-3,3] == '...'
53
+ if stmt.sql.end_with? ELLIPSIS
54
54
  return QUERY_TOO_LARGE_MESSAGE
55
55
  end
56
56
 
57
- obfuscate_double_quotes = stmt.adapter.to_s !~ /postgres|sqlite/
58
-
59
- obfuscated = obfuscate_numeric_literals(stmt.sql)
60
-
61
- if obfuscate_double_quotes
62
- obfuscated = obfuscate_quoted_literals(obfuscated)
63
- obfuscated = remove_comments(obfuscated)
64
- if contains_quotes?(obfuscated)
65
- obfuscated = FAILED_TO_OBFUSCATE_MESSAGE
66
- end
67
- else
68
- obfuscated = obfuscate_single_quote_literals(obfuscated)
69
- obfuscated = remove_comments(obfuscated)
70
- if contains_single_quotes?(obfuscated)
71
- obfuscated = FAILED_TO_OBFUSCATE_MESSAGE
72
- end
73
- end
74
-
75
-
76
- obfuscated.to_s # return back to a regular String
57
+ obfuscate(stmt.sql, stmt.adapter).to_s
77
58
  end
78
59
  end
79
60
  end
@@ -12,7 +12,7 @@ module NewRelic
12
12
 
13
13
  # Note that this regex can't be shared with the ones in the
14
14
  # Database::Obfuscator class because here we don't look for
15
- # backslash-escaped strings (and those regexes are backwards).
15
+ # backslash-escaped strings.
16
16
  QUOTED_STRINGS_REGEX = /'(?:[^']|'')*'|"(?:[^"]|"")*"/
17
17
  LABEL_LINE_REGEX = /^([^:\n]*:\s+).*$/.freeze
18
18
 
@@ -109,6 +109,9 @@ module NewRelic
109
109
  end
110
110
 
111
111
  DATA_MAPPER = "DataMapper".freeze
112
+ PASSWORD_REGEX = /&password=.*?&/
113
+ AMPERSAND = '&'.freeze
114
+ PASSWORD_PARAM = '&password='.freeze
112
115
 
113
116
  def self.method_body(clazz, method_name, operation_only)
114
117
  use_model_name = NewRelic::Helper.instance_methods_include?(clazz, :model)
@@ -136,7 +139,23 @@ module NewRelic
136
139
  name)
137
140
 
138
141
  NewRelic::Agent::MethodTracer.trace_execution_scoped(metrics) do
139
- self.send("#{method_name}_without_newrelic", *args, &blk)
142
+ begin
143
+ self.send("#{method_name}_without_newrelic", *args, &blk)
144
+ rescue ::DataObjects::SQLError => e
145
+ e.uri.gsub!(PASSWORD_REGEX, AMPERSAND) if e.uri.include?(PASSWORD_PARAM)
146
+
147
+ strategy = NewRelic::Agent::Database.record_sql_method(:slow_sql)
148
+ case strategy
149
+ when :obfuscated
150
+ statement = NewRelic::Agent::Database::Statement.new(e.query, :adapter => self.options[:adapter])
151
+ obfuscated_sql = NewRelic::Agent::Database.obfuscate_sql(statement)
152
+ e.instance_variable_set(:@query, obfuscated_sql)
153
+ when :off
154
+ e.instance_variable_set(:@query, nil)
155
+ end
156
+
157
+ raise
158
+ end
140
159
  end
141
160
  end
142
161
  end
@@ -64,7 +64,7 @@ module NewRelic
64
64
  # with a couple minor additions so we don't have a hard
65
65
  # dependency on ActiveSupport::Notifications.
66
66
  #
67
- # Represents an intrumentation event, provides timing and metric
67
+ # Represents an instrumentation event, provides timing and metric
68
68
  # name information useful when recording metrics.
69
69
  class Event
70
70
  attr_reader :name, :time, :transaction_id, :payload, :children
@@ -4,6 +4,7 @@
4
4
 
5
5
  require 'new_relic/agent/rules_engine/replacement_rule'
6
6
  require 'new_relic/agent/rules_engine/segment_terms_rule'
7
+ require 'new_relic/language_support'
7
8
 
8
9
  module NewRelic
9
10
  module Agent
@@ -26,12 +27,48 @@ module NewRelic
26
27
  txn_name_specs = connect_response['transaction_name_rules'] || []
27
28
  segment_rule_specs = connect_response['transaction_segment_terms'] || []
28
29
 
29
- txn_name_rules = txn_name_specs.map { |s| ReplacementRule.new(s) }
30
- segment_rules = segment_rule_specs.map { |s| SegmentTermsRule.new(s) }
30
+ txn_name_rules = txn_name_specs.map { |s| ReplacementRule.new(s) }
31
+
32
+ segment_rules = []
33
+
34
+ segment_rule_specs.each do |spec|
35
+ if spec[SegmentTermsRule::PREFIX_KEY] && SegmentTermsRule.valid?(spec)
36
+ # Build segment_rules in reverse order from which they're provided,
37
+ # so that when we eliminate duplicates with #uniq!, we retain the last
38
+ # instances of repeated rules.
39
+ segment_rules.unshift SegmentTermsRule.new(spec)
40
+ end
41
+ end
42
+
43
+ reject_rules_with_duplicate_prefixes!(segment_rules)
44
+
45
+ segment_rules.reverse! # Reset the rules to their original order.
31
46
 
32
47
  self.new(txn_name_rules, segment_rules)
33
48
  end
34
49
 
50
+ # When multiple rules share the same prefix,
51
+ # only apply the rule with the last instance of the prefix.
52
+ # Note that the incoming rules are in reverse order to facilitate this.
53
+ if NewRelic::LanguageSupport.uniq_accepts_block?
54
+ def self.reject_rules_with_duplicate_prefixes!(rules)
55
+ rules.uniq! { |rule| rule.prefix }
56
+ end
57
+ else
58
+ def self.reject_rules_with_duplicate_prefixes!(rules)
59
+ unique_rules = {}
60
+
61
+ rules.reject! do |rule|
62
+ if unique_rules[rule.prefix]
63
+ true
64
+ else
65
+ unique_rules[rule.prefix] = rule
66
+ false
67
+ end
68
+ end
69
+ end
70
+ end
71
+
35
72
  def initialize(rules=[], segment_term_rules=[])
36
73
  @rules = rules.sort
37
74
  @segment_term_rules = segment_term_rules
@@ -6,15 +6,32 @@ module NewRelic
6
6
  module Agent
7
7
  class RulesEngine
8
8
  class SegmentTermsRule
9
+ PREFIX_KEY = 'prefix'.freeze
10
+ TERMS_KEY = 'terms'.freeze
9
11
  SEGMENT_PLACEHOLDER = '*'.freeze
10
12
  ADJACENT_PLACEHOLDERS_REGEX = %r{((?:^|/)\*)(?:/\*)*}.freeze
11
13
  ADJACENT_PLACEHOLDERS_REPLACEMENT = '\1'.freeze
14
+ VALID_PREFIX_SEGMENT_COUNT = 2
12
15
 
13
16
  attr_reader :prefix, :terms
14
17
 
18
+ def self.valid?(rule_spec)
19
+ rule_spec[PREFIX_KEY].kind_of?(String) &&
20
+ rule_spec[TERMS_KEY].kind_of?(Array) &&
21
+ valid_prefix_segment_count?(rule_spec[PREFIX_KEY])
22
+ end
23
+
24
+ def self.valid_prefix_segment_count?(prefix)
25
+ count = prefix.count(SEGMENT_SEPARATOR)
26
+ rindex = prefix.rindex(SEGMENT_SEPARATOR)
27
+
28
+ (count == 2 && prefix[rindex + 1].nil?) ||
29
+ (count == 1 && !prefix[rindex + 1].nil?)
30
+ end
31
+
15
32
  def initialize(options)
16
- @prefix = options['prefix']
17
- @terms = options['terms']
33
+ @prefix = options[PREFIX_KEY]
34
+ @terms = options[TERMS_KEY]
18
35
  @trim_range = (@prefix.size..-1)
19
36
  end
20
37
 
@@ -23,14 +40,19 @@ module NewRelic
23
40
  end
24
41
 
25
42
  def matches?(string)
26
- string.start_with?(@prefix)
43
+ string.start_with?(@prefix) &&
44
+ (prefix_matches_on_segment_boundary?(string) || string.size == @prefix.size)
45
+ end
46
+
47
+ def prefix_matches_on_segment_boundary?(string)
48
+ string.size > @prefix.size &&
49
+ string[@prefix.chomp(SEGMENT_SEPARATOR).size].chr == SEGMENT_SEPARATOR
27
50
  end
28
51
 
29
52
  def apply(string)
30
53
  rest = string[@trim_range]
31
54
  leading_slash = rest.slice!(LEADING_SLASH_REGEX)
32
-
33
- segments = rest.split(SEGMENT_SEPARATOR)
55
+ segments = rest.split(SEGMENT_SEPARATOR, -1)
34
56
  segments.map! { |s| @terms.include?(s) ? s : SEGMENT_PLACEHOLDER }
35
57
  transformed_suffix = collapse_adjacent_placeholder_segments(segments)
36
58
 
@@ -313,10 +313,14 @@ module NewRelic
313
313
 
314
314
  private
315
315
 
316
+ # need to hash the same way in every process, to be able to aggregate slow SQL traces
316
317
  def consistent_hash(string)
317
- # need to hash the same way in every process
318
- Digest::MD5.hexdigest(string).hex \
319
- .modulo(2**31-1) # ensure sql_id fits in an INT(11)
318
+ if NewRelic::Agent.config[:'slow_sql.use_longer_sql_id']
319
+ Digest::MD5.hexdigest(string).hex.modulo(2**63-1)
320
+ else
321
+ # from when sql_id needed to fit in an INT(11)
322
+ Digest::MD5.hexdigest(string).hex.modulo(2**31-1)
323
+ end
320
324
  end
321
325
  end
322
326
  end
@@ -5,6 +5,8 @@
5
5
  module NewRelic::LanguageSupport
6
6
  extend self
7
7
 
8
+ RUBY_VERSION_192 = '1.9.2'.freeze
9
+
8
10
  # need to use syck rather than psych when possible
9
11
  def needs_syck?
10
12
  !NewRelic::LanguageSupport.using_engine?('jruby') &&
@@ -90,6 +92,12 @@ module NewRelic::LanguageSupport
90
92
  end
91
93
  end
92
94
 
95
+ if ::RUBY_VERSION >= RUBY_VERSION_192
96
+ def uniq_accepts_block?; true; end
97
+ else
98
+ def uniq_accepts_block?; false; end
99
+ end
100
+
93
101
  def jruby?
94
102
  defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
95
103
  end
@@ -12,7 +12,7 @@ module NewRelic
12
12
 
13
13
  MAJOR = 3
14
14
  MINOR = 14
15
- TINY = 2
15
+ TINY = 3
16
16
 
17
17
  begin
18
18
  require File.join(File.dirname(__FILE__), 'build')
@@ -6,7 +6,7 @@
6
6
  <dl class="clamshell-list">
7
7
  <% configs.each do |config|%>
8
8
  <!-- ********** <%= config[:key] %> ********** -->
9
- <dt id="<%=config[:key]%>"><%=config[:key]%></dt>
9
+ <dt id="<%= config[:key].gsub('.', '-') %>"><%=config[:key]%></dt>
10
10
  <dd>
11
11
  <table class="specs">
12
12
  <tbody>
@@ -18,6 +18,10 @@
18
18
  <th>Default</th>
19
19
  <td><%=config[:default]%></td>
20
20
  </tr>
21
+ <tr>
22
+ <th>Environ variable</th>
23
+ <td><code><%=config[:env_var]%></code></td>
24
+ </tr>
21
25
  </tbody>
22
26
  </table>
23
27