Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building a Performance Analytics Tool with ActiveSupport

Building a Performance Analytics Tool with ActiveSupport

Setting up a performance analytics tool like NewRelic or Skylight is one of the first things many developers do in a new project. However, have you ever wondered how these tools work under the hood?

In this talk, we will build a basic performance analytics tool from scratch. We will deep dive into ActiveSupport instrumentations to collect, group and normalise metrics from your app. To persist these metrics, we will use a time series database and visualise the data on a dashboard. By the end of the talk, you will know how your favourite performance analytics tool works.

Christian Bruckmayer

April 17, 2020
Tweet

More Decks by Christian Bruckmayer

Other Decks in Technology

Transcript

  1. 1.day.from_now => Sun, 29 Mar 2020 13:45:13 UTC +00:00 ["Ruby",

    "Rails"].to_sentence => "Ruby and Rails” ["Ruby", "Rails"].many? => true
  2. Notifications.instrument("add", term: "1 + 1") do 1 + 1 end

    Notifications.subscribe("add") do |payload| logger.log("#{payload[:start]}: #{payload[:term]}") end
  3. redirect_to.action_controller halted_callback.action_controller unpermitted_parameters.action_controller process_middleware.action_dispatch render_template.action_view render_partial.action_view render_collection.action_view sql.active_record instantiation.active_record …

    write_fragment.action_controller read_fragment.action_controller expire_fragment.action_controller exist_fragment?.action_controller write_page.action_controller expire_page.action_controller start_processing.action_controller process_action.action_controller send_file.action_controller send_data.action_controller 51 Hooks
  4. redirect_to.action_controller halted_callback.action_controller unpermitted_parameters.action_controller process_middleware.action_dispatch render_template.action_view render_partial.action_view render_collection.action_view sql.active_record instantiation.active_record …

    write_fragment.action_controller read_fragment.action_controller expire_fragment.action_controller exist_fragment?.action_controller write_page.action_controller expire_page.action_controller start_processing.action_controller process_action.action_controller send_file.action_controller send_data.action_controller 51 Hooks
  5. redirect_to.action_controller halted_callback.action_controller unpermitted_parameters.action_controller process_middleware.action_dispatch render_template.action_view render_partial.action_view render_collection.action_view sql.active_record instantiation.active_record …

    write_fragment.action_controller read_fragment.action_controller expire_fragment.action_controller exist_fragment?.action_controller write_page.action_controller expire_page.action_controller start_processing.action_controller process_action.action_controller send_file.action_controller send_data.action_controller 51 Hooks
  6. redirect_to.action_controller halted_callback.action_controller unpermitted_parameters.action_controller process_middleware.action_dispatch render_template.action_view render_partial.action_view render_collection.action_view sql.active_record instantiation.active_record …

    write_fragment.action_controller read_fragment.action_controller expire_fragment.action_controller exist_fragment?.action_controller write_page.action_controller expire_page.action_controller start_processing.action_controller process_action.action_controller send_file.action_controller send_data.action_controller 51 Hooks
  7. Notifications.instrument("add", term: "1 + 1") do 1 + 1 end

    Notifications.subscribe("add") do |payload| logger.log("#{payload[:start]}: #{payload[:term]}") end
  8. module ActiveMonitoring class Notifications class << self attr_accessor :notifier def

    subscribe(name, &block) notifier[name] ||= [] notifier[name] << block end end self.notifier = {} end end Plugin
  9. module ActiveMonitoring class Notifications class << self attr_accessor :notifier def

    subscribe(name, &block) notifier[name] ||= [] notifier[name] << block end end self.notifier = {} end end Plugin
  10. module ActiveMonitoring class Notifications class << self def instrument(name, payload

    = {}) start = Time.now result = yield if block_given? finish = Time.now notifier[name].to_a.each do |callback| callback.call payload.merge(start: start, finish: finish) end result end end end end Plugin
  11. module ActiveMonitoring class Notifications class << self def instrument(name, payload

    = {}) start = Time.now result = yield if block_given? finish = Time.now notifier[name].to_a.each do |callback| callback.call payload.merge(start: start, finish: finish) end result end end end end Plugin
  12. redirect_to.action_controller halted_callback.action_controller unpermitted_parameters.action_controller process_middleware.action_dispatch render_template.action_view render_partial.action_view render_collection.action_view sql.active_record instantiation.active_record …

    write_fragment.action_controller read_fragment.action_controller expire_fragment.action_controller exist_fragment?.action_controller write_page.action_controller expire_page.action_controller start_processing.action_controller process_action.action_controller send_file.action_controller send_data.action_controller 51 Hooks
  13. { controller: "PostsController", action: "index", params: {"action" => "index", "controller"

    => "posts"}, headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>, format: :html, method: "GET", path: "/posts", request: #<ActionDispatch::Request:0x00007ff1cb9bd7b8>, status: 200, view_runtime: 46.848, db_runtime: 0.157 }
  14. id timestamp duration hook_name location 1 2020-03-28 14:35:59 101 process_action

    Home#index 2 2020-03-28 14:35:59 28 process_action Home#index 3 2020-03-28 14:35:59 300 process_action SignUp#create 4 2020-03-28 14:35:59 56 process_action Home#index
  15. class BooksController < ApplicationController def new; end def create; end

    def show; end def edit; end def update; end def destroy; end end
  16. timestamp duration hook_name location 2020-03-28 14:35:59 101 process_action Home#index 2020-03-28

    14:35:59 28 process_action Home#index 2020-03-28 14:35:59 300 process_action SignUp#create 2020-03-28 14:35:59 56 process_action Home#index
  17. timestamp duration hook_name location 2020-03-28 14:35:59 101 process_action Home#index 2020-03-28

    14:35:59 28 process_action Home#index 2020-03-28 14:35:59 300 process_action SignUp#create 2020-03-28 14:35:59 56 process_action Home#index Measurement
  18. timestamp duration hook_name location 2020-03-28 14:35:59 101 process_action Home#index 2020-03-28

    14:35:59 28 process_action Home#index 2020-03-28 14:35:59 300 process_action SignUp#create 2020-03-28 14:35:59 56 process_action Home#index Primary Key
  19. timestamp duration hook_name location 2020-03-28 14:35:59 101 process_action Home#index 2020-03-28

    14:35:59 28 process_action Home#index 2020-03-28 14:35:59 300 process_action SignUp#create 2020-03-28 14:35:59 56 process_action Home#index Field
  20. timestamp duration hook_name location 2020-03-28 14:35:59 101 process_action Home#index 2020-03-28

    14:35:59 28 process_action Home#index 2020-03-28 14:35:59 300 process_action SignUp#create 2020-03-28 14:35:59 56 process_action Home#index Tag
  21. module ActiveMonitoring class Railtie < ::Rails::Engine config.after_initialize do Notifications.subscribe("process_action") do

    |payload| location = "#{payload[:controller]}##{payload[:action]}" InfluxDB::Client.write_point( "rails", timestamp: payload[:finish], tags: { hook_name: "process_action", location: location }, values: { duration: payload[:finish] - payload[:start] } ) end end end end Plugin
  22. { sql: 'SELECT "posts".* FROM "posts" WHERE id=1', name: "Post

    Load", connection_id: 70307250813140, connection: #SQLite3Adapter:0x00007f9f7a838850>, binds: [:WithCastValue:0x00007fe19d15dc00>], type_casted_binds: [11], statement_name: nil }
  23. { sql: 'SELECT "posts".* FROM "posts" WHERE id=1', name: "Post

    Load", connection_id: 70307250813140, connection: #SQLite3Adapter:0x00007f9f7a838850>, binds: [:WithCastValue:0x00007fe19d15dc00>], type_casted_binds: [11], statement_name: nil }
  24. SELECT "posts".* FROM "posts" WHERE id=1 SELECT "posts".* FROM "posts"

    WHERE id=2 SELECT "posts".* FROM "posts" WHERE id=x
  25. class Normalizer def initialize(query:) @query = query end def to_s

    query.squish! query.gsub!(/(\s(=|>|<|!=)\s)('[^']+'|[\$\+\-\w\.]+)/, '\1xxx') query.gsub!(/(\sIN\s)\([^\(\)]+\)/i, '\1(xxx)') query.gsub!(/(\sVALUES\s)\(.+\)/i, '\1(xxx)') query.gsub!(/(\s(LIKE||SIMILAR TO)\s)('[^']+')/i, '\1xxx') query.gsub!(/(\s(LIMIT|OFFSET)\s)(\d+)/i, '\1xxx') query end end
  26. { sql: "SELECT SQLITE_VERSION(*)", name: "SCHEMA", connection_id: 70307250813140, connection: #SQLite3Adapter:0x00007f9f7a838850>,

    binds: [:WithCastValue:0x00007fe19d15dc00>], type_casted_binds: [11], statement_name: nil }
  27. class Query TRACKED_SQL_COMMANDS = %w[SELECT INSERT UPDATE DELETE].freeze UNTRACKED_NAMES =

    %w[SCHEMA].freeze def track? query.start_with?(*TRACKED_SQL_COMMANDS) && !name.upcase.start_with?(*UNTRACKED_NAMES) end end
  28. { sql: 'SELECT "posts".* FROM "posts" WHERE id=1', name: "Post

    Load", connection_id: 70307250813140, connection: #SQLite3Adapter:0x00007f9f7a838850>, binds: [:WithCastValue:0x00007fe19d15dc00>], type_casted_binds: [11], statement_name: nil, request_id: ??? }
  29. redirect_to.action_controller halted_callback.action_controller unpermitted_parameters.action_controller process_middleware.action_dispatch render_template.action_view render_partial.action_view render_collection.action_view sql.active_record instantiation.active_record …

    write_fragment.action_controller read_fragment.action_controller expire_fragment.action_controller exist_fragment?.action_controller write_page.action_controller expire_page.action_controller start_processing.action_controller process_action.action_controller send_file.action_controller send_data.action_controller 51 Hooks
  30. module ActiveMonitoring class Engine < ::Rails::Engine config.after_initialize do Notifications.subscribe("start_processing") do

    |data| location = “#{payload[:controller]}##{payload[:action]}" Current.request_id = payload[:request_id] Current.location = location end end end end
  31. module ActiveMonitoring class Current class << self def request_id store[:request_id]

    end def request_id=(value) store[:request_id] = value end end end end
  32. module ActiveMonitoring class Railtie < ::Rails::Railtie config.after_initialize do Notifications.subscribe("sql") do

    |data| query = Query.new(query: data[:sql], name: data[:name]) if query.track? # ... end end end end end
  33. module ActiveMonitoring class Railtie < ::Rails::Railtie config.after_initialize do Notifications.subscribe("sql") do

    |data| # ... tags = { hook_name: "sql", request_id: Current.request_id, location: Current.location, query: query.normalized_query } end end end end
  34. module ActiveMonitoring class Railtie < ::Rails::Railtie config.after_initialize do Notifications.subscribe("sql") do

    |data| # ... tags = { hook_name: "sql", request_id: Current.request_id, location: Current.location, query: query.normalized_query } end end end end
  35. module ActiveMonitoring class Railtie < ::Rails::Railtie config.after_initialize do Notifications.subscribe("sql") do

    |data| # ... InfluxDB::Client.write_point( "rails", timestamp: Time.current, tags: tags, values: { duration: data[:finish] - data[:start] } ) end end end end
  36. timestamp duration hook_name location 2020-03-28 14:35:59 101 process_action Home#index 2020-03-28

    14:35:59 28 process_action Home#index 2020-03-28 14:35:59 300 process_action SignUp#create 2020-03-28 14:35:59 56 process_action Home#index
  37. timestamp duration hook_name location 2020-03-28 14:35:59 101 process_action Home#index 2020-03-28

    14:35:59 28 process_action Home#index 2020-03-28 14:35:59 300 process_action SignUp#create 2020-03-28 14:35:59 56 process_action SignUp#create 2020-03-28 14:35:59 120 process_action SignUp#create 2020-03-28 14:35:59 73 process_action Home#index 2020-03-28 14:35:59 48 process_action SignUp#create 2020-03-28 14:35:59 20 process_action SignUp#create 2020-03-28 14:35:59 800 process_action SignUp#create 2020-03-28 14:35:59 36 process_action Home#index
  38. response_time = [ 101, 28, 300, 42, 56 ] average

    = response_time.sum / response_time.length # 105 Average
  39. response_time = [ 28, 42, 56, 101, 300 ].sort median

    = response_time[response_time.length / 2] # 56 Median