newrelic_rpm 3.5.5.540.dev → 3.5.6.42.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/.gitignore +1 -1
  2. data/CHANGELOG +7 -0
  3. data/Rakefile +9 -18
  4. data/lib/new_relic/agent/agent.rb +51 -83
  5. data/lib/new_relic/agent/configuration/manager.rb +12 -0
  6. data/lib/new_relic/agent/configuration/yaml_source.rb +5 -1
  7. data/lib/new_relic/agent/cross_process_monitoring.rb +164 -20
  8. data/lib/new_relic/agent/error_collector.rb +2 -0
  9. data/lib/new_relic/agent/event_listener.rb +39 -0
  10. data/lib/new_relic/agent/instrumentation/browser_monitoring_timings.rb +17 -7
  11. data/lib/new_relic/agent/stats_engine/transactions.rb +1 -0
  12. data/lib/new_relic/build.rb +1 -0
  13. data/lib/new_relic/control/frameworks/rails.rb +17 -3
  14. data/lib/new_relic/rack/agent_hooks.rb +20 -0
  15. data/lib/new_relic/rack/error_collector.rb +11 -1
  16. data/lib/new_relic/recipes.rb +32 -10
  17. data/lib/new_relic/version.rb +10 -15
  18. data/newrelic.yml +6 -0
  19. data/newrelic_rpm.gemspec +23 -454
  20. data/test/multiverse/lib/multiverse/suite.rb +2 -0
  21. data/test/multiverse/suites/active_record/Envfile +1 -1
  22. data/test/multiverse/suites/active_record/config/newrelic.yml +2 -2
  23. data/test/multiverse/suites/agent_only/Envfile +2 -1
  24. data/test/multiverse/suites/agent_only/config/newrelic.yml +3 -1
  25. data/test/multiverse/suites/agent_only/cross_process_test.rb +56 -0
  26. data/test/multiverse/suites/{logging → agent_only}/logging_test.rb +24 -23
  27. data/test/multiverse/suites/agent_only/no_dns_resolv.rb +17 -0
  28. data/test/multiverse/suites/{rum_auto_instrumentation/sanity_test.rb → agent_only/rum_instrumentation_test.rb} +25 -46
  29. data/test/multiverse/suites/{no_load → agent_only}/start_up_test.rb +0 -0
  30. data/test/multiverse/suites/agent_only/testing_app.rb +17 -0
  31. data/test/multiverse/suites/agent_only/thread_profiling_test.rb +6 -5
  32. data/test/multiverse/suites/datamapper/config/newrelic.yml +1 -1
  33. data/test/multiverse/suites/{rails_3_queue_time → rails}/Envfile +3 -0
  34. data/test/multiverse/suites/{rails_3_views → rails}/app/views/foos/_foo.html.haml +0 -0
  35. data/test/multiverse/suites/{rails_3_views/app/views/test → rails/app/views/views}/_a_partial.html.erb +0 -0
  36. data/test/multiverse/suites/{rails_3_views/app/views/test → rails/app/views/views}/_mid_partial.html.erb +0 -0
  37. data/test/multiverse/suites/{rails_3_views/app/views/test → rails/app/views/views}/_top_partial.html.erb +0 -0
  38. data/test/multiverse/suites/{rails_3_views/app/views/test → rails/app/views/views}/deep_partial.html.erb +0 -0
  39. data/test/multiverse/suites/{rails_3_views/app/views/test → rails/app/views/views}/haml_view.html.haml +0 -0
  40. data/test/multiverse/suites/{rails_3_views/app/views/test → rails/app/views/views}/index.html.erb +0 -0
  41. data/test/multiverse/suites/rails/app.rb +49 -0
  42. data/test/multiverse/suites/{rails_3_error_tracing → rails}/config/newrelic.yml +0 -0
  43. data/test/multiverse/suites/{rails_3_error_tracing → rails}/error_tracing_test.rb +13 -68
  44. data/test/multiverse/suites/{rails_3_gc/instrumentation_test.rb → rails/gc_instrumentation_test.rb} +14 -35
  45. data/test/multiverse/suites/{rails_3_queue_time → rails}/queue_time_test.rb +3 -23
  46. data/test/multiverse/suites/{rails_3_views → rails}/view_instrumentation_test.rb +21 -61
  47. data/test/multiverse/suites/resque/config/newrelic.yml +1 -1
  48. data/test/multiverse/suites/sinatra/config/newrelic.yml +1 -2
  49. data/test/multiverse/test/suite_examples/one/a/config/newrelic.yml +1 -1
  50. data/test/multiverse/test/suite_examples/one/b/config/newrelic.yml +1 -1
  51. data/test/new_relic/agent/agent/connect_test.rb +17 -74
  52. data/test/new_relic/agent/agent/start_worker_thread_test.rb +3 -3
  53. data/test/new_relic/agent/agent_test.rb +59 -2
  54. data/test/new_relic/agent/configuration/manager_test.rb +28 -0
  55. data/test/new_relic/agent/configuration/yaml_source_test.rb +12 -2
  56. data/test/new_relic/agent/cross_process_monitoring_test.rb +144 -31
  57. data/test/new_relic/agent/event_listener_test.rb +46 -0
  58. data/test/new_relic/agent/instrumentation/browser_monitoring_timings_test.rb +57 -30
  59. data/test/new_relic/agent/worker_loop_test.rb +1 -1
  60. data/test/new_relic/fake_collector.rb +17 -4
  61. data/test/new_relic/rack/agent_hooks_test.rb +30 -0
  62. data/test/new_relic/rack/error_collector_test.rb +16 -0
  63. data/test/new_relic/version_number_test.rb +6 -30
  64. data/test/script/ci.sh +6 -5
  65. data/test/test_contexts.rb +1 -0
  66. data/test/test_helper.rb +2 -4
  67. metadata +34 -42
  68. data/newrelic_rpm.gemspec.erb +0 -53
  69. data/test/fixtures/gemspec_no_build.rb +0 -442
  70. data/test/fixtures/gemspec_with_build.rb +0 -442
  71. data/test/fixtures/gemspec_with_build_and_stage.rb +0 -442
  72. data/test/multiverse/suites/logging/Envfile +0 -4
  73. data/test/multiverse/suites/logging/config/newrelic.yml +0 -22
  74. data/test/multiverse/suites/monitor_mode_false/Envfile +0 -2
  75. data/test/multiverse/suites/monitor_mode_false/config/newrelic.yml +0 -25
  76. data/test/multiverse/suites/monitor_mode_false/no_dns_resolv.rb +0 -29
  77. data/test/multiverse/suites/no_load/Envfile +0 -2
  78. data/test/multiverse/suites/no_load/config/newrelic.yml +0 -22
  79. data/test/multiverse/suites/rails_3_error_tracing/Envfile +0 -15
  80. data/test/multiverse/suites/rails_3_gc/Envfile +0 -8
  81. data/test/multiverse/suites/rails_3_gc/config/newrelic.yml +0 -167
  82. data/test/multiverse/suites/rails_3_queue_time/config/newrelic.yml +0 -165
  83. data/test/multiverse/suites/rails_3_views/.gitignore +0 -3
  84. data/test/multiverse/suites/rails_3_views/Envfile +0 -16
  85. data/test/multiverse/suites/rails_3_views/config/newrelic.yml +0 -164
  86. data/test/multiverse/suites/rum_auto_instrumentation/Envfile +0 -4
  87. data/test/multiverse/suites/rum_auto_instrumentation/config/newrelic.yml +0 -24
  88. data/test/multiverse/suites/rum_auto_instrumentation/responses/worst_case_small.html +0 -5000
