Skip to content

Implement render_template & render_partial subscriptions #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 2, 2018
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
--format progress
--color
--order rand
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ InfluxDB::Rails.configure do |config|
# config.series_name_for_controller_runtimes = "rails.controller"
# config.series_name_for_view_runtimes = "rails.view"
# config.series_name_for_db_runtimes = "rails.db"
# config.series_name_for_render_template = "rails.render_template"
# config.series_name_for_render_partial = "rails.render_partial"
# config.series_name_for_render_collection = "rails.render_collection"
# config.series_name_for_exceptions = "rails.exceptions"
# config.series_name_for_instrumentation = "instrumentation"

Expand All @@ -68,8 +71,17 @@ To see all default values, take a look into `InfluxDB::Rails::Configuration::DEF
defined in `lib/influxdb/rails/configuration.rb`

Out of the box, you'll automatically get reporting of your controller,
view, and db runtimes for each request. You can also call through to the
underlying `InfluxDB::Client` object to write arbitrary data like this:
view, and db runtimes and rendering of template, partial and collection for each request.

It is possible to disable the rendering series by setting the series_name to nil.

```ruby
# config.series_name_for_render_template = nil
# config.series_name_for_render_partial = nil
# config.series_name_for_render_collection = nil
```

You can also call through to the underlying `InfluxDB::Client` object to write arbitrary data like this:

``` ruby
InfluxDB::Rails.client.write_point "events",
Expand Down
48 changes: 3 additions & 45 deletions lib/influxdb-rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
require "net/https"
require "rubygems"
require "socket"
require "influxdb/rails/middleware/render_subscriber"
require "influxdb/rails/middleware/request_subscriber"
require "influxdb/rails/version"
require "influxdb/rails/logger"
require "influxdb/rails/exception_presenter"
Expand Down Expand Up @@ -84,55 +86,11 @@ def report_exception(ex, env = {})
end
alias transmit report_exception

def handle_action_controller_metrics(_name, start, finish, _id, payload)
tags = configuration.tags_middleware.call({
status: payload[:status],
format: payload[:format],
http_method: payload[:method],
method: "#{payload[:controller]}##{payload[:action]}",
server: Socket.gethostname,
app_name: configuration.application_name,
}.reject { |_, value| value.nil? })

ts = convert_timestamp(finish.utc)

begin
{
configuration.series_name_for_controller_runtimes => ((finish - start) * 1000).ceil,
configuration.series_name_for_view_runtimes => (payload[:view_runtime] || 0).ceil,
configuration.series_name_for_db_runtimes => (payload[:db_runtime] || 0).ceil,
}.each do |series_name, value|
client.write_point series_name, values: { value: value }, tags: tags, timestamp: ts
end
rescue StandardError => e
log :error, "[InfluxDB::Rails] Unable to write points: #{e.message}"
end
end

# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize

TIMESTAMP_CONVERSIONS = {
"ns" => 1e9.to_r,
nil => 1e9.to_r,
"u" => 1e6.to_r,
"ms" => 1e3.to_r,
"s" => 1.to_r,
"m" => 1.to_r / 60,
"h" => 1.to_r / 60 / 60,
}.freeze
private_constant :TIMESTAMP_CONVERSIONS

def convert_timestamp(time)
conv = TIMESTAMP_CONVERSIONS.fetch(configuration.time_precision) do
raise "Invalid time precision: #{configuration.time_precision}"
end

(time.to_r * conv).to_i
end

def current_timestamp
convert_timestamp(Time.now.utc)
InfluxDB.convert_timestamp(Time.now.utc, configuration.time_precision)
end

def ignorable_exception?(ex)
Expand Down
9 changes: 9 additions & 0 deletions lib/influxdb/rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class Configuration # rubocop:disable Style/Documentation
attr_accessor :series_name_for_db_runtimes
attr_accessor :series_name_for_exceptions
attr_accessor :series_name_for_instrumentation
attr_accessor :series_name_for_render_template
attr_accessor :series_name_for_render_partial
attr_accessor :series_name_for_render_collection

