rails-instrumentation 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +42 -0
- data/Appraisals +19 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +152 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/rails/instrumentation.rb +60 -0
- data/lib/rails/instrumentation/patch.rb +42 -0
- data/lib/rails/instrumentation/subscriber.rb +41 -0
- data/lib/rails/instrumentation/subscribers/action_cable_subscriber.rb +65 -0
- data/lib/rails/instrumentation/subscribers/action_controller_subscriber.rb +158 -0
- data/lib/rails/instrumentation/subscribers/action_mailer_subscriber.rb +59 -0
- data/lib/rails/instrumentation/subscribers/action_view_subscriber.rb +44 -0
- data/lib/rails/instrumentation/subscribers/active_job_subscriber.rb +54 -0
- data/lib/rails/instrumentation/subscribers/active_record_subscriber.rb +39 -0
- data/lib/rails/instrumentation/subscribers/active_storage_subscriber.rb +87 -0
- data/lib/rails/instrumentation/subscribers/active_support_subscriber.rb +70 -0
- data/lib/rails/instrumentation/utils.rb +47 -0
- data/lib/rails/instrumentation/version.rb +5 -0
- data/rails-instrumentation.gemspec +32 -0
- metadata +191 -0
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rails/instrumentation"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rails/instrumentation/version'
|
2
|
+
require 'rails/instrumentation/patch'
|
3
|
+
require 'rails/instrumentation/subscriber'
|
4
|
+
require 'rails/instrumentation/subscribers/action_controller_subscriber'
|
5
|
+
require 'rails/instrumentation/subscribers/action_view_subscriber'
|
6
|
+
require 'rails/instrumentation/subscribers/active_record_subscriber'
|
7
|
+
require 'rails/instrumentation/subscribers/active_support_subscriber'
|
8
|
+
require 'rails/instrumentation/subscribers/action_mailer_subscriber'
|
9
|
+
require 'rails/instrumentation/subscribers/active_job_subscriber'
|
10
|
+
require 'rails/instrumentation/subscribers/action_cable_subscriber'
|
11
|
+
require 'rails/instrumentation/subscribers/active_storage_subscriber'
|
12
|
+
require 'rails/instrumentation/utils'
|
13
|
+
|
14
|
+
require 'opentracing'
|
15
|
+
|
16
|
+
module Rails
|
17
|
+
module Instrumentation
|
18
|
+
class Error < StandardError; end
|
19
|
+
|
20
|
+
TAGS = {
|
21
|
+
'component' => 'ruby-rails',
|
22
|
+
'instrumentation.version' => Rails::Instrumentation::VERSION
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
def self.instrument(tracer: OpenTracing.global_tracer,
|
26
|
+
exclude_events: [])
|
27
|
+
@tracer = tracer
|
28
|
+
|
29
|
+
add_subscribers(exclude_events: exclude_events)
|
30
|
+
Patch.patch_process_action
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.tracer
|
34
|
+
@tracer
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.add_subscribers(exclude_events: [])
|
38
|
+
ActiveRecordSubscriber.subscribe(exclude_events: exclude_events)
|
39
|
+
ActionControllerSubscriber.subscribe(exclude_events: exclude_events)
|
40
|
+
ActionViewSubscriber.subscribe(exclude_events: exclude_events)
|
41
|
+
ActiveSupportSubscriber.subscribe(exclude_events: exclude_events)
|
42
|
+
ActionMailerSubscriber.subscribe(exclude_events: exclude_events)
|
43
|
+
ActiveJobSubscriber.subscribe(exclude_events: exclude_events)
|
44
|
+
ActionCableSubscriber.subscribe(exclude_events: exclude_events)
|
45
|
+
ActiveStorageSubscriber.subscribe(exclude_events: exclude_events)
|
46
|
+
end
|
47
|
+
private_class_method :add_subscribers
|
48
|
+
|
49
|
+
def self.uninstrument
|
50
|
+
ActiveRecordSubscriber.unsubscribe
|
51
|
+
ActionControllerSubscriber.unsubscribe
|
52
|
+
ActionViewSubscriber.unsubscribe
|
53
|
+
ActiveSupportSubscriber.unsubscribe
|
54
|
+
ActionMailerSubscriber.unsubscribe
|
55
|
+
ActiveJobSubscriber.unsubscribe
|
56
|
+
ActionCableSubscriber.unsubscribe
|
57
|
+
ActiveStorageSubscriber.unsubscribe
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Rails
|
2
|
+
module Instrumentation
|
3
|
+
module Patch
|
4
|
+
def self.patch_process_action(klass: ::ActionController::Instrumentation)
|
5
|
+
klass.class_eval do
|
6
|
+
alias_method :process_action_original, :process_action
|
7
|
+
|
8
|
+
def process_action(method_name, *args)
|
9
|
+
# this naming scheme 'method.class' is how we ensure that the notification in the
|
10
|
+
# subscriber is the same one
|
11
|
+
name = "#{method_name}.#{self.class.name}"
|
12
|
+
puts ::Rails::Instrumentation.tracer
|
13
|
+
scope = ::Rails::Instrumentation.tracer.start_active_span(name)
|
14
|
+
|
15
|
+
# skip adding tags here. Getting the complete set of information is
|
16
|
+
# easiest in the notification
|
17
|
+
|
18
|
+
process_action_original(method_name, *args)
|
19
|
+
rescue Error => error
|
20
|
+
if scope
|
21
|
+
scope.span.set_tag('error', true)
|
22
|
+
scope.span.log_kv(key: 'message', value: error.message)
|
23
|
+
end
|
24
|
+
|
25
|
+
raise
|
26
|
+
ensure
|
27
|
+
scope.close
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.restore_process_action(klass: ::ActionController::Instrumentation)
|
33
|
+
puts klass.respond_to? :process_action_original, true
|
34
|
+
klass.class_eval do
|
35
|
+
remove_method :process_action
|
36
|
+
alias_method :process_action, :process_action_original
|
37
|
+
remove_method :process_action_original
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rails
|
2
|
+
module Instrumentation
|
3
|
+
module Subscriber
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def subscribe(exclude_events: [])
|
10
|
+
@subscriber_mutex = Mutex.new if @subscriber_mutex.nil?
|
11
|
+
|
12
|
+
# clear
|
13
|
+
unsubscribe
|
14
|
+
@subscribers = []
|
15
|
+
|
16
|
+
@subscriber_mutex.synchronize do
|
17
|
+
self::EVENTS.each do |event_name|
|
18
|
+
full_name = "#{event_name}.#{self::EVENT_NAMESPACE}"
|
19
|
+
|
20
|
+
next if exclude_events.include? full_name
|
21
|
+
|
22
|
+
@subscribers << Utils.register_subscriber(full_name: full_name,
|
23
|
+
event_name: event_name,
|
24
|
+
handler_module: self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def unsubscribe
|
30
|
+
return if @subscribers.nil? || @subscriber_mutex.nil?
|
31
|
+
|
32
|
+
@subscriber_mutex.synchronize do
|
33
|
+
@subscribers.each do |subscriber|
|
34
|
+
::ActiveSupport::Notifications.unsubscribe(subscriber)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Rails
|
2
|
+
module Instrumentation
|
3
|
+
module ActionCableSubscriber
|
4
|
+
include Subscriber
|
5
|
+
|
6
|
+
EVENT_NAMESPACE = 'action_cable'.freeze
|
7
|
+
|
8
|
+
EVENTS = %w[
|
9
|
+
perform_action
|
10
|
+
transmit
|
11
|
+
transmit_subscription_confirmation
|
12
|
+
transmit_subscription_rejection
|
13
|
+
broadcast
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def perform_action(event)
|
18
|
+
tags = {
|
19
|
+
'channel_class' => event.payload[:channel_class],
|
20
|
+
'action' => event.payload[:action],
|
21
|
+
'data' => event.payload[:data]
|
22
|
+
}
|
23
|
+
|
24
|
+
Utils.trace_notification(event: event, tags: tags)
|
25
|
+
end
|
26
|
+
|
27
|
+
def transmit(event)
|
28
|
+
tags = {
|
29
|
+
'channel_class' => event.payload[:channel_class],
|
30
|
+
'data' => event.payload[:data],
|
31
|
+
'via' => event.payload[:via]
|
32
|
+
}
|
33
|
+
|
34
|
+
Utils.trace_notification(event: event, tags: tags)
|
35
|
+
end
|
36
|
+
|
37
|
+
def transmit_subscription_confirmation(event)
|
38
|
+
tags = {
|
39
|
+
'channel_class' => event.payload[:channel_class]
|
40
|
+
}
|
41
|
+
|
42
|
+
Utils.trace_notification(event: event, tags: tags)
|
43
|
+
end
|
44
|
+
|
45
|
+
def transmit_subscription_rejection(event)
|
46
|
+
tags = {
|
47
|
+
'channel_class' => event.payload[:channel_class]
|
48
|
+
}
|
49
|
+
|
50
|
+
Utils.trace_notification(event: event, tags: tags)
|
51
|
+
end
|
52
|
+
|
53
|
+
def broadcast(event)
|
54
|
+
tags = {
|
55
|
+
'broadcasting' => event.payload[:broadcasting],
|
56
|
+
'message' => event.payload[:message],
|
57
|
+
'coder' => event.payload[:coder]
|
58
|
+
}
|
59
|
+
|
60
|
+
Utils.trace_notification(event: event, tags: tags)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module Rails
|
2
|
+
module Instrumentation
|
3
|
+
module ActionControllerSubscriber
|
4
|
+
include Subscriber
|
5
|
+
|
6
|
+
EVENT_NAMESPACE = 'action_controller'.freeze
|
7
|
+
|
8
|
+
EVENTS = %w[
|
9
|
+
write_fragment
|
10
|
+
read_fragment
|
11
|
+
expire_fragment
|
12
|
+
exist_fragment?
|
13
|
+
write_page
|
14
|
+
expire_page
|
15
|
+
start_processing
|
16
|
+
process_action
|
17
|
+
send_file
|
18
|
+
send_data
|
19
|
+
redirect_to
|
20
|
+
halted_callback
|
21
|
+
unpermitted_parameters
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def write_fragment(event)
|
26
|
+
tags = {
|
27
|
+
'key.write' => event.payload[:key]
|
28
|
+
}
|
29
|
+
|
30
|
+
Utils.trace_notification(event: event, tags: tags)
|
31
|
+
end
|
32
|
+
|
33
|
+
def read_fragment(event)
|
34
|
+
tags = {
|
35
|
+
'key.read' => event.payload[:key]
|
36
|
+
}
|
37
|
+
|
38
|
+
Utils.trace_notification(event: event, tags: tags)
|
39
|
+
end
|
40
|
+
|
41
|
+
def expire_fragment(event)
|
42
|
+
tags = {
|
43
|
+
'key.expire' => event.payload[:key]
|
44
|
+
}
|
45
|
+
|
46
|
+
Utils.trace_notification(event: event, tags: tags)
|
47
|
+
end
|
48
|
+
|
49
|
+
def exist_fragment?(event)
|
50
|
+
tags = {
|
51
|
+
'key.exist' => event.payload[:key]
|
52
|
+
}
|
53
|
+
|
54
|
+
Utils.trace_notification(event: event, tags: tags)
|
55
|
+
end
|
56
|
+
|
57
|
+
def write_page(event)
|
58
|
+
tags = {
|
59
|
+
'path.write' => event.payload[:path]
|
60
|
+
}
|
61
|
+
|
62
|
+
Utils.trace_notification(event: event, tags: tags)
|
63
|
+
end
|
64
|
+
|
65
|
+
def expire_page(event)
|
66
|
+
tags = {
|
67
|
+
'path.expire' => event.payload[:path]
|
68
|
+
}
|
69
|
+
|
70
|
+
Utils.trace_notification(event: event, tags: tags)
|
71
|
+
end
|
72
|
+
|
73
|
+
def start_processing(event)
|
74
|
+
tags = {
|
75
|
+
'controller' => event.payload[:controller],
|
76
|
+
'controller.action' => event.payload[:action],
|
77
|
+
'request.params' => event.payload[:params],
|
78
|
+
'request.format' => event.payload[:format],
|
79
|
+
'http.method' => event.payload[:method],
|
80
|
+
'http.url' => event.payload[:path]
|
81
|
+
}
|
82
|
+
|
83
|
+
Utils.trace_notification(event: event, tags: tags)
|
84
|
+
end
|
85
|
+
|
86
|
+
def process_action(event)
|
87
|
+
span_name = "#{event.payload[:action]}.#{event.payload[:controller]}"
|
88
|
+
|
89
|
+
tags = {
|
90
|
+
'controller' => event.payload[:controller],
|
91
|
+
'controller.action' => event.payload[:action],
|
92
|
+
'request.params' => event.payload[:params],
|
93
|
+
'request.format' => event.payload[:format],
|
94
|
+
'http.method' => event.payload[:method],
|
95
|
+
'http.url' => event.payload[:path],
|
96
|
+
'http.status_code' => event.payload[:status],
|
97
|
+
'view.runtime.ms' => event.payload[:view_runtime],
|
98
|
+
'db.runtime.ms' => event.payload[:db_runtime]
|
99
|
+
}
|
100
|
+
|
101
|
+
# Only append these tags onto the active span created by the patched 'process_action'
|
102
|
+
# Otherwise, create a new span for this notification as usual
|
103
|
+
active_span = ::Rails::Instrumentation.tracer.active_span
|
104
|
+
if active_span && active_span.operation_name == span_name
|
105
|
+
tags.each do |key, value|
|
106
|
+
::Rails::Instrumentation.tracer.active_span.set_tag(key, value)
|
107
|
+
end
|
108
|
+
else
|
109
|
+
Utils.trace_notification(event: event, tags: tags)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def send_file(event)
|
114
|
+
tags = {
|
115
|
+
'path.send' => event.payload[:path]
|
116
|
+
}
|
117
|
+
|
118
|
+
# there may be additional keys in the payload. It might be worth
|
119
|
+
# trying to tag them too
|
120
|
+
|
121
|
+
Utils.trace_notification(event: event, tags: tags)
|
122
|
+
end
|
123
|
+
|
124
|
+
def send_data(event)
|
125
|
+
# no defined keys, but user keys may be passed in. Might want to add
|
126
|
+
# them at some point
|
127
|
+
|
128
|
+
Utils.trace_notification(event: event, tags: tags)
|
129
|
+
end
|
130
|
+
|
131
|
+
def redirect_to(event)
|
132
|
+
tags = {
|
133
|
+
'http.status_code' => event.payload[:status],
|
134
|
+
'redirect.url' => event.payload[:location]
|
135
|
+
}
|
136
|
+
|
137
|
+
Utils.trace_notification(event: event, tags: tags)
|
138
|
+
end
|
139
|
+
|
140
|
+
def halted_callback(event)
|
141
|
+
tags = {
|
142
|
+
'filter' => event.payload[:filter]
|
143
|
+
}
|
144
|
+
|
145
|
+
Utils.trace_notification(event: event, tags: tags)
|
146
|
+
end
|
147
|
+
|
148
|
+
def unpermitted_parameters(event)
|
149
|
+
tags = {
|
150
|
+
'unpermitted_keys' => event.payload[:keys]
|
151
|
+
}
|
152
|
+
|
153
|
+
Utils.trace_notification(event: event, tags: tags)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Rails
|
2
|
+
module Instrumentation
|
3
|
+
module ActionMailerSubscriber
|
4
|
+
include Subscriber
|
5
|
+
|
6
|
+
EVENT_NAMESPACE = 'action_mailer'.freeze
|
7
|
+
|
8
|
+
EVENTS = %w[
|
9
|
+
receive
|
10
|
+
deliver
|
11
|
+
process
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def receive(event)
|
16
|
+
tags = {
|
17
|
+
'mailer' => event.payload[:mailer],
|
18
|
+
'message.id' => event.payload[:message_id],
|
19
|
+
'message.subject' => event.payload[:subject],
|
20
|
+
'message.to' => event.payload[:to],
|
21
|
+
'message.from' => event.payload[:from],
|
22
|
+
'message.bcc' => event.payload[:bcc],
|
23
|
+
'message.cc' => event.payload[:cc],
|
24
|
+
'message.date' => event.payload[:date],
|
25
|
+
'message.body' => event.payload[:mail]
|
26
|
+
}
|
27
|
+
|
28
|
+
Utils.trace_notification(event: event, tags: tags)
|
29
|
+
end
|
30
|
+
|
31
|
+
def deliver(event)
|
32
|
+
tags = {
|
33
|
+
'mailer' => event.payload[:mailer],
|
34
|
+
'message.id' => event.payload[:message_id],
|
35
|
+
'message.subject' => event.payload[:subject],
|
36
|
+
'message.to' => event.payload[:to],
|
37
|
+
'message.from' => event.payload[:from],
|
38
|
+
'message.bcc' => event.payload[:bcc],
|
39
|
+
'message.cc' => event.payload[:cc],
|
40
|
+
'message.date' => event.payload[:date],
|
41
|
+
'message.body' => event.payload[:mail]
|
42
|
+
}
|
43
|
+
|
44
|
+
Utils.trace_notification(event: event, tags: tags)
|
45
|
+
end
|
46
|
+
|
47
|
+
def process(event)
|
48
|
+
tags = {
|
49
|
+
'mailer' => event.payload[:mailer],
|
50
|
+
'action' => event.payload[:action],
|
51
|
+
'args' => event.payload[:args]
|
52
|
+
}
|
53
|
+
|
54
|
+
Utils.trace_notification(event: event, tags: tags)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|