data/.gitignore CHANGED
@@ -1,5 +1,4 @@
1
1
  Gemfile.lock
2
- newrelic_rpm.gemspec
3
2
  .DS\_Store
4
3
  .svn/
5
4
  *~
@@ -19,3 +18,4 @@ tags
19
18
  /lerg/
20
19
  gems/newrelic_rpm.gemspec
21
20
  gems/newrelic_rpm*.tar.gz
21
+ lib/new_relic/build.rb
data/CHANGELOG CHANGED
@@ -3,6 +3,13 @@
3
3
 
4
4
  ## v3.5.5 ##
5
5
 
6
+ * Add thread profiling support
7
+
8
+ Thread profiling performs statistical sampling of backtraces of all threads
9
+ within your Ruby processes. This feature requires MRI >= 1.9.2, and is
10
+ controlled via the New Relic web UI. JRuby support (in 1.9.x compat mode) is
11
+ considered experimental, due to issues with JRuby's Thread#backtrace.
12
+
6
13
  * Add audit logging capability
7
14
 
8
15
  The agent can now log all of the data it sends to the New Relic servers to
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require "#{File.dirname(__FILE__)}/lib/tasks/all.rb"
5
5
 
6
6
  task :default => :test
7
7
 
8
- task :test => [:gemspec, 'test:newrelic']
8
+ task :test => ['test:newrelic']
9
9
 
