union_station_hooks_core 1.0.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,182 @@
1
+ # Union Station - https://www.unionstationapp.com/
2
+ # Copyright (c) 2010-2015 Phusion Holding B.V.
3
+ #
4
+ # "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+ UnionStationHooks.require_lib 'utils'
25
+ UnionStationHooks.require_lib 'time_point'
26
+ UnionStationHooks.require_lib 'request_reporter'
27
+
28
+ module UnionStationHooks
29
+ class << self
30
+ # @note You do not have to call this! Passenger automatically calls
31
+ # this for you! Just obtain the RequestReporter object that has been made
32
+ # available for you.
33
+ #
34
+ # Indicates that a Rack request has begun. Given a Rack environment hash,
35
+ # this method returns {RequestReporter} object, which you can use for
36
+ # logging Union Station information about this request. This method should
37
+ # be called as early as possible during a request, before any processing
38
+ # has begun. Only after calling this method will it be possible to log
39
+ # request-specific information to Union Station.
40
+ #
41
+ # The {RequestReporter} object that this method creates is also made
42
+ # available through the `union_station_hooks` key in the Rack environment
43
+ # hash, as well as the `:union_station_hooks` key in the current thread's
44
+ # object:
45
+ #
46
+ # env['union_station_hooks']
47
+ # # => RequestReporter object or nil
48
+ #
49
+ # Thread.current[:union_station_hooks]
50
+ # # => RequestReporter object or nil
51
+ #
52
+ # If this method was already called on this Rack request, then this method
53
+ # does nothing and merely returns the previously created {RequestReporter}.
54
+ #
55
+ # See {RequestReporter} to learn what kind of information you can log to
56
+ # Union Station about Rack requests.
57
+ #
58
+ # @return [RequestReporter, nil] A {RequestReporter} object, or nil or
59
+ # because of certain error conditions. See the RequestReporter class
60
+ # description for when this may be nil.
61
+ def begin_rack_request(rack_env)
62
+ reporter = rack_env['union_station_hooks']
63
+ return reporter if reporter
64
+
65
+ # PASSENGER_TXN_ID may be nil here even if Union Station support is
66
+ # enabled. For example, if the user tried to access the application
67
+ # process directly through its private HTTP socket.
68
+ txn_id = rack_env['PASSENGER_TXN_ID']
69
+ return nil if !txn_id
70
+
71
+ reporter = RequestReporter.new(context, txn_id, app_group_name, key)
72
+ return if reporter.null?
73
+
74
+ rack_env['union_station_hooks'] = reporter
75
+ Thread.current[:union_station_hooks] = reporter
76
+ reporter.log_request_begin
77
+ reporter.log_gc_stats_on_request_begin
78
+ reporter
79
+ end
80
+
81
+ # @note You do not have to call this! Passenger automatically calls
82
+ # this for you!
83
+ #
84
+ # Indicates that a Rack request, on which {begin_rack_request} was called,
85
+ # has ended. You should call this method as late as possible during a
86
+ # request, after all processing have ended. Preferably after the Rack
87
+ # response body has closed.
88
+ #
89
+ # The {RequestReporter} object associated with this Rack request and with
90
+ # the current, will be closed (by calling {RequestReporter#close}), which
91
+ # finalizes the Union Station logs for this request.
92
+ #
93
+ # This method MUST be called in the same thread that called
94
+ # {begin_rack_request}.
95
+ #
96
+ # It is undefined what will happen if you call this method a Rack request
97
+ # on which {begin_rack_request} was not called, so don't do that.
98
+ #
99
+ # This method does nothing if it was already called on this Rack request.
100
+ def end_rack_request(rack_env,
101
+ uncaught_exception_raised_during_request = false)
102
+ reporter = rack_env.delete('union_station_hooks')
103
+ Thread.current[:union_station_hooks] = nil
104
+ if reporter
105
+ begin
106
+ reporter.log_gc_stats_on_request_end
107
+ reporter.log_request_end(uncaught_exception_raised_during_request)
108
+ ensure
109
+ reporter.close
110
+ end
111
+ end
112
+ end
113
+
114
+ # Returns an opaque object (a {TimePoint}) that represents a collection
115
+ # of metrics about the current time.
116
+ #
117
+ # Various API methods expect you to provide timing information. They
118
+ # accept standard Ruby `Time` objects, but it is generally better to
119
+ # pass `TimePoint` objects. Unlike the standard Ruby `Time` object,
120
+ # which only contains the wall clock time (the real time), `TimePoint`
121
+ # may contain additional timing information such as CPU time, time
122
+ # spent in userspace and kernel space, time spent context switching,
123
+ # etc. The exact information contained in the object is operating
124
+ # system specific, hence why the object is meant to be opaque.
125
+ #
126
+ # See {RequestReporter#log_controller_action_happened} for an example of
127
+ # an API method which expects timing information.
128
+ # `RequestReporter#log_controller_action_happened` expects you to
129
+ # provide timing information about a controller action. That timing
130
+ # information is supposed to be obtained by calling
131
+ # `UnionStationHooks.now`.
132
+ #
133
+ # In all API methods that expect a `TimePoint`, you can also pass a
134
+ # normal Ruby `Time` object instead. But if you do that, the logged
135
+ # timing information will be less detailed. Only do this if you cannot
136
+ # obtain a `TimePoint` object for some reason.
137
+ #
138
+ # @return [TimePoint]
139
+ def now
140
+ pt = Utils.process_times
141
+ TimePoint.new(Time.now, pt.utime, pt.stime)
142
+ end
143
+
144
+ private
145
+
146
+ def create_context
147
+ require_lib('context')
148
+ @@context = Context.new(config[:ust_router_address],
149
+ config[:ust_router_username] || 'logging',
150
+ config[:ust_router_password],
151
+ config[:node_name])
152
+ end
153
+
154
+ def install_event_pre_hook
155
+ preprocessor = @@config[:event_preprocessor]
156
+ if preprocessor
157
+ define_singleton_method(:call_event_pre_hook, &preprocessor)
158
+ else
159
+ def call_event_pre_hook(_event)
160
+ # Do nothing
161
+ end
162
+ end
163
+ end
164
+
165
+ def initialize_other_union_station_hooks_gems
166
+ @@initializers.each do |initializer|
167
+ initializer.initialize!
168
+ end
169
+ end
170
+
171
+ def finalize_install
172
+ @@config.freeze
173
+ @@initializers.freeze
174
+ @@app_group_name = @@config[:app_group_name]
175
+ @@key = @@config[:union_station_key]
176
+ if @@config[:debug]
177
+ UnionStationHooks::Log.debugging = true
178
+ end
179
+ @@initialized = true
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,67 @@
1
+ # Union Station - https://www.unionstationapp.com/
2
+ # Copyright (c) 2010-2015 Phusion Holding B.V.
3
+ #
4
+ # "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+ require 'thread'
25
+ UnionStationHooks.require_lib 'message_channel'
26
+
27
+ module UnionStationHooks
28
+ # Represents a connection to the UstRouter process.
29
+ #
30
+ # @private
31
+ class Connection
32
+ attr_reader :mutex
33
+ attr_accessor :channel
34
+
35
+ def initialize(io)
36
+ @mutex = Mutex.new
37
+ @refcount = 1
38
+ @channel = MessageChannel.new(io) if io
39
+ end
40
+
41
+ def connected?
42
+ !!@channel
43
+ end
44
+
45
+ def disconnect
46
+ @channel.io.close if @channel
47
+ @channel = nil
48
+ end
49
+
50
+ def ref
51
+ @refcount += 1
52
+ end
53
+
54
+ def unref
55
+ @refcount -= 1
56
+ if @refcount == 0
57
+ disconnect
58
+ end
59
+ end
60
+
61
+ def synchronize
62
+ @mutex.synchronize do
63
+ yield
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,297 @@
1
+ # Union Station - https://www.unionstationapp.com/
2
+ # Copyright (c) 2010-2015 Phusion Holding B.V.
3
+ #
4
+ # "Union Station" and "Passenger" are trademarks of Phusion Holding B.V.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+ require 'thread'
25
+ require 'socket'
26
+ UnionStationHooks.require_lib 'connection'
27
+ UnionStationHooks.require_lib 'transaction'
28
+ UnionStationHooks.require_lib 'log'
29
+ UnionStationHooks.require_lib 'lock'
30
+ UnionStationHooks.require_lib 'utils'
31
+
32
+ module UnionStationHooks
33
+ # A Context object is the "heart" of all `union_station_hooks_*` gems. It
34
+ # contains a connection to the UstRouter (through a Connection object)
35
+ # and allows you to create Transaction objects.
36
+ #
37
+ # Context is a singleton. During initialization
38
+ # (`UnionStationHooks.initialize!`), an instance is created and stored in
39
+ # `UnionStationHooks.context`. All the public API methods make use of this
40
+ # singleton context.
41
+ #
42
+ # See hacking/Architecture.md for an overview.
43
+ #
44
+ # @private
45
+ class Context
46
+ RETRY_SLEEP = 0.2
47
+ NETWORK_ERRORS = [
48
+ Errno::EPIPE, Errno::ECONNREFUSED, Errno::ECONNRESET,
49
+ Errno::EHOSTUNREACH, Errno::ENETDOWN, Errno::ENETUNREACH,
50
+ Errno::ETIMEDOUT
51
+ ]
52
+
53
+ include Utils
54
+
55
+ attr_accessor :max_connect_tries
56
+ attr_accessor :reconnect_timeout
57
+
58
+ def initialize(ust_router_address, username, password, node_name)
59
+ @server_address = ust_router_address
60
+ @username = username
61
+ @password = password
62
+ if node_name && !node_name.empty?
63
+ @node_name = node_name
64
+ else
65
+ @node_name = `hostname`.strip
66
+ end
67
+ @random_dev = File.open('/dev/urandom')
68
+
69
+ # This mutex protects the following instance variables, but
70
+ # not the contents of @connection.
71
+ @mutex = Mutex.new
72
+
73
+ @connection = Connection.new(nil)
74
+ if @server_address && local_socket_address?(@server_address)
75
+ @max_connect_tries = 10
76
+ else
77
+ @max_connect_tries = 1
78
+ end
79
+ @reconnect_timeout = 1
80
+ @next_reconnect_time = Time.utc(1980, 1, 1)
81
+ end
82
+
83
+ def connection
84
+ @mutex.synchronize do
85
+ @connection
86
+ end
87
+ end
88
+
89
+ def clear_connection
90
+ @mutex.synchronize do
91
+ @connection.synchronize do
92
+ @random_dev = File.open('/dev/urandom') if @random_dev.closed?
93
+ @connection.unref
94
+ @connection = Connection.new(nil)
95
+ end
96
+ end
97
+ end
98
+
99
+ def close
100
+ @mutex.synchronize do
101
+ @connection.synchronize do
102
+ @random_dev.close
103
+ @connection.unref
104
+ @connection = nil
105
+ end
106
+ end
107
+ end
108
+
109
+ def new_transaction(group_name, category, key)
110
+ if !@server_address
111
+ return Transaction.new(nil, nil)
112
+ elsif !group_name || group_name.empty?
113
+ raise ArgumentError, 'Group name may not be empty'
114
+ end
115
+
116
+ txn_id = create_txn_id
117
+
118
+ Lock.new(@mutex).synchronize do |_lock|
119
+ if Time.now < @next_reconnect_time
120
+ return Transaction.new(nil, nil)
121
+ end
122
+
123
+ Lock.new(@connection.mutex).synchronize do |connection_lock|
124
+ if !@connection.connected?
125
+ begin
126
+ connect
127
+ connection_lock.reset(@connection.mutex)
128
+ rescue SystemCallError, IOError
129
+ @connection.disconnect
130
+ UnionStationHooks::Log.warn(
131
+ "Cannot connect to the UstRouter at #{@server_address}; " \
132
+ "retrying in #{@reconnect_timeout} second(s).")
133
+ @next_reconnect_time = Time.now + @reconnect_timeout
134
+ return Transaction.new(nil, nil)
135
+ rescue Exception => e
136
+ @connection.disconnect
137
+ raise e
138
+ end
139
+ end
140
+
141
+ begin
142
+ @connection.channel.write('openTransaction',
143
+ txn_id, group_name, '', category,
144
+ Utils.encoded_timestamp,
145
+ key,
146
+ true,
147
+ true)
148
+ result = @connection.channel.read
149
+ if result[0] != 'status'
150
+ raise "Expected UstRouter to respond with 'status', " \
151
+ "but got #{result.inspect} instead"
152
+ elsif result[1] == 'ok'
153
+ # Do nothing
154
+ elsif result[1] == 'error'
155
+ if result[2]
156
+ raise "Unable to close transaction: #{result[2]}"
157
+ else
158
+ raise 'Unable to close transaction (no server message given)'
159
+ end
160
+ else
161
+ raise "Expected UstRouter to respond with 'ok' or 'error', " \
162
+ "but got #{result.inspect} instead"
163
+ end
164
+
165
+ return Transaction.new(@connection, txn_id)
166
+ rescue SystemCallError, IOError
167
+ @connection.disconnect
168
+ UnionStationHooks::Log.warn(
169
+ "The UstRouter at #{@server_address}" \
170
+ ' closed the connection; will reconnect in ' \
171
+ "#{@reconnect_timeout} second(s).")
172
+ @next_reconnect_time = Time.now + @reconnect_timeout
173
+ return Transaction.new(nil, nil)
174
+ rescue Exception => e
175
+ @connection.disconnect
176
+ raise e
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ def continue_transaction(txn_id, group_name, category, key)
183
+ if !@server_address
184
+ return Transaction.new(nil, nil)
185
+ elsif !txn_id || txn_id.empty?
186
+ raise ArgumentError, 'Transaction ID may not be empty'
187
+ end
188
+
189
+ Lock.new(@mutex).synchronize do |_lock|
190
+ if Time.now < @next_reconnect_time
191
+ return Transaction.new(nil, nil)
192
+ end
193
+
194
+ Lock.new(@connection.mutex).synchronize do |connection_lock|
195
+ if !@connection.connected?
196
+ begin
197
+ connect
198
+ connection_lock.reset(@connection.mutex)
199
+ rescue SystemCallError, IOError
200
+ @connection.disconnect
201
+ UnionStationHooks::Log.warn(
202
+ "Cannot connect to the UstRouter at #{@server_address}; " \
203
+ "retrying in #{@reconnect_timeout} second(s).")
204
+ @next_reconnect_time = Time.now + @reconnect_timeout
205
+ return Transaction.new(nil, nil)
206
+ rescue Exception => e
207
+ @connection.disconnect
208
+ raise e
209
+ end
210
+ end
211
+
212
+ begin
213
+ @connection.channel.write('openTransaction',
214
+ txn_id, group_name, '', category,
215
+ Utils.encoded_timestamp,
216
+ key,
217
+ true)
218
+ return Transaction.new(@connection, txn_id)
219
+ rescue SystemCallError, IOError
220
+ @connection.disconnect
221
+ UnionStationHooks::Log.warn(
222
+ "The UstRouter at #{@server_address}" \
223
+ ' closed the connection; will reconnect in ' \
224
+ "#{@reconnect_timeout} second(s).")
225
+ @next_reconnect_time = Time.now + @reconnect_timeout
226
+ return Transaction.new(nil, nil)
227
+ rescue Exception => e
228
+ @connection.disconnect
229
+ raise e
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ private
236
+
237
+ RANDOM_CHARS = %w(
238
+ abcdefghijklmnopqrstuvwxyz
239
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ
240
+ 0123456789
241
+ )
242
+
243
+ def connect
244
+ socket = connect_to_server(@server_address)
245
+ channel = MessageChannel.new(socket)
246
+
247
+ handshake_version(channel)
248
+ handshake_authentication(channel)
249
+ handshake_initialization(channel)
250
+
251
+ @connection.unref
252
+ @connection = Connection.new(socket)
253
+ rescue Exception => e
254
+ socket.close if socket && !socket.closed?
255
+ raise e
256
+ end
257
+
258
+ def handshake_version(channel)
259
+ result = channel.read
260
+ if result.nil?
261
+ raise EOFError
262
+ elsif result.size != 2 || result[0] != 'version'
263
+ raise IOError, "The UstRouter didn't sent a valid version identifier"
264
+ elsif result[1] != '1'
265
+ raise IOError, "Unsupported UstRouter protocol version #{result[1]}"
266
+ end
267
+ end
268
+
269
+ def handshake_authentication(channel)
270
+ channel.write_scalar(@username)
271
+ channel.write_scalar(@password)
272
+ process_ust_router_reply(channel,
273
+ 'UstRouter client authentication error',
274
+ SecurityError)
275
+ end
276
+
277
+ def handshake_initialization(channel)
278
+ channel.write('init', @node_name)
279
+ process_ust_router_reply(channel,
280
+ 'UstRouter client initialization error')
281
+ end
282
+
283
+ def random_token(length)
284
+ token = ''
285
+ @random_dev.read(length).each_byte do |c|
286
+ token << RANDOM_CHARS[c % RANDOM_CHARS.size]
287
+ end
288
+ token
289
+ end
290
+
291
+ def create_txn_id
292
+ result = (Time.now.to_i / 60).to_s(36)
293
+ result << "-#{random_token(11)}"
294
+ result
295
+ end
296
+ end
297
+ end