attr_accessor :tags_middleware
attr_accessor :rails_app_name
Expand Down Expand Up @@ -66,6 +69,9 @@ class Configuration # rubocop:disable Style/Documentation
series_name_for_db_runtimes: "rails.db".freeze,
series_name_for_exceptions: "rails.exceptions".freeze,
series_name_for_instrumentation: "instrumentation".freeze,
series_name_for_render_template: "rails.render_template".freeze,
series_name_for_render_partial: "rails.render_partial".freeze,
series_name_for_render_collection: "rails.render_collection".freeze,

tags_middleware: ->(tags) { tags },
rails_app_name: nil,
Expand Down Expand Up @@ -126,6 +132,9 @@ def initialize
@series_name_for_db_runtimes = DEFAULTS[:series_name_for_db_runtimes]
@series_name_for_exceptions = DEFAULTS[:series_name_for_exceptions]
@series_name_for_instrumentation = DEFAULTS[:series_name_for_instrumentation]
@series_name_for_render_template = DEFAULTS[:series_name_for_render_template]
@series_name_for_render_partial = DEFAULTS[:series_name_for_render_partial]
@series_name_for_render_collection = DEFAULTS[:series_name_for_render_collection]

@tags_middleware = DEFAULTS[:tags_middleware]
@rails_app_name = DEFAULTS[:rails_app_name]
Expand Down
50 changes: 50 additions & 0 deletions lib/influxdb/rails/middleware/render_subscriber.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require "influxdb/rails/middleware/subscriber"

module InfluxDB
module Rails
module Middleware
class RenderSubscriber < Subscriber
attr_reader :series_name

def initialize(configuration, series_name)
@series_name = series_name
super(configuration)
end

def call(_name, started, finished, _unique_id, payload)
return unless enabled?

value = ((finished - started) * 1000).ceil
ts = InfluxDB.convert_timestamp(finished.utc, configuration.time_precision)
begin
InfluxDB::Rails.client.write_point series_name, values: { value: value }, tags: tags(payload), timestamp: ts
rescue StandardError => e
log :error, "[InfluxDB::Rails] Unable to write points: #{e.message}"
end
end

private

def enabled?
super && series_name.present?
end

def location
[
Thread.current[:_influxdb_rails_controller],
Thread.current[:_influxdb_rails_action],
].reject(&:blank?).join("#")
end

def tags(payload)
{
location: location,
filename: payload[:identifier],
count: payload[:count],
cache_hits: payload[:cache_hits],
}.reject { |_, value| value.blank? }
end
end
end
end
end
47 changes: 47 additions & 0 deletions lib/influxdb/rails/middleware/request_subscriber.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require "influxdb/rails/middleware/subscriber"

module InfluxDB
module Rails
module Middleware
class RequestSubscriber < Subscriber
def call(_name, start, finish, _id, payload)
return unless enabled?

ts = InfluxDB.convert_timestamp(finish.utc, configuration.time_precision)
begin
series(payload, start, finish).each do |series_name, value|
InfluxDB::Rails.client.write_point series_name, values: { value: value }, tags: tags(payload), timestamp: ts
end
rescue StandardError => e
log :error, "[InfluxDB::Rails] Unable to write points: #{e.message}"
ensure
Thread.current[:_influxdb_rails_controller] = nil
Thread.current[:_influxdb_rails_action] = nil
end
end

private

def series(payload, start, finish)
{
configuration.series_name_for_controller_runtimes => ((finish - start) * 1000).ceil,
configuration.series_name_for_view_runtimes => (payload[:view_runtime] || 0).ceil,
configuration.series_name_for_db_runtimes => (payload[:db_runtime] || 0).ceil,
}
end

def tags(payload)
configuration.tags_middleware.call(
{
method: "#{payload[:controller]}##{payload[:action]}",
status: payload[:status],
format: payload[:format],
http_method: payload[:method],
server: Socket.gethostname,
app_name: configuration.application_name,
}.reject { |_, value| value.nil? })
end
end
end
end
end
24 changes: 24 additions & 0 deletions lib/influxdb/rails/middleware/subscriber.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require "influxdb/rails/logger"

module InfluxDB
module Rails
module Middleware
class Subscriber
include InfluxDB::Rails::Logger

attr_reader :configuration

def initialize(configuration)
@configuration = configuration
end

private

def enabled?
configuration.instrumentation_enabled? &&
!configuration.ignore_current_environment?
end
end
end
end
end
26 changes: 15 additions & 11 deletions lib/influxdb/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,23 @@ class Railtie < ::Rails::Railtie # :nodoc:
::ActionDispatch::DebugExceptions.prepend InfluxDB::Rails::Middleware::HijackRenderException