10
10
  namespace :test do
11
11
  desc "Run all tests"
@@ -14,7 +14,7 @@ namespace :test do
14
14
  agent_home = File.expand_path(File.dirname(__FILE__))
15
15
 
16
16
  desc "Run functional test suite for newrelic"
17
- task :multiverse, [:suite, :mode] => [:gemspec] do |t, args|
17
+ task :multiverse, [:suite, :mode] => [] do |t, args|
18
18
  args.with_defaults(:suite => "", :mode => "")
19
19
  if args.mode == "run_one"
20
20
  puts `#{agent_home}/test/multiverse/script/run_one #{args.suite}`
@@ -24,7 +24,7 @@ namespace :test do
24
24
  end
25
25
 
26
26
  desc "Test the multiverse testing framework by executing tests in test/multiverse/test. Get meta with it."
27
- task 'multiverse:self', [:suite, :mode] => [:gemspec] do |t, args|
27
+ task 'multiverse:self', [:suite, :mode] => [] do |t, args|
28
28
  args.with_defaults(:suite => "", :mode => "")
29
29
  puts ("Testing the multiverse testing framework...")
30
30
  test_files = FileList['test/multiverse/test/*_test.rb']
@@ -43,20 +43,11 @@ namespace :test do
43
43
 
44
44
  end
45
45
 
46
- desc 'Generate gemspec [ build_number, stage ]'
47
- task :gemspec, [ :build_number, :stage ] do |t, args|
48
- require 'erb'
49
- version = NewRelic::VERSION::STRING.split('.')[0..2]
50
- version << args.build_number.to_s if args.build_number
51
- version << args.stage.to_s if args.stage
52
-
53
- version_string = version.join('.')
54
- gem_version = Gem::VERSION
55
- date = Time.now.strftime('%Y-%m-%d')
56
- files = `git ls-files`.split + ['newrelic_rpm.gemspec']
57
-
58
- template = ERB.new(File.read('newrelic_rpm.gemspec.erb'))
59
- File.open('newrelic_rpm.gemspec', 'w') do |gemspec|
60
- gemspec.write(template.result(binding))
46
+ desc 'Record build number and stage'
47
+ task :record_build, [ :build_number, :stage ] do |t, args|
48
+ build_string = args.build_number
49
+ build_string << ".#{args.stage}" if args.stage
50
+ File.open("lib/new_relic/build.rb", "w") do |f|
51
+ f.write("module NewRelic; module VERSION; BUILD='#{build_string}'; end; end\n")
61
52
  end
62
53
  end
@@ -24,11 +24,15 @@ module NewRelic
24
24
  @launch_time = Time.now
25
25
 
26
26
  @metric_ids = {}
27
+ @events = NewRelic::Agent::EventListener.new
27
28
  @stats_engine = NewRelic::Agent::StatsEngine.new
28
29
  @transaction_sampler = NewRelic::Agent::TransactionSampler.new
29
30
  @sql_sampler = NewRelic::Agent::SqlSampler.new
30
31
  @thread_profiler = NewRelic::Agent::ThreadProfiler.new
32
+ @cross_process_monitor = NewRelic::Agent::CrossProcessMonitor.new(@events)
31
33
  @error_collector = NewRelic::Agent::ErrorCollector.new
34
+
35
+ @connect_state = :pending
32
36
  @connect_attempts = 0
