plainflow 2.2.3.pre.p1
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.
- checksums.yaml +7 -0
- data/Gemfile +2 -0
- data/History.md +200 -0
- data/Makefile +11 -0
- data/README.md +66 -0
- data/RELEASING.md +10 -0
- data/Rakefile +7 -0
- data/bin/plainflow +152 -0
- data/lib/plainflow/analytics.rb +31 -0
- data/lib/plainflow/analytics/client.rb +376 -0
- data/lib/plainflow/analytics/defaults.rb +20 -0
- data/lib/plainflow/analytics/logging.rb +35 -0
- data/lib/plainflow/analytics/request.rb +82 -0
- data/lib/plainflow/analytics/response.rb +16 -0
- data/lib/plainflow/analytics/utils.rb +88 -0
- data/lib/plainflow/analytics/version.rb +5 -0
- data/lib/plainflow/analytics/worker.rb +60 -0
- data/lib/segment.rb +1 -0
- data/plainflow-ruby.gemspec +25 -0
- data/spec/plainflow/analytics/client_spec.rb +311 -0
- data/spec/plainflow/analytics/request_spec.rb +191 -0
- data/spec/plainflow/analytics/response_spec.rb +30 -0
- data/spec/plainflow/analytics/worker_spec.rb +102 -0
- data/spec/plainflow/analytics_spec.rb +120 -0
- data/spec/spec_helper.rb +102 -0
- metadata +144 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'plainflow/analytics/defaults'
|
|
2
|
+
require 'plainflow/analytics/utils'
|
|
3
|
+
require 'plainflow/analytics/version'
|
|
4
|
+
require 'plainflow/analytics/client'
|
|
5
|
+
require 'plainflow/analytics/worker'
|
|
6
|
+
require 'plainflow/analytics/request'
|
|
7
|
+
require 'plainflow/analytics/response'
|
|
8
|
+
require 'plainflow/analytics/logging'
|
|
9
|
+
|
|
10
|
+
module Plainflow
|
|
11
|
+
class Analytics
|
|
12
|
+
def initialize options = {}
|
|
13
|
+
Request.stub = options[:stub] if options.has_key?(:stub)
|
|
14
|
+
@client = Plainflow::Analytics::Client.new options
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def method_missing message, *args, &block
|
|
18
|
+
if @client.respond_to? message
|
|
19
|
+
@client.send message, *args, &block
|
|
20
|
+
else
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
26
|
+
@client.respond_to?(method_name) || super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
include Logging
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
require 'time'
|
|
3
|
+
require 'plainflow/analytics/utils'
|
|
4
|
+
require 'plainflow/analytics/worker'
|
|
5
|
+
require 'plainflow/analytics/defaults'
|
|
6
|
+
|
|
7
|
+
module Plainflow
|
|
8
|
+
class Analytics
|
|
9
|
+
class Client
|
|
10
|
+
include Plainflow::Analytics::Utils
|
|
11
|
+
|
|
12
|
+
# public: Creates a new client
|
|
13
|
+
#
|
|
14
|
+
# attrs - Hash
|
|
15
|
+
# :secret_key - String of your project's secret_key
|
|
16
|
+
# :max_queue_size - Fixnum of the max calls to remain queued (optional)
|
|
17
|
+
# :on_error - Proc which handles error calls from the API
|
|
18
|
+
def initialize attrs = {}
|
|
19
|
+
symbolize_keys! attrs
|
|
20
|
+
|
|
21
|
+
@queue = Queue.new
|
|
22
|
+
@secret_key = attrs[:secret_key]
|
|
23
|
+
@max_queue_size = attrs[:max_queue_size] || Defaults::Queue::MAX_SIZE
|
|
24
|
+
@options = attrs
|
|
25
|
+
@worker_mutex = Mutex.new
|
|
26
|
+
@worker = Worker.new @queue, @secret_key, @options
|
|
27
|
+
|
|
28
|
+
check_secret_key!
|
|
29
|
+
|
|
30
|
+
at_exit { @worker_thread && @worker_thread[:should_exit] = true }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# public: Synchronously waits until the worker has flushed the queue.
|
|
34
|
+
# Use only for scripts which are not long-running, and will
|
|
35
|
+
# specifically exit
|
|
36
|
+
#
|
|
37
|
+
def flush
|
|
38
|
+
while !@queue.empty? || @worker.is_requesting?
|
|
39
|
+
ensure_worker_running
|
|
40
|
+
sleep(0.1)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# public: Tracks an event
|
|
45
|
+
#
|
|
46
|
+
# attrs - Hash
|
|
47
|
+
# :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id)
|
|
48
|
+
# :context - Hash of context. (optional)
|
|
49
|
+
# :event - String of event name.
|
|
50
|
+
# :integrations - Hash specifying what integrations this event goes to. (optional)
|
|
51
|
+
# :options - Hash specifying options such as user traits. (optional)
|
|
52
|
+
# :properties - Hash of event properties. (optional)
|
|
53
|
+
# :timestamp - Time of when the event occurred. (optional)
|
|
54
|
+
# :user_id - String of the user id.
|
|
55
|
+
# :message_id - String of the message id that uniquely identified a message across the API. (optional)
|
|
56
|
+
def track attrs
|
|
57
|
+
symbolize_keys! attrs
|
|
58
|
+
check_user_id! attrs
|
|
59
|
+
|
|
60
|
+
event = attrs[:event]
|
|
61
|
+
properties = attrs[:properties] || {}
|
|
62
|
+
timestamp = attrs[:timestamp] || Time.new
|
|
63
|
+
context = attrs[:context] || {}
|
|
64
|
+
message_id = attrs[:message_id].to_s if attrs[:message_id]
|
|
65
|
+
|
|
66
|
+
check_timestamp! timestamp
|
|
67
|
+
|
|
68
|
+
if event.nil? || event.empty?
|
|
69
|
+
fail ArgumentError, 'Must supply event as a non-empty string'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
fail ArgumentError, 'Properties must be a Hash' unless properties.is_a? Hash
|
|
73
|
+
isoify_dates! properties
|
|
74
|
+
|
|
75
|
+
add_context context
|
|
76
|
+
|
|
77
|
+
enqueue({
|
|
78
|
+
:event => event,
|
|
79
|
+
:userId => attrs[:user_id],
|
|
80
|
+
:anonymousId => attrs[:anonymous_id],
|
|
81
|
+
:context => context,
|
|
82
|
+
:options => attrs[:options],
|
|
83
|
+
:integrations => attrs[:integrations],
|
|
84
|
+
:properties => properties,
|
|
85
|
+
:messageId => message_id,
|
|
86
|
+
:timestamp => datetime_in_iso8601(timestamp),
|
|
87
|
+
:type => 'track'
|
|
88
|
+
})
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# public: Identifies a user
|
|
92
|
+
#
|
|
93
|
+
# attrs - Hash
|
|
94
|
+
# :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id)
|
|
95
|
+
# :context - Hash of context. (optional)
|
|
96
|
+
# :integrations - Hash specifying what integrations this event goes to. (optional)
|
|
97
|
+
# :options - Hash specifying options such as user traits. (optional)
|
|
98
|
+
# :timestamp - Time of when the event occurred. (optional)
|
|
99
|
+
# :traits - Hash of user traits. (optional)
|
|
100
|
+
# :user_id - String of the user id
|
|
101
|
+
# :message_id - String of the message id that uniquely identified a message across the API. (optional)
|
|
102
|
+
def identify attrs
|
|
103
|
+
symbolize_keys! attrs
|
|
104
|
+
check_user_id! attrs
|
|
105
|
+
|
|
106
|
+
traits = attrs[:traits] || {}
|
|
107
|
+
timestamp = attrs[:timestamp] || Time.new
|
|
108
|
+
context = attrs[:context] || {}
|
|
109
|
+
message_id = attrs[:message_id].to_s if attrs[:message_id]
|
|
110
|
+
|
|
111
|
+
check_timestamp! timestamp
|
|
112
|
+
|
|
113
|
+
fail ArgumentError, 'Must supply traits as a hash' unless traits.is_a? Hash
|
|
114
|
+
isoify_dates! traits
|
|
115
|
+
|
|
116
|
+
add_context context
|
|
117
|
+
|
|
118
|
+
enqueue({
|
|
119
|
+
:userId => attrs[:user_id],
|
|
120
|
+
:anonymousId => attrs[:anonymous_id],
|
|
121
|
+
:integrations => attrs[:integrations],
|
|
122
|
+
:context => context,
|
|
123
|
+
:traits => traits,
|
|
124
|
+
:options => attrs[:options],
|
|
125
|
+
:messageId => message_id,
|
|
126
|
+
:timestamp => datetime_in_iso8601(timestamp),
|
|
127
|
+
:type => 'identify'
|
|
128
|
+
})
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# public: Aliases a user from one id to another
|
|
132
|
+
#
|
|
133
|
+
# attrs - Hash
|
|
134
|
+
# :context - Hash of context (optional)
|
|
135
|
+
# :integrations - Hash specifying what integrations this event goes to. (optional)
|
|
136
|
+
# :options - Hash specifying options such as user traits. (optional)
|
|
137
|
+
# :previous_id - String of the id to alias from
|
|
138
|
+
# :timestamp - Time of when the alias occured (optional)
|
|
139
|
+
# :user_id - String of the id to alias to
|
|
140
|
+
# :message_id - String of the message id that uniquely identified a message across the API. (optional)
|
|
141
|
+
def alias(attrs)
|
|
142
|
+
symbolize_keys! attrs
|
|
143
|
+
|
|
144
|
+
from = attrs[:previous_id]
|
|
145
|
+
to = attrs[:user_id]
|
|
146
|
+
timestamp = attrs[:timestamp] || Time.new
|
|
147
|
+
context = attrs[:context] || {}
|
|
148
|
+
message_id = attrs[:message_id].to_s if attrs[:message_id]
|
|
149
|
+
|
|
150
|
+
check_presence! from, 'previous_id'
|
|
151
|
+
check_presence! to, 'user_id'
|
|
152
|
+
check_timestamp! timestamp
|
|
153
|
+
add_context context
|
|
154
|
+
|
|
155
|
+
enqueue({
|
|
156
|
+
:previousId => from,
|
|
157
|
+
:userId => to,
|
|
158
|
+
:integrations => attrs[:integrations],
|
|
159
|
+
:context => context,
|
|
160
|
+
:options => attrs[:options],
|
|
161
|
+
:messageId => message_id,
|
|
162
|
+
:timestamp => datetime_in_iso8601(timestamp),
|
|
163
|
+
:type => 'alias'
|
|
164
|
+
})
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# public: Associates a user identity with a group.
|
|
168
|
+
#
|
|
169
|
+
# attrs - Hash
|
|
170
|
+
# :context - Hash of context (optional)
|
|
171
|
+
# :integrations - Hash specifying what integrations this event goes to. (optional)
|
|
172
|
+
# :options - Hash specifying options such as user traits. (optional)
|
|
173
|
+
# :previous_id - String of the id to alias from
|
|
174
|
+
# :timestamp - Time of when the alias occured (optional)
|
|
175
|
+
# :user_id - String of the id to alias to
|
|
176
|
+
# :message_id - String of the message id that uniquely identified a message across the API. (optional)
|
|
177
|
+
def group(attrs)
|
|
178
|
+
symbolize_keys! attrs
|
|
179
|
+
check_user_id! attrs
|
|
180
|
+
|
|
181
|
+
group_id = attrs[:group_id]
|
|
182
|
+
user_id = attrs[:user_id]
|
|
183
|
+
traits = attrs[:traits] || {}
|
|
184
|
+
timestamp = attrs[:timestamp] || Time.new
|
|
185
|
+
context = attrs[:context] || {}
|
|
186
|
+
message_id = attrs[:message_id].to_s if attrs[:message_id]
|
|
187
|
+
|
|
188
|
+
fail ArgumentError, '.traits must be a hash' unless traits.is_a? Hash
|
|
189
|
+
isoify_dates! traits
|
|
190
|
+
|
|
191
|
+
check_presence! group_id, 'group_id'
|
|
192
|
+
check_timestamp! timestamp
|
|
193
|
+
add_context context
|
|
194
|
+
|
|
195
|
+
enqueue({
|
|
196
|
+
:groupId => group_id,
|
|
197
|
+
:userId => user_id,
|
|
198
|
+
:traits => traits,
|
|
199
|
+
:integrations => attrs[:integrations],
|
|
200
|
+
:options => attrs[:options],
|
|
201
|
+
:context => context,
|
|
202
|
+
:messageId => message_id,
|
|
203
|
+
:timestamp => datetime_in_iso8601(timestamp),
|
|
204
|
+
:type => 'group'
|
|
205
|
+
})
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# public: Records a page view
|
|
209
|
+
#
|
|
210
|
+
# attrs - Hash
|
|
211
|
+
# :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id)
|
|
212
|
+
# :category - String of the page category (optional)
|
|
213
|
+
# :context - Hash of context (optional)
|
|
214
|
+
# :integrations - Hash specifying what integrations this event goes to. (optional)
|
|
215
|
+
# :name - String name of the page
|
|
216
|
+
# :options - Hash specifying options such as user traits. (optional)
|
|
217
|
+
# :properties - Hash of page properties (optional)
|
|
218
|
+
# :timestamp - Time of when the pageview occured (optional)
|
|
219
|
+
# :user_id - String of the id to alias from
|
|
220
|
+
# :message_id - String of the message id that uniquely identified a message across the API. (optional)
|
|
221
|
+
def page(attrs)
|
|
222
|
+
symbolize_keys! attrs
|
|
223
|
+
check_user_id! attrs
|
|
224
|
+
|
|
225
|
+
name = attrs[:name].to_s
|
|
226
|
+
properties = attrs[:properties] || {}
|
|
227
|
+
timestamp = attrs[:timestamp] || Time.new
|
|
228
|
+
context = attrs[:context] || {}
|
|
229
|
+
message_id = attrs[:message_id].to_s if attrs[:message_id]
|
|
230
|
+
|
|
231
|
+
fail ArgumentError, '.properties must be a hash' unless properties.is_a? Hash
|
|
232
|
+
isoify_dates! properties
|
|
233
|
+
|
|
234
|
+
check_timestamp! timestamp
|
|
235
|
+
add_context context
|
|
236
|
+
|
|
237
|
+
enqueue({
|
|
238
|
+
:userId => attrs[:user_id],
|
|
239
|
+
:anonymousId => attrs[:anonymous_id],
|
|
240
|
+
:name => name,
|
|
241
|
+
:category => attrs[:category],
|
|
242
|
+
:properties => properties,
|
|
243
|
+
:integrations => attrs[:integrations],
|
|
244
|
+
:options => attrs[:options],
|
|
245
|
+
:context => context,
|
|
246
|
+
:messageId => message_id,
|
|
247
|
+
:timestamp => datetime_in_iso8601(timestamp),
|
|
248
|
+
:type => 'page'
|
|
249
|
+
})
|
|
250
|
+
end
|
|
251
|
+
# public: Records a screen view (for a mobile app)
|
|
252
|
+
#
|
|
253
|
+
# attrs - Hash
|
|
254
|
+
# :anonymous_id - String of the user's id when you don't know who they are yet. (optional but you must provide either an anonymous_id or user_id. See: https://segment.io/docs/tracking - api/track/#user - id)
|
|
255
|
+
# :category - String screen category (optional)
|
|
256
|
+
# :context - Hash of context (optional)
|
|
257
|
+
# :integrations - Hash specifying what integrations this event goes to. (optional)
|
|
258
|
+
# :name - String name of the screen
|
|
259
|
+
# :options - Hash specifying options such as user traits. (optional)
|
|
260
|
+
# :properties - Hash of screen properties (optional)
|
|
261
|
+
# :timestamp - Time of when the screen occured (optional)
|
|
262
|
+
# :user_id - String of the id to alias from
|
|
263
|
+
def screen(attrs)
|
|
264
|
+
symbolize_keys! attrs
|
|
265
|
+
check_user_id! attrs
|
|
266
|
+
|
|
267
|
+
name = attrs[:name].to_s
|
|
268
|
+
properties = attrs[:properties] || {}
|
|
269
|
+
timestamp = attrs[:timestamp] || Time.new
|
|
270
|
+
context = attrs[:context] || {}
|
|
271
|
+
message_id = attrs[:message_id].to_s if attrs[:message_id]
|
|
272
|
+
|
|
273
|
+
fail ArgumentError, '.properties must be a hash' unless properties.is_a? Hash
|
|
274
|
+
isoify_dates! properties
|
|
275
|
+
|
|
276
|
+
check_timestamp! timestamp
|
|
277
|
+
add_context context
|
|
278
|
+
|
|
279
|
+
enqueue({
|
|
280
|
+
:userId => attrs[:user_id],
|
|
281
|
+
:anonymousId => attrs[:anonymous_id],
|
|
282
|
+
:name => name,
|
|
283
|
+
:properties => properties,
|
|
284
|
+
:category => attrs[:category],
|
|
285
|
+
:options => attrs[:options],
|
|
286
|
+
:integrations => attrs[:integrations],
|
|
287
|
+
:context => context,
|
|
288
|
+
:messageId => message_id,
|
|
289
|
+
:timestamp => timestamp.iso8601,
|
|
290
|
+
:type => 'screen'
|
|
291
|
+
})
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# public: Returns the number of queued messages
|
|
295
|
+
#
|
|
296
|
+
# returns Fixnum of messages in the queue
|
|
297
|
+
def queued_messages
|
|
298
|
+
@queue.length
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
private
|
|
302
|
+
|
|
303
|
+
# private: Enqueues the action.
|
|
304
|
+
#
|
|
305
|
+
# returns Boolean of whether the item was added to the queue.
|
|
306
|
+
def enqueue(action)
|
|
307
|
+
# add our request id for tracing purposes
|
|
308
|
+
action[:messageId] ||= uid
|
|
309
|
+
unless queue_full = @queue.length >= @max_queue_size
|
|
310
|
+
ensure_worker_running
|
|
311
|
+
@queue << action
|
|
312
|
+
end
|
|
313
|
+
!queue_full
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# private: Ensures that a string is non-empty
|
|
317
|
+
#
|
|
318
|
+
# obj - String|Number that must be non-blank
|
|
319
|
+
# name - Name of the validated value
|
|
320
|
+
#
|
|
321
|
+
def check_presence!(obj, name)
|
|
322
|
+
if obj.nil? || (obj.is_a?(String) && obj.empty?)
|
|
323
|
+
fail ArgumentError, "#{name} must be given"
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# private: Adds contextual information to the call
|
|
328
|
+
#
|
|
329
|
+
# context - Hash of call context
|
|
330
|
+
def add_context(context)
|
|
331
|
+
context[:library] = { :name => "analytics-ruby", :version => Plainflow::Analytics::VERSION.to_s }
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# private: Checks that the secret_key is properly initialized
|
|
335
|
+
def check_secret_key!
|
|
336
|
+
fail ArgumentError, 'Write key must be initialized' if @secret_key.nil?
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# private: Checks the timstamp option to make sure it is a Time.
|
|
340
|
+
def check_timestamp!(timestamp)
|
|
341
|
+
fail ArgumentError, 'Timestamp must be a Time' unless timestamp.is_a? Time
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def event attrs
|
|
345
|
+
symbolize_keys! attrs
|
|
346
|
+
|
|
347
|
+
{
|
|
348
|
+
:userId => user_id,
|
|
349
|
+
:name => name,
|
|
350
|
+
:properties => properties,
|
|
351
|
+
:context => context,
|
|
352
|
+
:timestamp => datetime_in_iso8601(timestamp),
|
|
353
|
+
:type => 'screen'
|
|
354
|
+
}
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def check_user_id! attrs
|
|
358
|
+
fail ArgumentError, 'Must supply either user_id or anonymous_id' unless attrs[:user_id] || attrs[:anonymous_id]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def ensure_worker_running
|
|
362
|
+
return if worker_running?
|
|
363
|
+
@worker_mutex.synchronize do
|
|
364
|
+
return if worker_running?
|
|
365
|
+
@worker_thread = Thread.new do
|
|
366
|
+
@worker.run
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def worker_running?
|
|
372
|
+
@worker_thread && @worker_thread.alive?
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Plainflow
|
|
2
|
+
class Analytics
|
|
3
|
+
module Defaults
|
|
4
|
+
module Request
|
|
5
|
+
HOST = 'pipe.plainflow.net'
|
|
6
|
+
PORT = 443
|
|
7
|
+
PATH = '/v1/import'
|
|
8
|
+
SSL = true
|
|
9
|
+
HEADERS = { :accept => 'application/json' }
|
|
10
|
+
RETRIES = 4
|
|
11
|
+
BACKOFF = 30.0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Queue
|
|
15
|
+
BATCH_SIZE = 100
|
|
16
|
+
MAX_SIZE = 10000
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module Plainflow
|
|
4
|
+
class Analytics
|
|
5
|
+
module Logging
|
|
6
|
+
class << self
|
|
7
|
+
def logger
|
|
8
|
+
@logger ||= if defined?(Rails)
|
|
9
|
+
Rails.logger
|
|
10
|
+
else
|
|
11
|
+
logger = Logger.new STDOUT
|
|
12
|
+
logger.progname = 'Plainflow::Analytics'
|
|
13
|
+
logger
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def logger= logger
|
|
18
|
+
@logger = logger
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.included base
|
|
23
|
+
class << base
|
|
24
|
+
def logger
|
|
25
|
+
Logging.logger
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def logger
|
|
31
|
+
Logging.logger
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|