union_station_hooks_core 1.0.0

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