33
37
 
34
38
  @last_harvest_time = Time.now
@@ -94,6 +98,10 @@ module NewRelic
94
98
  attr_reader :cross_process_encoding_bytes
95
99
  # service for communicating with collector
96
100
  attr_accessor :service
101
+ # Global events dispatcher. This will provides our primary mechanism
102
+ # for agent-wide events, such as finishing configuration, error notification
103
+ # and request before/after from Rack.
104
+ attr_reader :events
97
105
 
98
106
 
99
107
  # Returns the length of the unsent errors array, if it exists,
@@ -175,19 +183,21 @@ module NewRelic
175
183
  @forked = true
176
184
  Agent.config.apply_config(NewRelic::Agent::Configuration::ManualSource.new(options), 1)
177
185
 
178
- # @connected gets false after we fail to connect or have an error
179
- # connecting. @connected has nil if we haven't finished trying to connect.
180
- # or we didn't attempt a connection because this is the master process
181
-
182
186
  if channel_id = options[:report_to_channel]
183
187
  @service = NewRelic::Agent::PipeService.new(channel_id)
184
- @connected_pid = $$
185
- @metric_ids = {}
188
+ if connected?
189
+ @connected_pid = $$
190
+ @metric_ids = {}
191
+ else
192
+ ::NewRelic::Agent.logger.debug("Child process #{$$} not reporting to non-connected parent.")
193
+ @service.shutdown(Time.now)
194
+ disconnect
195
+ end
186
196
  end
187
197
 
188
198
  return if !Agent.config[:agent_enabled] ||
189
199
  !Agent.config[:monitor_mode] ||
190
- @connected == false ||
200
+ disconnected? ||
191
201
  @worker_thread && @worker_thread.alive?
192
202
 
193
203
  ::NewRelic::Agent.logger.debug "Starting the worker thread in #{$$} after forking."
@@ -210,12 +220,6 @@ module NewRelic
210
220
  @started
211
221
  end
212
222
 
213
- # Return nil if not yet connected, true if successfully started
214
- # and false if we failed to start.
215
- def connected?
216
- @connected
217
- end
218
-
219
223
  # Attempt a graceful shutdown of the agent, running the worker
220
224
  # loop if it exists and is running.
221
225
  #
@@ -518,7 +522,7 @@ module NewRelic
518
522
  ::NewRelic::Agent.logger.debug error.message
519
523
  reset_stats
520
524
  @metric_ids = {}
521
- @connected = nil
525
+ @connect_state = :pending
522
526
  sleep 30
523
527
  end
524
528
 
@@ -577,7 +581,7 @@ module NewRelic
577
581
  # just exit the thread. If it returns nil
578
582
  # that means it didn't try to connect because we're in the master.
579
583
  connect(connection_options)
580
- if @connected
584
+ if connected?
581
585
  log_worker_loop_start
582
586
  create_and_run_worker_loop
583
587
  # never reaches here unless there is a problem or
@@ -610,60 +614,41 @@ module NewRelic
610
614
  # method - all of its methods are used in that context, so it
611
615
  # can be refactored at will. It should be fully tested
612
616
  module Connect
613
- # the frequency with which we should try to connect to the
614
- # server at the moment.
615
- attr_accessor :connect_retry_period
616
617
  # number of attempts we've made to contact the server
617
618
  attr_accessor :connect_attempts
618
619
 
619
620
  # Disconnect just sets connected to false, which prevents
620
621
  # the agent from trying to connect again
621
622
  def disconnect
622
- @connected = false
623
+ @connect_state = :disconnected
623
624
  true
624
625
  end
625
626
 
626
- # We've tried to connect if @connected is not nil, or if we
627
- # are forcing reconnection (i.e. in the case of an
628
- # after_fork with long running processes)
629
- def tried_to_connect?(options)
630
- !(@connected.nil? || options[:force_reconnect])
627
+ def connected?
628
+ @connect_state == :connected
631
629
  end
632
630
 
