Skip to content

Implement SQL Subscriber #55

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 1 commit into from
Dec 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ AllCops:
- 'bin/**/*'
- 'smoke/**/*'
- 'Gemfile'
- 'vendor/bundle/**/*'
DisplayCopNames: true
StyleGuideCopsOnly: false
TargetRubyVersion: 2.3
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ InfluxDB::Rails.configure do |config|
# 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_sql = nil
# config.series_name_for_exceptions = "rails.exceptions"
# config.series_name_for_instrumentation = "instrumentation"

Expand All @@ -72,6 +73,10 @@ defined in `lib/influxdb/rails/configuration.rb`

Out of the box, you'll automatically get reporting of your controller,
view, and db runtimes and rendering of template, partial and collection for each request.
Reporting of SQL queries is disabled by default because it is still in experimental mode
and currently requires String parsing which might cause performance issues on query
intensive applications. You can enable it by setting the `series_name_for_sql`
configuration.

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

Expand Down
2 changes: 2 additions & 0 deletions influxdb-rails.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency "influxdb", "~> 0.6", ">= 0.6.4"
spec.add_runtime_dependency "railties", ">= 4.2"

spec.add_development_dependency "activerecord"
spec.add_development_dependency "bundler", ">= 1.0.0"
spec.add_development_dependency "fakeweb"
spec.add_development_dependency "rake"
spec.add_development_dependency "rdoc"
spec.add_development_dependency "rspec"
spec.add_development_dependency "rspec-rails", ">= 3.0.0"
spec.add_development_dependency "rubocop", "~> 0.60.0"
spec.add_development_dependency "sqlite3"
spec.add_development_dependency "tzinfo"
end
2 changes: 2 additions & 0 deletions lib/influxdb-rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require "socket"
require "influxdb/rails/middleware/render_subscriber"
require "influxdb/rails/middleware/request_subscriber"
require "influxdb/rails/middleware/sql_subscriber"
require "influxdb/rails/sql/query"
require "influxdb/rails/version"
require "influxdb/rails/logger"
require "influxdb/rails/exception_presenter"
Expand Down
3 changes: 3 additions & 0 deletions lib/influxdb/rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Configuration # rubocop:disable Style/Documentation
attr_accessor :series_name_for_render_template
attr_accessor :series_name_for_render_partial
attr_accessor :series_name_for_render_collection
attr_accessor :series_name_for_sql

attr_accessor :tags_middleware
attr_accessor :rails_app_name
Expand Down Expand Up @@ -72,6 +73,7 @@ class Configuration # rubocop:disable Style/Documentation
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,
series_name_for_sql: nil,

tags_middleware: ->(tags) { tags },
rails_app_name: nil,
Expand Down Expand Up @@ -135,6 +137,7 @@ def initialize
@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]
@series_name_for_sql = DEFAULTS[:series_name_for_sql]

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

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

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

# rubocop:disable Metrics/MethodLength

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

# rubocop:enable Metrics/MethodLength

class RenderSubscriber < SimpleSubscriber # :nodoc:
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,
Expand Down
46 changes: 46 additions & 0 deletions lib/influxdb/rails/middleware/simple_subscriber.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require "influxdb/rails/middleware/subscriber"

module InfluxDB
module Rails
module Middleware
# Subscriber acts as base class for different *Subscriber classes,
# which are intended as ActiveSupport::Notifications.subscribe
# consumers.
class SimpleSubscriber < Subscriber
attr_reader :series_name

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

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

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

private

def values(started, finished, _payload)
{ value: ((finished - started) * 1000).ceil }
end

def timestamp(finished)
InfluxDB.convert_timestamp(finished.utc, configuration.time_precision)
end

def enabled?
super && series_name.present?
end
end
end
end
end
32 changes: 32 additions & 0 deletions lib/influxdb/rails/middleware/sql_subscriber.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require "influxdb/rails/middleware/simple_subscriber"
require "influxdb/rails/sql/query"

module InfluxDB
module Rails
module Middleware
class SqlSubscriber < SimpleSubscriber # :nodoc:
def call(_name, started, finished, _unique_id, payload)
return unless InfluxDB::Rails::Sql::Query.new(payload).track?

super
end

private

def values(started, finished, payload)
super.merge(sql: InfluxDB::Rails::Sql::Normalizer.new(payload[:sql]).perform)
end

def tags(payload)
query = InfluxDB::Rails::Sql::Query.new(payload)
{
location: location,
operation: query.operation,
class_name: query.class_name,
name: query.name,
}
end
end
end
end
end
7 changes: 7 additions & 0 deletions lib/influxdb/rails/middleware/subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ def enabled?
configuration.instrumentation_enabled? &&
!configuration.ignore_current_environment?
end

