rails-instrumentation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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__)
@@ -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,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