633
- # We keep trying by default, but you can disable it with the
634
- # :keep_retrying option set to false
635
- def should_keep_retrying?(options)
636
- @keep_retrying = (options[:keep_retrying].nil? || options[:keep_retrying])
631
+ def disconnected?
632
+ @connect_state == :disconnected
633
+ end
634
+
635
+ # Don't connect if we're already connected, or if we tried to connect
636
+ # and were rejected with prejudice because of a license issue, unless
637
+ # we're forced to by force_reconnect.
638
+ def should_connect?(force=false)
639
+ force || (!connected? && !disconnected?)
637
640
  end
638
641
 
639
642
  # Retry period is a minute for each failed attempt that
640
643
  # we've made. This should probably do some sort of sane TCP
641
644
  # backoff to prevent hammering the server, but a minute for
642
645
  # each attempt seems to work reasonably well.
643
- def get_retry_period
644
- return 600 if self.connect_attempts > 6
645
- connect_attempts * 60
646
- end
647
-
648
- def increment_retry_period! #:nodoc:
649
- self.connect_retry_period=(get_retry_period)
646
+ def connect_retry_period
647
+ [600, connect_attempts * 60].min
650
648
  end
651
649
 
652
- # We should only retry when there has not been a more
653
- # serious condition that would prevent it. We increment the
654
- # connect attempts and the retry period, to prevent constant
655
- # connection attempts, and tell the user what we're doing by
656
- # logging.
657
- def should_retry?
658
- if @keep_retrying
659
- self.connect_attempts=(connect_attempts + 1)
660
- increment_retry_period!
661
- ::NewRelic::Agent.logger.warn "Will re-attempt in #{connect_retry_period} seconds"
662
- true
663
- else
664
- disconnect
665
- false
666
- end
650
+ def note_connect_failure
651
+ self.connect_attempts += 1
667
652
  end
668
653
 
669
654
  # When we have a problem connecting to the server, we need
@@ -744,24 +729,12 @@ module NewRelic
744
729
  Agent.config.apply_config(server_config, 1)
745
730
  log_connection!(config_data) if @service
746
731
 
747
- @cross_process_id = Agent.config[:cross_process_id]
748
- @cross_process_encoding_key = Agent.config[:encoding_key]
749
- @cross_process_encoding_bytes = get_bytes(@cross_process_encoding_key) unless @cross_process_encoding_key.nil?
732
+ # If you're adding something else here to respond to the server-side config,
733
+ # use Agent.instance.events.subscribe(:finished_configuring) callback instead!
750
734
 
751
735
  @beacon_configuration = BeaconConfiguration.new
752
736
  end
753
737
 
754
- # Ruby 1.8.6 doesn't support the bytes method on strings.
755
- def get_bytes(value)
756
- return [] if value.nil?
757
-
758
- bytes = []
759
- value.each_byte do |b|
760
- bytes << b
761
- end
762
- bytes
763
- end
764
-
765
738
  # Logs when we connect to the server, for debugging purposes
766
739
  # - makes sure we know if an agent has not connected
767
740
  def log_connection!(config_data)
@@ -825,7 +798,7 @@ module NewRelic
825
798
  public :merge_data_from
826
799
 
827
800
  # Connect to the server and validate the license. If successful,
828
- # @connected has true when finished. If not successful, you can
801
+ # connected? returns true when finished. If not successful, you can
829
802
  # keep calling this. Return false if we could not establish a
830
803
  # connection with the server and we should not retry, such as if
831
804
  # there's a bad license key.
@@ -840,25 +813,27 @@ module NewRelic
840
813
  # * <tt>force_reconnect => true</tt> if you want to establish a new connection
841
814
  # to the server before running the worker loop. This means you get a separate
842
815
  # agent run and New Relic sees it as a separate instance (default is false).
843
- def connect(options)
844
- # Don't proceed if we already connected (@connected=true) or if we tried
845
- # to connect and were rejected with prejudice because of a license issue
846
- # (@connected=false), unless we're forced to by force_reconnect.
847
- return if tried_to_connect?(options)
816
+ def connect(options={})
817
+ defaults = {
818
+ :keep_retrying => true,
819
+ :force_reconnect => false
820
+ }
821
+ opts = defaults.merge(options)
848
822
 