def location
[
Thread.current[:_influxdb_rails_controller],
Thread.current[:_influxdb_rails_action],
].reject(&:blank?).join("#")
end
end
end
end
Expand Down
5 changes: 4 additions & 1 deletion lib/influxdb/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Railtie < ::Rails::Railtie # :nodoc:
app.config.middleware.insert 0, InfluxDB::Rails::Rack
end

config.after_initialize do
config.after_initialize do # rubocop:disable Metrics/BlockLength
InfluxDB::Rails.configure(true, &:load_rails_defaults)

ActiveSupport.on_load(:action_controller) do
Expand Down Expand Up @@ -40,6 +40,9 @@ class Railtie < ::Rails::Railtie # :nodoc:

collections = Middleware::RenderSubscriber.new(c, c.series_name_for_render_collection)
ActiveSupport::Notifications.subscribe "render_collection.action_view", collections

sql = Middleware::SqlSubscriber.new(c, c.series_name_for_sql)
ActiveSupport::Notifications.subscribe "sql.active_record", sql
end
end
end
Expand Down
27 changes: 27 additions & 0 deletions lib/influxdb/rails/sql/normalizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module InfluxDB
module Rails
module Sql
class Normalizer # :nodoc:
def initialize(query)
@query = query.dup
end

def perform
query.squish!
query.gsub!(/(\s(=|>|<|>=|<=|<>|!=)\s)('[^']+'|[\$\+\-\w\.]+)/, '\1xxx')
query.gsub!(/(\sIN\s)\([^\(\)]+\)/i, '\1(xxx)')
regex = /(\sBETWEEN\s)('[^']+'|[\+\-\w\.]+)(\sAND\s)('[^']+'|[\+\-\w\.]+)/i
query.gsub!(regex, '\1xxx\3xxx')
query.gsub!(/(\sVALUES\s)\(.+\)/i, '\1(xxx)')
query.gsub!(/(\s(LIKE|ILIKE|SIMILAR TO|NOT SIMILAR TO)\s)('[^']+')/i, '\1xxx')
query.gsub!(/(\s(LIMIT|OFFSET)\s)(\d+)/i, '\1xxx')
query
end

private

attr_reader :query
end
end
end
end
30 changes: 30 additions & 0 deletions lib/influxdb/rails/sql/query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "influxdb/rails/sql/normalizer"

module InfluxDB
module Rails
module Sql
class Query # :nodoc:
attr_reader :query, :name

TRACKED_SQL_COMMANDS = %w[SELECT INSERT UPDATE DELETE].freeze

def initialize(payload)
@query = payload[:sql].to_s.dup
@name = payload[:name].to_s.dup
end

def operation
query.split.first.upcase
end

def class_name
name.split.first
end

def track?
@track ||= query.start_with?(*TRACKED_SQL_COMMANDS)
end
end
end
end
end
12 changes: 12 additions & 0 deletions spec/integration/metrics_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,17 @@
expect(InfluxDB::Rails.client).to receive(:write_point).exactly(6).times
get :index
end

context "with sql reports enabled" do
before do
allow_any_instance_of(InfluxDB::Rails::Middleware::SqlSubscriber).to receive(:series_name).and_return("rails.sql")
get :index # to not count ActiveRecord initialization
end

it "should result in attempts to write metrics via the client" do
expect(InfluxDB::Rails.client).to receive(:write_point).exactly(7).times
get :index
end
end
end
end
16 changes: 15 additions & 1 deletion spec/support/rails4/app.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require "action_controller/railtie"
require "active_record"

app = Class.new(Rails::Application)
app.config.secret_key_base = "1234567890abcdef1234567890abcdef"
app.config.secret_token = "1234567890abcdef1234567890abcdef"
app.config.session_store :cookie_store, key: "_myapp_session"
app.config.active_support.deprecation = :log
Expand All @@ -16,11 +18,23 @@
InfluxDB::Rails.configure do |config|
end

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define do
create_table :widgets, force: true do |t|
t.string :title

t.timestamps
end
end

class Widget < ActiveRecord::Base; end
class ApplicationController < ActionController::Base; end
class WidgetsController < ApplicationController
prepend_view_path File.join(__dir__, "..", "views")

def index; end
def index
Widget.create!(title: "test")
end

def new
1 / 0
Expand Down
15 changes: 14 additions & 1 deletion spec/support/rails5/app.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "action_controller/railtie"
require "active_record"

app = Class.new(Rails::Application)
app.config.secret_key_base = "1234567890abcdef1234567890abcdef"
Expand All @@ -17,11 +18,23 @@
InfluxDB::Rails.configure do |config|
end

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Schema.define do
create_table :widgets, force: true do |t|
t.string :title

t.timestamps
end
end

class Widget < ActiveRecord::Base; end
class ApplicationController < ActionController::Base; end
class WidgetsController < ApplicationController
prepend_view_path File.join(__dir__, "..", "views")

def index; end
def index
Widget.create!(title: "test")
end

def new
1 / 0
Expand Down
Loading