if defined?(ActiveSupport::Notifications)
listen = lambda do |name, start, finish, id, payload|
c = InfluxDB::Rails.configuration

if c.instrumentation_enabled? && !c.ignore_current_environment?
begin
InfluxDB::Rails.handle_action_controller_metrics(name, start, finish, id, payload)
rescue StandardError => e
c.logger.error "[InfluxDB::Rails] Failed writing points to InfluxDB: #{e.message}"
end
end
ActiveSupport::Notifications.subscribe "start_processing.action_controller" do |name, start, finish, id, payload|
Thread.current[:_influxdb_rails_controller] = payload[:controller]
Thread.current[:_influxdb_rails_action] = payload[:action]
end

ActiveSupport::Notifications.subscribe "process_action.action_controller", &listen
config = InfluxDB::Rails.configuration
request_subsriber = Middleware::RequestSubscriber.new(config)
ActiveSupport::Notifications.subscribe "process_action.action_controller", request_subsriber

render_template_subscriber = Middleware::RenderSubscriber.new(config, config.series_name_for_render_template)
ActiveSupport::Notifications.subscribe "render_template.action_view", render_template_subscriber

render_partial_subscriber = Middleware::RenderSubscriber.new(config, config.series_name_for_render_partial)
ActiveSupport::Notifications.subscribe "render_partial.action_view", render_partial_subscriber

render_collection_subscriber = Middleware::RenderSubscriber.new(config, config.series_name_for_render_collection)
ActiveSupport::Notifications.subscribe "render_collection.action_view", render_collection_subscriber
end
end
end
Expand Down
17 changes: 6 additions & 11 deletions spec/integration/metrics_spec.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
require File.expand_path(File.dirname(__FILE__) + "/integration_helper")

RSpec.describe "collecting metrics through ActiveSupport::Notifications", type: :request do
RSpec.describe WidgetsController, type: :controller do
render_views

before do
InfluxDB::Rails.configure do |config|
config.ignored_environments = %w[development]
end
allow_any_instance_of(InfluxDB::Rails::Configuration).to receive(:ignored_environments).and_return(%w[development])
end

describe "in a normal request" do
it "should attempt to handle ActionController metrics" do
expect(InfluxDB::Rails).to receive(:handle_action_controller_metrics).once
get "/widgets"
end

it "should result in attempts to write metrics via the client" do
expect(InfluxDB::Rails.client).to receive(:write_point).exactly(3).times
get "/widgets"
expect(InfluxDB::Rails.client).to receive(:write_point).exactly(6).times
get :index
end
end
end
8 changes: 4 additions & 4 deletions spec/support/rails4/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
app.config.session_store :cookie_store, key: "_myapp_session"
app.config.active_support.deprecation = :log
app.config.eager_load = false
app.config.root = File.dirname(__FILE__)
app.config.root = __dir__
Rails.backtrace_cleaner.remove_silencers!
app.initialize!

Expand All @@ -18,9 +18,9 @@

class ApplicationController < ActionController::Base; end
class WidgetsController < ApplicationController
def index
render nothing: true
end
prepend_view_path File.join(__dir__, "..", "views")

def index; end

def new
1 / 0
Expand Down
8 changes: 4 additions & 4 deletions spec/support/rails5/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
app.config.session_store :cookie_store, key: "_myapp_session"
app.config.active_support.deprecation = :log
app.config.eager_load = false
app.config.root = File.dirname(__FILE__)
app.config.root = __dir__
Rails.backtrace_cleaner.remove_silencers!
app.initialize!

Expand All @@ -19,9 +19,9 @@

class ApplicationController < ActionController::Base; end
class WidgetsController < ApplicationController
def index
head 200
end
prepend_view_path File.join(__dir__, "..", "views")

def index; end

def new
1 / 0
Expand Down
1 change: 1 addition & 0 deletions spec/support/views/widgets/_item.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div>item</div
5 changes: 5 additions & 0 deletions spec/support/views/widgets/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<h1>Index page</h1>
<div>
<%= render partial: 'item' %>
<%= render partial: 'item', collection: [1,2,3] %>
</div>
Loading