849
- # wait a few seconds for the web server to boot, necessary in development
850
- @connect_retry_period = should_keep_retrying?(options) ? 10 : 0
823
+ return unless should_connect?(opts[:force_reconnect])
851
824
 
852
- sleep connect_retry_period
853
825
  ::NewRelic::Agent.logger.debug "Connecting Process to New Relic: #$0"
854
826
  query_server_for_configuration
855
827
  @connected_pid = $$
856
- @connected = true
828
+ @connect_state = :connected
857
829
  rescue NewRelic::Agent::LicenseException => e
858
830
  handle_license_error(e)
859
831
  rescue Timeout::Error, StandardError => e
860
832
  log_error(e)
861
- if should_retry?
833
+ if opts[:keep_retrying]
834
+ note_connect_failure
835
+ ::NewRelic::Agent.logger.warn "Will re-attempt in #{connect_retry_period} seconds"
836
+ sleep connect_retry_period
862
837
  retry
863
838
  else
864
839
  disconnect
@@ -876,13 +851,6 @@ module NewRelic
876
851
  control.root
877
852
  end
878
853
 
879
- # Checks whether this process is a Passenger or Unicorn
880
- # spawning server - if so, we probably don't intend to report
881
- # statistics from this process
882
- def is_application_spawner?
883
- $0 =~ /ApplicationSpawner|^unicorn\S* master/
884
- end
885
-
886
854
  # calls the busy harvester and collects timeslice data to
887
855
  # send later
888
856
  def harvest_timeslice_data(time=Time.now)
@@ -1071,7 +1039,7 @@ module NewRelic
1071
1039
  # If this process comes from a parent process, it will not
1072
1040
  # disconnect, so that the parent process can continue to send data
1073
1041
  def graceful_disconnect
1074
- if @connected
1042
+ if connected?
1075
1043
  begin
1076
1044
  @service.request_timeout = 10
1077
1045
  transmit_data(true)
@@ -20,10 +20,14 @@ module NewRelic
20
20
  end
21
21
 
22
22
  def apply_config(source, level=0)
23
+ was_finished = finished_configuring?
24
+
23
25
  invoke_callbacks(:add, source)
24
26
  @config_stack.insert(level, source.freeze)
25
27
  reset_cache
26
28
  log_config(:add, source)
29
+
30
+ notify_finished_configuring if !was_finished && finished_configuring?
27
31
  end
28
32
 
29
33
  def remove_config(source=nil)
@@ -86,6 +90,14 @@ module NewRelic
86
90
  end
87
91
  end
88
92
 
93
+ def notify_finished_configuring
94
+ NewRelic::Agent.instance.events.notify(:finished_configuring)
95
+ end
96
+
97
+ def finished_configuring?
98
+ @config_stack.any? {|s| s.is_a?(ServerSource)}
99
+ end
100
+
89
101
  def flattened
90
102
  @config_stack.reverse.inject({}) do |flat,layer|
91
103
  thawed_layer = layer.dup
@@ -25,7 +25,11 @@ module NewRelic
25
25
  license_key = ''
26
26
 
27
27
  erb = ERB.new(file).result(binding)
28
- config = merge!(YAML.load(erb)[env] || {})
28
+ confighash = YAML.load(erb)
29
+ ::NewRelic::Agent.logger.error("Config (#{path}) doesn't include a '#{env}' environment!") unless
30
+ confighash.key?(env)
31
+
32
+ config = merge!(confighash[env] || {})
29
33
  rescue ScriptError, StandardError => e
30
34
  ::NewRelic::Agent.logger.error("Unable to read configuration file #{path}: #{e}")
31
35
  end
@@ -1,43 +1,187 @@
1
+ require 'new_relic/rack/agent_hooks'
2
+ require 'new_relic/agent/thread'
3
+
1
4
  module NewRelic
2
5
  module Agent
