rails-instrumentation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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