Читаем исходники newrelic_rpm

Illustration of a person sitting on the floor against a wall, engrossed in reading a book, with a shadow casting over half of the scene. Illustration of a person sitting on the floor against a wall, engrossed in reading a book, with a shadow casting over half of the scene.

Чтение исходников – лучший способ разобраться в том, как что-то работает. Гем newrelic_rpm (-v 3.5.4.34) – кладезь знаний о практическом применении самых изощрённых конструкций ruby. Из его сорцов можно многое понять об объектной модели ruby, о том как работает rails и, конечно же, о том как незаметно следить за всем, что делает пользователь вашего приложения. Я потратил большую часть своего воскресенья на изучение "кишок" newrelic_rpm и готов поделиться своими открытиями. Надеюсь, у меня получилось понятно описать как именно работает, собирает и отправляет данные гем самой популярной платформы для аналитики ruby приложений.

В основе newrelic_rpm лежит railtie, определённый в ./lib/newrelic_rpm.rb. Использование railtie позволяет получить доступ к процессу загрузки rails приложения. В данном случае - инициализировать агента NewRelic.

# ./lib/newrelic_rpm.rb

module NewRelic
      class Railtie < Rails::Railtie
        initializer "newrelic_rpm.start_plugin" do |app|
             NewRelic::Control.instance.init_plugin(:config => app.config)
        end
      end
end

Метод init_plugin наконец-то запускает агента (если всё в порядке с конфигом в newrelic.yml вашего приложения), который и будет собирать данные о приложении:

# ./lib/new_relic/control/instance_methods.rb:80

...
    NewRelic::Agent.agent = NewRelic::Agent::Agent.instance
    if Agent.config[:agent_enabled] && !NewRelic::Agent.instance.started?
      setup_log unless logger_override
      start_agent
      install_instrumentation
      load_samplers unless Agent.config['disable_samplers']
      local_env.gather_environment_info
      append_environment_info
    elsif !Agent.config[:agent_enabled]
      install_shim
    end
 end

 def start_agent
    NewRelic::Agent.agent.start
 end

Обратите внимание на метод local_env. Используя класс NewRelic::LocalEnvironment NewRelic определяет что же у вас за приложение - rails, sinatra и т.д. Затем эта информация используется для загрузки специфичных для каждого вида приложений методов, которые разбиты по модулям в ./lib/new_relic/control/frameworks/. Выглядит эта загрузка следующим образом:

# ./lib/new_relic/control/class_methods.rb:35

def load_framework_class(framework)
  begin
    require "new_relic/control/frameworks/#{framework}"
  rescue LoadError
  end
  NewRelic::Control::Frameworks.const_get(framework.to_s.capitalize)
end

В коде NewRelic::LocalEnvironment можно найти хорошие примеры о том как, используя ruby, получить информацию об используемых в приложении фреймворке, веб-сервере, гемах, архитектуры ОС и т.д. Советую изучить ./lib/new_relic/local_environment.rb и поиграться с этим классом в консоли. Вот какую информацию получил NewRelic о моём окружении после вызова gather_environment_info и local_env.append_environment_info:

{"Framework"=>"rails3", "Dispatcher"=>"unicorn", "Environment"=>"development", "Ruby version"=>"1.9.3", 
"Ruby description"=>"ruby 1.9.3p327 (2012-11-10 revision 37606) [x86_64-darwin12.2.1]", 
"Ruby platform"=>"x86_64-darwin12.2.1", "Ruby patchlevel"=>327, "Arch"=>"i386\n", 
"Database adapter"=>"postgresql", "Rails version"=>"3.2.11", "Rails Env"=>"development"}

Работает агент в отдельном потоке. При этом в первую очередь он пытается соединится с сервером NewRelic, и если соединение успешно, то запускает бесконечный цикл, который прерывается только если работа агента завершена.

#./lib/new_relic/agent/agent.rb

def start_worker_thread(connection_options = {})
  log.debug "Creating Ruby Agent worker thread."
  @worker_thread = NewRelic::Agent::AgentThread.new('Worker Loop') do
    deferred_work!(connection_options)
  end
end

 def deferred_work!(connection_options)
  catch_errors do
    NewRelic::Agent.disable_all_tracing do
      connect(connection_options)
      if @connected
        log_worker_loop_start
        create_and_run_worker_loop
      else
        log.debug "No connection. Worker thread ending."
      end
    end
  end
end

Сам цикл работает так: получает на выполнение блок кода, выполняет его, потом останавливает поток с агентом на некоторое время, затем снова выполняет переданный блок кода.