3
- module CrossProcessMonitoring
6
+ class CrossProcessMonitor
7
+
8
+ def initialize(events = nil)
9
+ # When we're starting up for real in the agent, we get passed the events
10
+ # Other spots can pull from the agent, during startup the agent doesn't exist yet!
11
+ events ||= Agent.instance.events
12
+ @trusted_ids = []
13
+
14
+ events.subscribe(:finished_configuring) do
15
+ finish_setup(Agent.config)
16
+ register_event_listeners
17
+ end
18
+ end
19
+
20
+ def finish_setup(config)
21
+ @cross_process_id = config[:cross_process_id]
22
+ @encoding_key = config[:encoding_key]
23
+ @encoding_bytes = get_bytes(@encoding_key) unless @encoding_key.nil?
24
+ @trusted_ids = config[:trusted_account_ids] || []
25
+ end
26
+
27
+ # Expected sequence of events:
28
+ # :before_call will save our cross process request id to the thread
29
+ # :start_transaction will get called when a transaction starts up
30
+ # :after_call will write our response headers/metrics and clean up the thread
31
+ def register_event_listeners
32
+ NewRelic::Agent.logger.debug("Wiring up Cross Process monitoring to events after finished configuring")
33
+
34
+ events = Agent.instance.events
35
+ events.subscribe(:before_call) do |env|
36
+ save_client_cross_process_id(env)
37
+ end
38
+
39
+ events.subscribe(:start_transaction) do |name|
40
+ set_transaction_custom_parameters
41
+ end
42
+
43
+ events.subscribe(:after_call) do |env, (status_code, headers, body)|
44
+ insert_response_header(env, headers)
45
+ end
46
+
47
+ events.subscribe(:notice_error) do |_, options|
48
+ set_error_custom_parameters(options)
49
+ end
50
+ end
51
+
52
+ # Because we aren't in the right spot when our transaction actually
53
+ # starts, hold client_cross_process_id we get thread local until then.
54
+ THREAD_ID_KEY = :newrelic_client_cross_process_id
55
+
56
+ def save_client_cross_process_id(request_headers)
57
+ if should_process_request(request_headers)
58
+ NewRelic::Agent::AgentThread.current[THREAD_ID_KEY] = decoded_id(request_headers)
59
+ end
60
+ end
4
61
 
5
- module_function
62
+ def clear_client_cross_process_id
63
+ NewRelic::Agent::AgentThread.current[THREAD_ID_KEY] = nil
64
+ end
6
65
 
7
- def insert_response_header(request, response)
8
- if Agent.config[:'cross_process.enabled'] &&
9
- NewRelic::Agent.instance.cross_process_id && (id = id_from_request(request))
66
+ def client_cross_process_id
67
+ NewRelic::Agent::AgentThread.current[THREAD_ID_KEY]
68
+ end
10
69
 
11
- content_length = -1
70
+ def insert_response_header(request_headers, response_headers)
71
+ unless client_cross_process_id.nil?
12
72
  timings = NewRelic::Agent::BrowserMonitoring.timings
