notification_tracer 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aaec82476ba796bf8332bbc1583742a0f9bbfd6c
4
+ data.tar.gz: 4f32399aa750ab13688c62227575ba3de7723709
5
+ SHA512:
6
+ metadata.gz: 6ead73d748cdf8e7f2be2ebc0c0aff7fb2594283cd981683c2021758d4488c6de1ac86031b36e57d2c481950dd7b83f1146989a2e506b48eba19541028a21238
7
+ data.tar.gz: a1dc73b4e32448bc582543888a5aa14af4a4d7540e6895fbf90e0fe4f811396bee30efe372029f91894af63a18f65587c92746fa0094fce2302a440870c003b8
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .ruby-gemset
11
+ .ruby-version
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in notification_tracer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 David Feldman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # NotificationTracer
2
+
3
+ A convenient way to process ActiveSupport notifications together with the call stack.
4
+
5
+ ## Installation
6
+
7
+ Add `gem 'notification_tracer'` to your Gemfile.
8
+
9
+ ## Usage
10
+
11
+ The `ActiveSupport::Notifications` API allows a consumer to subscribe to specific notification events that match a provided pattern. The `NotificationTracer::Subscriber` wraps and streamlines this process, passing matching event data to a callback:
12
+
13
+ ```ruby
14
+ subscriber = NotificationTracer::Subscriber.new(
15
+ pattern: 'matches.notification',
16
+ callback: ->(**opts){ puts opts.inspect }
17
+ )
18
+ ```
19
+ **:pattern** can be either a `String` (exact matching) or a `Regexp` (pattern matching).
20
+
21
+ **:callback** must respond to `.call` with the following options:
22
+ - **:stack** ==> the cleaned Ruby callstack
23
+ - **:payload** ==> an event-specific data hash
24
+ - **:duration** ==> how long the event took
25
+ - **:event_id** ==> a unique id for this event
26
+ - **:event_name** ==> the full name of the event
27
+
28
+ `Subscriber` initialization also takes an optional parameter, **:cleaner**, for scrubbing the callstack. It is recommended to use an instance of `ActiveSupport::BacktraceCleaner` but any object with a `clean :: Array -> Array` method is acceptable.
29
+
30
+ You must explicitly call `.subscribe` on the `Subscriber` in order to start receiving events:
31
+ ```ruby
32
+ :005 > subscriber.subscribed?
33
+ => false
34
+ :006 > subscriber.subscribe
35
+ => #<NotificationTracer::Subscriber:0x007fb03b8b8628 @pattern="matches.notification", @callback=#<Proc:0x007fb03b8b8740@(irb):3 (lambda)>, @real_subscriber=#<ActiveSupport::Notifications::Fanout::Subscribers::Timed:0x007fb03b8836a8 ...>
36
+ :007 > subscriber.subscribed?
37
+ => true
38
+ :008 > 10.times.each{ subscriber.subscribe } # no harm to recall subscribe
39
+ => 10
40
+ :009 > subscriber.subscribed?
41
+ => true
42
+ :010 > subscriber.unsubscribe
43
+ => #<NotificationTracer::Subscriber:0x007fb03b8b8628 @pattern="matches.notification", @callback=#<Proc:0x007fb03b8b8740@(irb):3 (lambda)>, @real_subscriber=nil, ...>
44
+ :011 > subscriber.subscribed?
45
+ => false
46
+ ```
47
+
48
+ ### Rails Specific
49
+
50
+ `NotificationTracer::RailsSql` provides out-of-the-box logging of `sql.active_record` events:
51
+ ```ruby
52
+ tracer = NotificationTracer::RailsSql.new(
53
+ matcher: <a callable that takes a sql string and returns true or false>,
54
+ formatter: <a callable that merges :stack, :sql, :duration, and :uuid into a single output>,
55
+ logger: <a callable that records the output of the formatter>,
56
+ lines: <limits the stack trace to the first N lines, or nil for no limit>,
57
+ silence_rails_code: <true or false, includes framework code in the stack trace>
58
+ )
59
+ tracer.start # subscribes & enables logging
60
+ tracer.pause # disables logging
61
+ tracer.stop # unsubscribes & disables logging
62
+ ```
63
+ An example **:formatter** can be found in `NotificationTracer::SqlFormatter`. This implementation converts the SQL event data into a String suitable for a text logger. It takes an optional parameter, **:prefix**, which prepends a given String to all messages.
64
+
65
+ For convenience, `NotificationTracer.rails_sql` creates a `RailsSql` instance with a `SqlFormatter` formatter:
66
+ ```ruby
67
+ log_users_sql = NotificationTracer.rails_sql(
68
+ prefix: 'DEBUG 2847428',
69
+ logger: ->(msg){ Rails.logger.debug(msg) },
70
+ matcher: ->(sql){ sql =~ /users/ }
71
+ ); log_users_sql.start
72
+ ```
73
+
74
+ ## Development
75
+
76
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
77
+
78
+ ## Contributing
79
+
80
+ Bug reports and pull requests are welcome on GitHub at https://github.com/fledman/notification_tracer.
81
+
82
+ ## License
83
+
84
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
85
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "notification_tracer"
5
+
6
+ require "pry"
7
+ require "irb"
8
+
9
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ module NotificationTracer
2
+ class SubscriptionError < StandardError; end
3
+ end
@@ -0,0 +1,69 @@
1
+ module NotificationTracer
2
+ class RailsSql
3
+ attr_reader :enabled, :lines
4
+
5
+ def initialize(matcher:, logger:, formatter:,
6
+ lines: nil, silence_rails_code: true)
7
+ @enabled = false
8
+ @lines = Integer(lines) if lines
9
+ @matcher = matcher
10
+ @logger = logger
11
+ @formatter = formatter
12
+ @subscriber = make_subscriber(silence_rails_code: silence_rails_code)
13
+ end
14
+
15
+ def start
16
+ @enabled = true
17
+ subscriber.subscribe
18
+ self
19
+ end
20
+
21
+ def pause
22
+ @enabled = false
23
+ self
24
+ end
25
+
26
+ def stop
27
+ @enabled = false
28
+ subscriber.unsubscribe
29
+ self
30
+ end
31
+
32
+ def call(stack:, payload:, duration:, event_id:, event_name:)
33
+ return unless enabled
34
+ return unless matches?(payload)
35
+ stack = stack[0..(lines-1)] if lines
36
+ stack = stack.select{ |l| l && !l.empty? }
37
+ return if stack.empty?
38
+ data = formatter.call(stack: stack, sql: payload[:sql],
39
+ duration: duration, uuid: event_id)
40
+ logger.call(data) if data
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :matcher, :logger, :subscriber, :formatter
46
+
47
+ def pattern
48
+ 'sql.active_record'
49
+ end
50
+
51
+ def make_subscriber(silence_rails_code:)
52
+ cleaner = make_cleaner(silence_rails_code: silence_rails_code)
53
+ Subscriber.new(pattern: pattern, callback: self, cleaner: cleaner)
54
+ end
55
+
56
+ def make_cleaner(silence_rails_code:)
57
+ Rails::BacktraceCleaner.new.tap do |rbc|
58
+ rbc.remove_silencers! unless silence_rails_code
59
+ end
60
+ end
61
+
62
+ def matches?(payload)
63
+ return false if payload[:name] == 'SCHEMA'
64
+ return false if payload[:name] == 'CACHE'
65
+ matcher.call(payload[:sql])
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,28 @@
1
+ module NotificationTracer
2
+ class SqlFormatter
3
+ attr_reader :prefix
4
+
5
+ def initialize(prefix: nil)
6
+ @prefix = ensure_non_empty_string(prefix) if prefix
7
+ end
8
+
9
+ def call(stack:, sql:, duration:, uuid:)
10
+ message = "Matching Query"
11
+ message = "#{prefix} | #{message}" if prefix
12
+ message += " | #{duration} ms | ##{uuid}"
13
+ message += "\n ** SQL: " + sql.gsub("\n",'\n')
14
+ ([message] + stack).join("\n >>> ")
15
+ end
16
+
17
+ private
18
+
19
+ def ensure_non_empty_string(string)
20
+ if string.is_a?(String)
21
+ return string.freeze unless string.empty?
22
+ raise ArgumentError, "prefix should not be empty, use nil instead"
23
+ end
24
+ raise ArgumentError, "expected a String prefix, got: #{string.inspect}"
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,75 @@
1
+ module NotificationTracer
2
+ class Subscriber
3
+ attr_reader :pattern, :cleaner
4
+
5
+ def initialize(pattern:, callback:, cleaner: nil)
6
+ @pattern = pattern.freeze
7
+ @callback = callback
8
+ @cleaner = setup_cleaner(cleaner)
9
+ end
10
+
11
+ def subscribed?
12
+ !!real_subscriber && listening?
13
+ end
14
+
15
+ def subscribe(silent: false)
16
+ @real_subscriber = nil if real_subscriber && !listening?
17
+ @real_subscriber ||= notifier.subscribe(pattern) do |*args|
18
+ event = ActiveSupport::Notifications::Event.new(*args)
19
+ trace(event: event, stack: caller)
20
+ end
21
+ subscription_error('subscribe') if !silent && !subscribed?
22
+ self
23
+ end
24
+
25
+ def unsubscribe(silent: false)
26
+ if real_subscriber
27
+ notifier.unsubscribe(real_subscriber)
28
+ if listening?
29
+ subscription_error('unsubscribe') if !silent
30
+ else
31
+ @real_subscriber = nil
32
+ end
33
+ end
34
+ self
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :real_subscriber, :callback
40
+
41
+ def notifier
42
+ ActiveSupport::Notifications
43
+ end
44
+
45
+ def subscription_error(type)
46
+ raise SubscriptionError, "#{type} failed for #{pattern}"
47
+ end
48
+
49
+ def setup_cleaner(input)
50
+ if input.nil?
51
+ ActiveSupport::BacktraceCleaner.new
52
+ elsif input.respond_to?(:clean)
53
+ input
54
+ else
55
+ raise ArgumentError, "cleaner must respond to clean: #{input.inspect}"
56
+ end
57
+ end
58
+
59
+ def listening?
60
+ name = pattern.is_a?(Regexp) ? pattern.source : pattern
61
+ notifier.notifier.listeners_for(name).include?(real_subscriber)
62
+ end
63
+
64
+ def trace(event:, stack:)
65
+ callback.call(
66
+ stack: cleaner.clean(stack),
67
+ payload: event.payload,
68
+ duration: event.duration,
69
+ event_id: event.transaction_id,
70
+ event_name: event.name
71
+ )
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ module NotificationTracer
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,16 @@
1
+ require "notification_tracer/version"
2
+ require "notification_tracer/errors"
3
+ require "active_support/notifications"
4
+ require "active_support/backtrace_cleaner"
5
+ require "notification_tracer/subscriber"
6
+ require "notification_tracer/rails_sql"
7
+ require "notification_tracer/sql_formatter"
8
+
9
+ module NotificationTracer
10
+ extend self
11
+
12
+ def rails_sql(prefix: nil, **pass)
13
+ RailsSql.new(formatter: SqlFormatter.new(prefix: prefix), **pass)
14
+ end
15
+
16
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'notification_tracer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "notification_tracer"
8
+ spec.version = NotificationTracer::VERSION
9
+ spec.authors = ["David Feldman"]
10
+ spec.email = ["dbfeldman@gmail.com"]
11
+
12
+ spec.summary = "trace ActiveSupport notifications"
13
+ spec.description = "A convenient way to process ActiveSupport notifications together with the call stack."
14
+ spec.homepage = "https://github.com/fledman/notification_tracer"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_runtime_dependency "activesupport", ">= 4.0", "< 5.1"
21
+
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec", "~> 3.0"
24
+ spec.add_development_dependency "pry", "~> 0.10"
25
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: notification_tracer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - David Feldman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-11-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.1'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.1'
33
+ - !ruby/object:Gem::Dependency
34
+ name: rake
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '10.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '10.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: pry
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.10'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.10'
75
+ description: A convenient way to process ActiveSupport notifications together with
76
+ the call stack.
77
+ email:
78
+ - dbfeldman@gmail.com
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - ".gitignore"
84
+ - ".rspec"
85
+ - Gemfile
86
+ - LICENSE.txt
87
+ - README.md
88
+ - Rakefile
89
+ - bin/console
90
+ - bin/setup
91
+ - lib/notification_tracer.rb
92
+ - lib/notification_tracer/errors.rb
93
+ - lib/notification_tracer/rails_sql.rb
94
+ - lib/notification_tracer/sql_formatter.rb
95
+ - lib/notification_tracer/subscriber.rb
96
+ - lib/notification_tracer/version.rb
97
+ - notification_tracer.gemspec
98
+ homepage: https://github.com/fledman/notification_tracer
99
+ licenses:
100
+ - MIT
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 2.4.8
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: trace ActiveSupport notifications
122
+ test_files: []