newrelic_rpm 3.1.2 → 3.2.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of newrelic_rpm might be problematic. Click here for more details.
- data/CHANGELOG +9 -0
- data/lib/new_relic/agent.rb +12 -3
- data/lib/new_relic/agent/agent.rb +99 -97
- data/lib/new_relic/agent/database.rb +203 -0
- data/lib/new_relic/agent/instrumentation/active_merchant.rb +2 -0
- data/lib/new_relic/agent/instrumentation/acts_as_solr.rb +2 -0
- data/lib/new_relic/agent/instrumentation/authlogic.rb +2 -0
- data/lib/new_relic/agent/instrumentation/data_mapper.rb +2 -0
- data/lib/new_relic/agent/instrumentation/delayed_job_instrumentation.rb +2 -0
- data/lib/new_relic/agent/instrumentation/memcache.rb +23 -13
- data/lib/new_relic/agent/instrumentation/merb/controller.rb +2 -1
- data/lib/new_relic/agent/instrumentation/merb/errors.rb +2 -0
- data/lib/new_relic/agent/instrumentation/metric_frame.rb +7 -1
- data/lib/new_relic/agent/instrumentation/metric_frame/pop.rb +1 -0
- data/lib/new_relic/agent/instrumentation/net.rb +2 -0
- data/lib/new_relic/agent/instrumentation/passenger_instrumentation.rb +2 -0
- data/lib/new_relic/agent/instrumentation/rails/action_controller.rb +63 -36
- data/lib/new_relic/agent/instrumentation/rails/action_web_service.rb +2 -0
- data/lib/new_relic/agent/instrumentation/rails/active_record_instrumentation.rb +5 -2
- data/lib/new_relic/agent/instrumentation/rails/errors.rb +4 -2
- data/lib/new_relic/agent/instrumentation/rails3/action_controller.rb +56 -2
- data/lib/new_relic/agent/instrumentation/rails3/active_record_instrumentation.rb +5 -2
- data/lib/new_relic/agent/instrumentation/rails3/errors.rb +3 -1
- data/lib/new_relic/agent/instrumentation/sunspot.rb +2 -0
- data/lib/new_relic/agent/instrumentation/unicorn_instrumentation.rb +2 -1
- data/lib/new_relic/agent/shim_agent.rb +1 -0
- data/lib/new_relic/agent/sql_sampler.rb +230 -0
- data/lib/new_relic/agent/stats_engine/transactions.rb +10 -0
- data/lib/new_relic/agent/transaction_sampler.rb +11 -6
- data/lib/new_relic/collection_helper.rb +7 -4
- data/lib/new_relic/commands/deployments.rb +1 -1
- data/lib/new_relic/commands/install.rb +2 -13
- data/lib/new_relic/control/class_methods.rb +4 -3
- data/lib/new_relic/control/configuration.rb +21 -0
- data/lib/new_relic/control/frameworks/rails.rb +1 -1
- data/lib/new_relic/control/logging_methods.rb +17 -6
- data/lib/new_relic/delayed_job_injection.rb +1 -1
- data/lib/new_relic/local_environment.rb +8 -14
- data/lib/new_relic/rack/developer_mode.rb +1 -0
- data/lib/new_relic/stats.rb +1 -0
- data/lib/new_relic/transaction_sample.rb +5 -60
- data/lib/new_relic/transaction_sample/segment.rb +7 -82
- data/lib/new_relic/version.rb +3 -3
- data/newrelic_rpm.gemspec +8 -3
- data/test/new_relic/agent/agent/connect_test.rb +95 -0
- data/test/new_relic/agent/agent/start_test.rb +0 -85
- data/test/new_relic/agent/agent/start_worker_thread_test.rb +1 -0
- data/test/new_relic/agent/agent_test.rb +0 -73
- data/test/new_relic/agent/browser_monitoring_test.rb +1 -1
- data/test/new_relic/agent/database_test.rb +160 -0
- data/test/new_relic/agent/instrumentation/metric_frame/pop_test.rb +3 -0
- data/test/new_relic/agent/memcache_instrumentation_test.rb +14 -15
- data/test/new_relic/agent/sql_sampler_test.rb +135 -0
- data/test/new_relic/agent/transaction_sampler_test.rb +12 -3
- data/test/new_relic/collection_helper_test.rb +4 -4
- data/test/new_relic/control/configuration_test.rb +31 -0
- data/test/new_relic/control/logging_methods_test.rb +20 -4
- data/test/new_relic/delayed_job_injection_test.rb +1 -1
- data/test/new_relic/rack/developer_mode_helper_test.rb +141 -0
- data/test/new_relic/stats_test.rb +3 -3
- data/test/new_relic/transaction_sample/segment_test.rb +4 -92
- data/test/new_relic/transaction_sample_test.rb +1 -1
- data/test/test_helper.rb +1 -1
- data/ui/helpers/developer_mode_helper.rb +14 -8
- data/ui/helpers/google_pie_chart.rb +0 -1
- data/vendor/gems/dependency_detection-0.0.1.build/lib/dependency_detection.rb +5 -0
- data/vendor/gems/metric_parser-0.1.0.pre1/lib/new_relic/metric_parser/mem_cache.rb +11 -11
- data/vendor/gems/metric_parser-0.1.0.pre1/lib/new_relic/metric_parser/view.rb +4 -0
- metadata +15 -10
data/CHANGELOG
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
v3.2.0
|
2
|
+
* Fix over-detection of mongrel and unicorn and only start the agent when
|
3
|
+
actual server is running
|
4
|
+
* Improve developer mode backtraces to support ruby 1.9.2, windows, and fix
|
5
|
+
nil de-reference in developer mode
|
6
|
+
* Fixed some cases where Memcache instrumentation was failing to load
|
7
|
+
* Added View instrumentation back for Rails 3.1
|
8
|
+
* Ability to set log destination by NEW_RELIC_LOG env var
|
9
|
+
|
1
10
|
v3.1.2
|
2
11
|
* Fixed some thread safety issues
|
3
12
|
* Work around for Ruby 1.8.7 Marshal crash bug
|
data/lib/new_relic/agent.rb
CHANGED
@@ -81,9 +81,11 @@ module NewRelic
|
|
81
81
|
require 'new_relic/agent/worker_loop'
|
82
82
|
require 'new_relic/agent/stats_engine'
|
83
83
|
require 'new_relic/agent/transaction_sampler'
|
84
|
+
require 'new_relic/agent/sql_sampler'
|
84
85
|
require 'new_relic/agent/error_collector'
|
85
86
|
require 'new_relic/agent/busy_calculator'
|
86
87
|
require 'new_relic/agent/sampler'
|
88
|
+
require 'new_relic/agent/database'
|
87
89
|
|
88
90
|
require 'new_relic/agent/instrumentation/controller_instrumentation'
|
89
91
|
|
@@ -146,9 +148,16 @@ module NewRelic
|
|
146
148
|
alias get_stats_no_scope get_stats
|
147
149
|
|
148
150
|
# Get the logger for the agent. Available after the agent has initialized.
|
149
|
-
# This sends output to the agent log file.
|
151
|
+
# This sends output to the agent log file. If the agent has not initialized
|
152
|
+
# a standard output logger is returned.
|
150
153
|
def logger
|
151
|
-
NewRelic::Control.instance
|
154
|
+
control = NewRelic::Control.instance(false)
|
155
|
+
if control
|
156
|
+
control.log
|
157
|
+
else
|
158
|
+
require 'logger'
|
159
|
+
@stdoutlog ||= Logger.new $stdout
|
160
|
+
end
|
152
161
|
end
|
153
162
|
|
154
163
|
# Call this to manually start the Agent in situations where the Agent does
|
@@ -272,7 +281,7 @@ module NewRelic
|
|
272
281
|
# end
|
273
282
|
#
|
274
283
|
def set_sql_obfuscator(type = :replace, &block)
|
275
|
-
|
284
|
+
NewRelic::Agent::Database.set_sql_obfuscator(type, &block)
|
276
285
|
end
|
277
286
|
|
278
287
|
|
@@ -35,14 +35,16 @@ module NewRelic
|
|
35
35
|
@metric_ids = {}
|
36
36
|
@stats_engine = NewRelic::Agent::StatsEngine.new
|
37
37
|
@transaction_sampler = NewRelic::Agent::TransactionSampler.new
|
38
|
+
@sql_sampler = NewRelic::Agent::SqlSampler.new
|
38
39
|
@stats_engine.transaction_sampler = @transaction_sampler
|
40
|
+
@stats_engine.sql_sampler = @sql_sampler
|
39
41
|
@error_collector = NewRelic::Agent::ErrorCollector.new
|
40
42
|
@connect_attempts = 0
|
41
43
|
|
42
44
|
@request_timeout = NewRelic::Control.instance.fetch('timeout', 2 * 60)
|
43
45
|
|
44
46
|
@last_harvest_time = Time.now
|
45
|
-
@obfuscator =
|
47
|
+
@obfuscator = lambda {|sql| NewRelic::Agent::Database.default_sql_obfuscator(sql) }
|
46
48
|
end
|
47
49
|
|
48
50
|
# contains all the class-level methods for NewRelic::Agent::Agent
|
@@ -64,6 +66,7 @@ module NewRelic
|
|
64
66
|
attr_reader :stats_engine
|
65
67
|
# the transaction sampler that handles recording transactions
|
66
68
|
attr_reader :transaction_sampler
|
69
|
+
attr_reader :sql_sampler
|
67
70
|
# error collector is a simple collection of recorded errors
|
68
71
|
attr_reader :error_collector
|
69
72
|
# whether we should record raw, obfuscated, or no sql
|
@@ -271,29 +274,6 @@ module NewRelic
|
|
271
274
|
Thread.current[:newrelic_untraced].pop if Thread.current[:newrelic_untraced]
|
272
275
|
end
|
273
276
|
|
274
|
-
# Sets the sql obfuscator used to clean up sql when sending it
|
275
|
-
# to the server. Possible types are:
|
276
|
-
#
|
277
|
-
# :before => sets the block to run before the existing
|
278
|
-
# obfuscators
|
279
|
-
#
|
280
|
-
# :after => sets the block to run after the existing
|
281
|
-
# obfuscator(s)
|
282
|
-
#
|
283
|
-
# :replace => removes the current obfuscator and replaces it
|
284
|
-
# with the provided block
|
285
|
-
def set_sql_obfuscator(type, &block)
|
286
|
-
if type == :before
|
287
|
-
@obfuscator = NewRelic::ChainedCall.new(block, @obfuscator)
|
288
|
-
elsif type == :after
|
289
|
-
@obfuscator = NewRelic::ChainedCall.new(@obfuscator, block)
|
290
|
-
elsif type == :replace
|
291
|
-
@obfuscator = block
|
292
|
-
else
|
293
|
-
fail "unknown sql_obfuscator type #{type}"
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
277
|
# Shorthand to the NewRelic::Agent.logger method
|
298
278
|
def log
|
299
279
|
NewRelic::Agent.logger
|
@@ -331,68 +311,6 @@ module NewRelic
|
|
331
311
|
log.info "Application: #{control.app_names.join(", ")}"
|
332
312
|
end
|
333
313
|
|
334
|
-
# apdex_f is always 4 times the apdex_t
|
335
|
-
def apdex_f
|
336
|
-
(4 * NewRelic::Control.instance.apdex_t).to_f
|
337
|
-
end
|
338
|
-
|
339
|
-
# If the transaction threshold is set to the string
|
340
|
-
# 'apdex_f', we use 4 times the apdex_t value to record
|
341
|
-
# transactions. This gears well with using apdex since you
|
342
|
-
# will attempt to send any transactions that register as 'failing'
|
343
|
-
def apdex_f_threshold?
|
344
|
-
sampler_config.fetch('transaction_threshold', '') =~ /apdex_f/i
|
345
|
-
end
|
346
|
-
|
347
|
-
# Sets the sql recording configuration by trying to detect
|
348
|
-
# any attempt to disable the sql collection - 'off',
|
349
|
-
# 'false', 'none', and friends. Otherwise, we accept 'raw',
|
350
|
-
# and unrecognized values default to 'obfuscated'
|
351
|
-
def set_sql_recording!
|
352
|
-
record_sql_config = sampler_config.fetch('record_sql', :obfuscated)
|
353
|
-
case record_sql_config.to_s
|
354
|
-
when 'off'
|
355
|
-
@record_sql = :off
|
356
|
-
when 'none'
|
357
|
-
@record_sql = :off
|
358
|
-
when 'false'
|
359
|
-
@record_sql = :off
|
360
|
-
when 'raw'
|
361
|
-
@record_sql = :raw
|
362
|
-
else
|
363
|
-
@record_sql = :obfuscated
|
364
|
-
end
|
365
|
-
|
366
|
-
log_sql_transmission_warning?
|
367
|
-
end
|
368
|
-
|
369
|
-
# Warn the user when we are sending raw sql across the wire
|
370
|
-
# - they should probably be using ssl when this is true
|
371
|
-
def log_sql_transmission_warning?
|
372
|
-
log_if((@record_sql == :raw), :warn, "Agent is configured to send raw SQL to the service")
|
373
|
-
end
|
374
|
-
|
375
|
-
# gets the sampler configuration from the control object's settings
|
376
|
-
def sampler_config
|
377
|
-
control.fetch('transaction_tracer', {})
|
378
|
-
end
|
379
|
-
|
380
|
-
# this entire method should be done on the transaction
|
381
|
-
# sampler object, rather than here. We should pass in the
|
382
|
-
# sampler config.
|
383
|
-
def config_transaction_tracer
|
384
|
-
@should_send_samples = @config_should_send_samples = sampler_config.fetch('enabled', true)
|
385
|
-
@should_send_random_samples = sampler_config.fetch('random_sample', false)
|
386
|
-
@explain_threshold = sampler_config.fetch('explain_threshold', 0.5).to_f
|
387
|
-
@explain_enabled = sampler_config.fetch('explain_enabled', true)
|
388
|
-
set_sql_recording!
|
389
|
-
|
390
|
-
# default to 2.0, string 'apdex_f' will turn into your
|
391
|
-
# apdex * 4
|
392
|
-
@slowest_transaction_threshold = sampler_config.fetch('transaction_threshold', 2.0).to_f
|
393
|
-
@slowest_transaction_threshold = apdex_f if apdex_f_threshold?
|
394
|
-
end
|
395
|
-
|
396
314
|
# Connecting in the foreground blocks further startup of the
|
397
315
|
# agent until we have a connection - useful in cases where
|
398
316
|
# you're trying to log a very-short-running process and want
|
@@ -548,6 +466,16 @@ module NewRelic
|
|
548
466
|
end
|
549
467
|
end
|
550
468
|
|
469
|
+
def check_sql_sampler_status
|
470
|
+
# disable sql sampling if disabled by the server
|
471
|
+
# and we're not in dev mode
|
472
|
+
if control.developer_mode? || @should_send_samples
|
473
|
+
@sql_sampler.enable
|
474
|
+
else
|
475
|
+
@sql_sampler.disable
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
551
479
|
# logs info about the worker loop so users can see when the
|
552
480
|
# agent actually begins running in the background
|
553
481
|
def log_worker_loop_start
|
@@ -633,6 +561,7 @@ module NewRelic
|
|
633
561
|
connect(connection_options)
|
634
562
|
if @connected
|
635
563
|
check_transaction_sampler_status
|
564
|
+
check_sql_sampler_status
|
636
565
|
log_worker_loop_start
|
637
566
|
create_and_run_worker_loop
|
638
567
|
# never reaches here unless there is a problem or
|
@@ -796,6 +725,8 @@ module NewRelic
|
|
796
725
|
# are allowed to send errors. Pretty simple, and logs at
|
797
726
|
# debug whether errors will or will not be sent.
|
798
727
|
def configure_error_collector!(server_enabled)
|
728
|
+
# Reinitialize the error collector
|
729
|
+
@error_collector = NewRelic::Agent::ErrorCollector.new
|
799
730
|
# Ask for permission to collect error data
|
800
731
|
enabled = if error_collector.config_enabled && server_enabled
|
801
732
|
error_collector.enabled = true
|
@@ -820,6 +751,24 @@ module NewRelic
|
|
820
751
|
log.info "Transaction sampling enabled, rate = #{@transaction_sampler.sampling_rate}"
|
821
752
|
end
|
822
753
|
|
754
|
+
# this entire method should be done on the transaction
|
755
|
+
# sampler object, rather than here. We should pass in the
|
756
|
+
# sampler config.
|
757
|
+
def config_transaction_tracer
|
758
|
+
# Reconfigure the transaction tracer
|
759
|
+
@transaction_sampler.configure!
|
760
|
+
@should_send_samples = @config_should_send_samples = sampler_config.fetch('enabled', true)
|
761
|
+
@should_send_random_samples = sampler_config.fetch('random_sample', false)
|
762
|
+
@explain_threshold = sampler_config.fetch('explain_threshold', 0.5).to_f
|
763
|
+
@explain_enabled = sampler_config.fetch('explain_enabled', true)
|
764
|
+
set_sql_recording!
|
765
|
+
|
766
|
+
# default to 2.0, string 'apdex_f' will turn into your
|
767
|
+
# apdex * 4
|
768
|
+
@slowest_transaction_threshold = sampler_config.fetch('transaction_threshold', 2.0).to_f
|
769
|
+
@slowest_transaction_threshold = apdex_f if apdex_f_threshold?
|
770
|
+
end
|
771
|
+
|
823
772
|
# Enables or disables the transaction tracer and sets its
|
824
773
|
# options based on the options provided to the
|
825
774
|
# method.
|
@@ -837,6 +786,52 @@ module NewRelic
|
|
837
786
|
end
|
838
787
|
end
|
839
788
|
|
789
|
+
# apdex_f is always 4 times the apdex_t
|
790
|
+
def apdex_f
|
791
|
+
(4 * NewRelic::Control.instance.apdex_t).to_f
|
792
|
+
end
|
793
|
+
|
794
|
+
# If the transaction threshold is set to the string
|
795
|
+
# 'apdex_f', we use 4 times the apdex_t value to record
|
796
|
+
# transactions. This gears well with using apdex since you
|
797
|
+
# will attempt to send any transactions that register as 'failing'
|
798
|
+
def apdex_f_threshold?
|
799
|
+
sampler_config.fetch('transaction_threshold', '') =~ /apdex_f/i
|
800
|
+
end
|
801
|
+
|
802
|
+
# Sets the sql recording configuration by trying to detect
|
803
|
+
# any attempt to disable the sql collection - 'off',
|
804
|
+
# 'false', 'none', and friends. Otherwise, we accept 'raw',
|
805
|
+
# and unrecognized values default to 'obfuscated'
|
806
|
+
def set_sql_recording!
|
807
|
+
record_sql_config = sampler_config.fetch('record_sql', :obfuscated)
|
808
|
+
case record_sql_config.to_s
|
809
|
+
when 'off'
|
810
|
+
@record_sql = :off
|
811
|
+
when 'none'
|
812
|
+
@record_sql = :off
|
813
|
+
when 'false'
|
814
|
+
@record_sql = :off
|
815
|
+
when 'raw'
|
816
|
+
@record_sql = :raw
|
817
|
+
else
|
818
|
+
@record_sql = :obfuscated
|
819
|
+
end
|
820
|
+
|
821
|
+
log_sql_transmission_warning?
|
822
|
+
end
|
823
|
+
|
824
|
+
# Warn the user when we are sending raw sql across the wire
|
825
|
+
# - they should probably be using ssl when this is true
|
826
|
+
def log_sql_transmission_warning?
|
827
|
+
log.warn("Agent is configured to send raw SQL to the service") if @record_sql == :raw
|
828
|
+
end
|
829
|
+
|
830
|
+
# gets the sampler configuration from the control object's settings
|
831
|
+
def sampler_config
|
832
|
+
control.fetch('transaction_tracer', {})
|
833
|
+
end
|
834
|
+
|
840
835
|
# Asks the collector to tell us which sub-collector we
|
841
836
|
# should be reporting to, and then does the name resolution
|
842
837
|
# on that host so we don't block on DNS during the normal
|
@@ -868,7 +863,10 @@ module NewRelic
|
|
868
863
|
@report_period = config_data['data_report_period']
|
869
864
|
@url_rules = config_data['url_rules']
|
870
865
|
@beacon_configuration = BeaconConfiguration.new(config_data)
|
866
|
+
@server_side_config_enabled = config_data['listen_to_server_config']
|
871
867
|
|
868
|
+
control.merge_server_side_config(config_data) if @server_side_config_enabled
|
869
|
+
config_transaction_tracer
|
872
870
|
log_connection!(config_data)
|
873
871
|
configure_transaction_tracer!(config_data['collect_traces'], config_data['sample_rate'])
|
874
872
|
configure_error_collector!(config_data['collect_errors'])
|
@@ -1043,6 +1041,20 @@ module NewRelic
|
|
1043
1041
|
@traces
|
1044
1042
|
end
|
1045
1043
|
|
1044
|
+
def harvest_and_send_slowest_sql
|
1045
|
+
# FIXME add the code to try to resend if our connection is down
|
1046
|
+
sql_traces = @sql_sampler.harvest
|
1047
|
+
unless sql_traces.empty?
|
1048
|
+
log.debug "Sending (#{sql_traces.count}) sql traces"
|
1049
|
+
begin
|
1050
|
+
response = invoke_remote :sql_trace_data, sql_traces
|
1051
|
+
# log.debug "Sql trace response: #{response}"
|
1052
|
+
rescue
|
1053
|
+
@sql_sampler.merge sql_traces
|
1054
|
+
end
|
1055
|
+
end
|
1056
|
+
end
|
1057
|
+
|
1046
1058
|
# This handles getting the transaction traces and then sending
|
1047
1059
|
# them across the wire. This includes gathering SQL
|
1048
1060
|
# explanations, stripping out stack traces, and normalizing
|
@@ -1257,6 +1269,7 @@ module NewRelic
|
|
1257
1269
|
NewRelic::Agent.load_data
|
1258
1270
|
harvest_and_send_errors
|
1259
1271
|
harvest_and_send_slowest_sample
|
1272
|
+
harvest_and_send_slowest_sql
|
1260
1273
|
harvest_and_send_timeslice_data
|
1261
1274
|
else
|
1262
1275
|
log.debug "Serializing agent data to disk"
|
@@ -1289,17 +1302,6 @@ module NewRelic
|
|
1289
1302
|
log.debug "Bypassing graceful disconnect - agent not connected"
|
1290
1303
|
end
|
1291
1304
|
end
|
1292
|
-
def default_sql_obfuscator(sql)
|
1293
|
-
sql = sql.dup
|
1294
|
-
# This is hardly readable. Use the unit tests.
|
1295
|
-
# remove single quoted strings:
|
1296
|
-
sql.gsub!(/'(.*?[^\\'])??'(?!')/, '?')
|
1297
|
-
# remove double quoted strings:
|
1298
|
-
sql.gsub!(/"(.*?[^\\"])??"(?!")/, '?')
|
1299
|
-
# replace all number literals
|
1300
|
-
sql.gsub!(/\d+/, "?")
|
1301
|
-
sql
|
1302
|
-
end
|
1303
1305
|
end
|
1304
1306
|
|
1305
1307
|
extend ClassMethods
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module NewRelic
|
4
|
+
# columns for a mysql explain plan
|
5
|
+
MYSQL_EXPLAIN_COLUMNS = [
|
6
|
+
"Id",
|
7
|
+
"Select Type",
|
8
|
+
"Table",
|
9
|
+
"Type",
|
10
|
+
"Possible Keys",
|
11
|
+
"Key",
|
12
|
+
"Key Length",
|
13
|
+
"Ref",
|
14
|
+
"Rows",
|
15
|
+
"Extra"
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
module Agent
|
19
|
+
module Database
|
20
|
+
extend self
|
21
|
+
|
22
|
+
def obfuscate_sql(sql)
|
23
|
+
Obfuscator.instance.obfuscator.call(sql)
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_sql_obfuscator(type, &block)
|
27
|
+
Obfuscator.instance.set_sql_obfuscator(type, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_connection(config)
|
31
|
+
ConnectionManager.instance.get_connection(config)
|
32
|
+
end
|
33
|
+
|
34
|
+
def close_connections
|
35
|
+
ConnectionManager.instance.close_connections
|
36
|
+
end
|
37
|
+
|
38
|
+
# Perform this in the runtime environment of a managed
|
39
|
+
# application, to explain the sql statement executed within a
|
40
|
+
# segment of a transaction sample. Returns an array of
|
41
|
+
# explanations (which is an array rows consisting of an array of
|
42
|
+
# strings for each column returned by the the explain query)
|
43
|
+
# Note this happens only for statements whose execution time
|
44
|
+
# exceeds a threshold (e.g. 500ms) and only within the slowest
|
45
|
+
# transaction in a report period, selected for shipment to New
|
46
|
+
# Relic
|
47
|
+
def explain_sql(sql, connection_config)
|
48
|
+
return nil unless sql && connection_config
|
49
|
+
statement = sql.split(";\n")[0] # only explain the first
|
50
|
+
explain_sql = explain_statement(statement, connection_config)
|
51
|
+
return explain_sql || []
|
52
|
+
end
|
53
|
+
|
54
|
+
def explain_statement(statement, config)
|
55
|
+
if is_select?(statement)
|
56
|
+
handle_exception_in_explain do
|
57
|
+
connection = get_connection(config)
|
58
|
+
plan = nil
|
59
|
+
if connection
|
60
|
+
plan = process_resultset(connection.execute("EXPLAIN #{statement}"))
|
61
|
+
end
|
62
|
+
return plan
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def process_resultset(items)
|
68
|
+
# The resultset type varies for different drivers. Only thing you can count on is
|
69
|
+
# that it implements each. Also: can't use select_rows because the native postgres
|
70
|
+
# driver doesn't know that method.
|
71
|
+
|
72
|
+
headers = []
|
73
|
+
values = []
|
74
|
+
if items.respond_to?(:each_hash)
|
75
|
+
items.each_hash do |row|
|
76
|
+
headers = row.keys
|
77
|
+
values << headers.map{|h| row[h] }
|
78
|
+
end
|
79
|
+
elsif items.respond_to?(:each)
|
80
|
+
items.each do |row|
|
81
|
+
if row.kind_of?(Hash)
|
82
|
+
headers = row.keys
|
83
|
+
values << headers.map{|h| row[h] }
|
84
|
+
else
|
85
|
+
values << row
|
86
|
+
end
|
87
|
+
end
|
88
|
+
else
|
89
|
+
values = [items]
|
90
|
+
end
|
91
|
+
|
92
|
+
headers = nil if headers.empty?
|
93
|
+
[headers, values]
|
94
|
+
end
|
95
|
+
|
96
|
+
def handle_exception_in_explain
|
97
|
+
yield
|
98
|
+
rescue Exception => e
|
99
|
+
begin
|
100
|
+
# guarantees no throw from explain_sql
|
101
|
+
NewRelic::Control.instance.log.error("Error getting query plan: #{e.message}")
|
102
|
+
NewRelic::Control.instance.log.debug(e.backtrace.join("\n"))
|
103
|
+
rescue Exception
|
104
|
+
# double exception. throw up your hands
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def is_select?(statement)
|
109
|
+
# split the string into at most two segments on the
|
110
|
+
# system-defined field separator character
|
111
|
+
first_word, rest_of_statement = statement.split($;, 2)
|
112
|
+
(first_word.upcase == 'SELECT')
|
113
|
+
end
|
114
|
+
|
115
|
+
class ConnectionManager
|
116
|
+
include Singleton
|
117
|
+
|
118
|
+
# Returns a cached connection for a given ActiveRecord
|
119
|
+
# configuration - these are stored or reopened as needed, and if
|
120
|
+
# we cannot get one, we ignore it and move on without explaining
|
121
|
+
# the sql
|
122
|
+
def get_connection(config)
|
123
|
+
@connections ||= {}
|
124
|
+
|
125
|
+
connection = @connections[config]
|
126
|
+
|
127
|
+
return connection if connection
|
128
|
+
|
129
|
+
begin
|
130
|
+
connection = ActiveRecord::Base.send("#{config[:adapter]}_connection", config)
|
131
|
+
@connections[config] = connection
|
132
|
+
rescue => e
|
133
|
+
NewRelic::Agent.agent.log.error("Caught exception #{e} trying to get connection to DB for explain. Control: #{config}")
|
134
|
+
NewRelic::Agent.agent.log.error(e.backtrace.join("\n"))
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Closes all the connections in the internal connection cache
|
140
|
+
def close_connections
|
141
|
+
@connections ||= {}
|
142
|
+
@connections.values.each do |connection|
|
143
|
+
begin
|
144
|
+
connection.disconnect!
|
145
|
+
rescue
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
@connections = {}
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class Obfuscator
|
154
|
+
include Singleton
|
155
|
+
|
156
|
+
attr_reader :obfuscator
|
157
|
+
|
158
|
+
def initialize
|
159
|
+
reset
|
160
|
+
end
|
161
|
+
|
162
|
+
def reset
|
163
|
+
@obfuscator = method(:default_sql_obfuscator)
|
164
|
+
end
|
165
|
+
|
166
|
+
# Sets the sql obfuscator used to clean up sql when sending it
|
167
|
+
# to the server. Possible types are:
|
168
|
+
#
|
169
|
+
# :before => sets the block to run before the existing
|
170
|
+
# obfuscators
|
171
|
+
#
|
172
|
+
# :after => sets the block to run after the existing
|
173
|
+
# obfuscator(s)
|
174
|
+
#
|
175
|
+
# :replace => removes the current obfuscator and replaces it
|
176
|
+
# with the provided block
|
177
|
+
def set_sql_obfuscator(type, &block)
|
178
|
+
if type == :before
|
179
|
+
@obfuscator = NewRelic::ChainedCall.new(block, @obfuscator)
|
180
|
+
elsif type == :after
|
181
|
+
@obfuscator = NewRelic::ChainedCall.new(@obfuscator, block)
|
182
|
+
elsif type == :replace
|
183
|
+
@obfuscator = block
|
184
|
+
else
|
185
|
+
fail "unknown sql_obfuscator type #{type}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def default_sql_obfuscator(sql)
|
190
|
+
sql = sql.dup
|
191
|
+
# This is hardly readable. Use the unit tests.
|
192
|
+
# remove single quoted strings:
|
193
|
+
sql.gsub!(/'(.*?[^\\'])??'(?!')/, '?')
|
194
|
+
# remove double quoted strings:
|
195
|
+
sql.gsub!(/"(.*?[^\\"])??"(?!")/, '?')
|
196
|
+
# replace all number literals
|
197
|
+
sql.gsub!(/\d+/, "?")
|
198
|
+
sql
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|