73
+ content_length = content_length_from_request(request_headers)
74
+
75
+ set_response_headers(response_headers, timings, content_length)
76
+ set_metrics(client_cross_process_id, timings)
77
+
78
+ clear_client_cross_process_id
79
+ end
80
+ end
81
+
82
+ def should_process_request(request_headers)
83
+ return Agent.config[:'cross_process.enabled'] &&
84
+ @cross_process_id &&
85
+ trusts?(request_headers)
86
+ end
87
+
88
+ # Expects an ID of format "12#345", and will only accept that!
89
+ def trusts?(request)
90
+ id = decoded_id(request)
91
+ split_id = id.match(/(\d+)#\d+/)
92
+ return false if split_id.nil?
13
93
 
14
- # FIXME the transaction name might not be properly encoded. use a json generator
15
- payload = %[["#{NewRelic::Agent.instance.cross_process_id}","#{timings.transaction_name}",#{timings.queue_time_in_millis},#{timings.app_time_in_millis},#{content_length}] ]
16
- payload = obfuscate_with_key(payload, NewRelic::Agent.instance.cross_process_encoding_bytes)
94
+ @trusted_ids.include?(split_id.captures.first.to_i)
95
+ end
96
+
97
+ def set_response_headers(response_headers, timings, content_length)
98
+ response_headers['X-NewRelic-App-Data'] = build_payload(timings, content_length)
99
+ end
100
+
101
+ def build_payload(timings, content_length)
102
+
103
+ # FIXME The transaction name might not be properly encoded. use a json generator
104
+ # For now we just handle quote characters by dropping them
105
+ transaction_name = timings.transaction_name.gsub(/["']/, "")
106
+
107
+ payload = %[["#{@cross_process_id}","#{transaction_name}",#{timings.queue_time_in_seconds},#{timings.app_time_in_seconds},#{content_length}] ]
108
+ payload = obfuscate_with_key(payload)
109
+ end
110
+
111
+ def set_transaction_custom_parameters
112
+ # We expect to get the before call to set the id (if we have it) before
113
+ # this, and then write our custom parameter when the transaction starts
114
+ NewRelic::Agent.add_custom_parameters(:client_cross_process_id => client_cross_process_id) unless client_cross_process_id.nil?
115
+ end
116
+
117
+ def set_error_custom_parameters(options)
118
+ options[:client_cross_process_id] = client_cross_process_id unless client_cross_process_id.nil?
119
+ end
120
+
121
+ def set_metrics(id, timings)
122
+ metric = NewRelic::Agent.instance.stats_engine.get_stats_no_scope("ClientApplication/#{id}/all")
123
+ metric.record_data_point(timings.app_time_in_seconds)
124
+ end
125
+
126
+ def obfuscate_with_key(text)
127
+ Base64.encode64(encode_with_key(text)).chomp
128
+ end
17
129
 
18
- response['X-NewRelic-App-Data'] = payload
19
- #FIXME generate ClientApplication metric. id must be decoded first
20
- # String metricName = MessageFormat.format("ClientApplication/{0}/all", id);
130
+ def decode_with_key(text)
131
+ encode_with_key(Base64.decode64(text))
132
+ end
133
+
134
+ NEWRELIC_ID_HEADER_KEYS = %w{X-NewRelic-ID HTTP_X_NEWRELIC_ID X_NEWRELIC_ID}
135
+ CONTENT_LENGTH_HEADER_KEYS = %w{Content-Length HTTP_CONTENT_LENGTH CONTENT_LENGTH}
136
+
137
+ def decoded_id(request)
138
+ encoded_id = from_headers(request, NEWRELIC_ID_HEADER_KEYS)
139
+ return "" if encoded_id.nil?
140
+
141
+ decode_with_key(encoded_id)
142
+ end
143
+
144
+ def content_length_from_request(request)
145
+ from_headers(request, CONTENT_LENGTH_HEADER_KEYS) || -1
146
+ end
147
+
148
+
149
+
150
+ private
151
+
152
+ # Ruby 1.8.6 doesn't support the bytes method on strings.
153
+ def get_bytes(value)
154
+ return [] if value.nil?
155
+
156
+ bytes = []
157
+ value.each_byte do |b|
158
+ bytes << b
21
159
  end
160
+ bytes
22
161
  end
23
162
 
24
- def obfuscate_with_key(text, key_bytes)
25
- obfuscated = ""
163
+ def encode_with_key(text)
164
+ key_bytes = @encoding_bytes
165
+
166
+ encoded = ""
26
167
  index = 0
27
168
  text.each_byte{|byte|
28
- obfuscated.concat((byte ^ key_bytes[index % key_bytes.length].to_i))
169
+ encoded.concat((byte ^ key_bytes[index % key_bytes.length].to_i))
29
170
  index+=1
30
171
  }
31
-
32
- [obfuscated].pack("m0").gsub("\n", '')
172
+ encoded
33
173
  end
34
174
 
35
- def id_from_request(request)
36
- %w{X-NewRelic-ID HTTP_X_NEWRELIC_ID X_NEWRELIC_ID}.each do |header|
37
- return request.env[header] if request.env.has_key?(header)
175
+ def from_headers(request, try_keys)
176
+ # For lookups, upcase all our keys on both sides just to be safe
177
+ upcased_keys = try_keys.map{|k| k.upcase}
178
+ upcased_keys.each do |header|
179
+ found_key = request.keys.find { |k| k.upcase == header }
180
+ return request[found_key] unless found_key.nil?
38
181
  end
39
182
  nil
40
183
  end
184
+
41
185
  end
42
186
  end
43
187
  end