#./lib/new_relic/agen/worker_loop.rb

def run(period=nil, &block)
  @period = period if period
  @next_invocation_time = (Time.now + @period)
  @task = block
  while keep_running? do
    while @now < @next_invocation_time
      sleep_time = @next_invocation_time - @now
      sleep sleep_time if sleep_time > 0
      @now = Time.now
    end
    run_task if keep_running?
    @iterations += 1 if !@limit.nil?
  end
end

Метод, который NewRelic крутит в этом цикле определён всё в том же классе Agent, называется transmit_data. Занимается он отправкой данных на сервера NewRelic. Данные при этом хранятся в объектах класса TransactionSample (./lib/new_relic/transaction_sample.rb), а все собранные сэмплы в свою очередь содержатся в атрибуте @samples класса TransactionSampler (./lib/new_relic/agent/transactions_sampler.rb), экземпляр которого является атрибутом того самого Агента. Сэмплы разбиты на сегменты (./lib/new_relic/transaction_sample/segment.rb). Перед отправкой на сервера NewRelic из сэмплов убираются все лишние данные. Пример сэмпла, сформированного при загрузке блог поста в FJEngine:

Transaction Sample collected at 2013-02-24 19:32:43 +0100
  {
  Path: Controller/posts/show 
  segment_count: 20
  request_params: 
  uri: /posts/57
  custom_params: [:cpu_time, 0.1900000000000004]
  }

>> 0 ms [Segment] ROOT 
  >> 1 ms [Segment] Controller/posts/show 
      -backtrace : ["/Users/fodoj/.rvm/gems/ruby-1.9.3-p327/gems/actionpack-3.2.11/lib/abstract_cont
    >> 20 ms [Segment] Database/SQL/other 
        -backtrace : ["/Users/fodoj/.rvm/gems/ruby-1.9.3-p327/gems/activerecord-3.2.11/lib/active_reco
        -sql : SELECT a.attname, format_type(a.atttypid, a.atttypmod),

        -connection_config: {:adapter=>"postgresql", :host=>"localhost", :encoding=>"utf8", :database=>"fjzon
    << 24 ms Database/SQL/other
    >> 25 ms [Segment] Database/SQL/other 
        -backtrace : ["/Users/fodoj/.rvm/gems/ruby-1.9.3-p327/gems/activerecord-3.2.11/lib/active_reco
        -sql : SELECT COUNT(*)
            FROM pg_class c
            LEFT JOIN pg_
        -connection_config: {:adapter=>"postgresql", :host=>"localhost", :encoding=>"utf8", :database=>"fjzon
    << 26 ms Database/SQL/other
    >> 27 ms [Segment] Database/SQL/other 
        -backtrace : ["/Users/fodoj/.rvm/gems/ruby-1.9.3-p327/gems/activerecord-3.2.11/lib/active_reco
        -sql : SELECT DISTINCT(attr.attname)
          FROM pg_attribute attr

        -connection_config: {:adapter=>"postgresql", :host=>"localhost", :encoding=>"utf8", :database=>"fjzon
    << 29 ms Database/SQL/other

И вот тот же сэмпл, но подготовленный для отправки в NewRelic (./lib/new_relic/transaction_sample.rb:167#prepare_to_send):

Transaction Sample collected at 2013-02-24 19:35:02 +0100
  {
  Path: Controller/posts/show 
  segment_count: 40
  request_params: 
  uri: /posts/57
  custom_params: [:cpu_time, 0.17999999999999972]
  }

>> 0 ms [Segment] ROOT 
  >> 0 ms [Segment] Controller/posts/show 
    >> 18 ms [Segment] Database/SQL/other 
    << 23 ms Database/SQL/other
    >> 23 ms [Segment] Database/SQL/other 
    << 25 ms Database/SQL/other
    >> 25 ms [Segment] Database/SQL/other 
    << 27 ms Database/SQL/other

Вкратце о том, как newrelic_rpm собирает данные. В ./lib/new_relic/agent/instrumentation/ определены различные расширения и модули для перехвата выполняемых в приложении методов. К примеру, ./lib/new_relic/agent/instrumentation/active_record.rb переопределяет метод log, тем самым получая доступ к выполняемым sql запросам. В том же файле можно посмотреть использование чудесного метода add_method_tracer из модуля MethodTracer (./lib/new_relic/agent/method_tracer.rb) – уже ради одного этого модуля стоит изучить исходники гема. Для анализа времени выполнения кода используется StatsEngine, в дебри которого пока что лезть не будем.