natswork-server 0.0.1
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/CHANGELOG.md +0 -0
- data/LICENSE +21 -0
- data/README.md +286 -0
- data/lib/natswork/cli.rb +420 -0
- data/lib/natswork/error_tracker.rb +338 -0
- data/lib/natswork/health_check.rb +252 -0
- data/lib/natswork/instrumentation.rb +141 -0
- data/lib/natswork/job_executor.rb +271 -0
- data/lib/natswork/job_hooks.rb +63 -0
- data/lib/natswork/logger.rb +183 -0
- data/lib/natswork/metrics.rb +241 -0
- data/lib/natswork/middleware.rb +142 -0
- data/lib/natswork/middleware_chain.rb +40 -0
- data/lib/natswork/monitoring.rb +397 -0
- data/lib/natswork/protocol.rb +454 -0
- data/lib/natswork/queue_manager.rb +164 -0
- data/lib/natswork/retry_handler.rb +125 -0
- data/lib/natswork/server/version.rb +7 -0
- data/lib/natswork/server.rb +47 -0
- data/lib/natswork/simple_worker.rb +101 -0
- data/lib/natswork/thread_pool.rb +192 -0
- data/lib/natswork/worker.rb +217 -0
- data/lib/natswork/worker_manager.rb +62 -0
- data/lib/natswork-server.rb +5 -0
- metadata +151 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent'
|
4
|
+
|
5
|
+
module NatsWork
|
6
|
+
module Instrumentation
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# Event subscribers
|
10
|
+
@subscribers = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
|
11
|
+
@enabled = true
|
12
|
+
|
13
|
+
def subscribe(event, &block)
|
14
|
+
return unless block_given?
|
15
|
+
|
16
|
+
subscription = Subscription.new(event, block)
|
17
|
+
@subscribers[event.to_s] << subscription
|
18
|
+
subscription
|
19
|
+
end
|
20
|
+
|
21
|
+
def unsubscribe(subscription)
|
22
|
+
@subscribers.each_value { |subs| subs.delete(subscription) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def instrument(event, payload = {})
|
26
|
+
return yield if block_given? && !@enabled
|
27
|
+
|
28
|
+
event_name = event.to_s
|
29
|
+
payload = payload.dup
|
30
|
+
payload[:event] = event_name
|
31
|
+
payload[:started_at] = Time.now
|
32
|
+
|
33
|
+
begin
|
34
|
+
if block_given?
|
35
|
+
result = yield
|
36
|
+
payload[:finished_at] = Time.now
|
37
|
+
payload[:duration] = (payload[:finished_at] - payload[:started_at]) * 1000
|
38
|
+
payload[:result] = result
|
39
|
+
|
40
|
+
notify_subscribers(event_name, payload)
|
41
|
+
result
|
42
|
+
else
|
43
|
+
notify_subscribers(event_name, payload)
|
44
|
+
end
|
45
|
+
rescue StandardError => e
|
46
|
+
payload[:finished_at] = Time.now
|
47
|
+
payload[:duration] = (payload[:finished_at] - payload[:started_at]) * 1000
|
48
|
+
payload[:error] = e
|
49
|
+
payload[:exception] = [e.class.name, e.message]
|
50
|
+
|
51
|
+
notify_subscribers("#{event_name}.error", payload)
|
52
|
+
notify_subscribers(event_name, payload)
|
53
|
+
raise
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def enable!
|
58
|
+
@enabled = true
|
59
|
+
end
|
60
|
+
|
61
|
+
def disable!
|
62
|
+
@enabled = false
|
63
|
+
end
|
64
|
+
|
65
|
+
def enabled?
|
66
|
+
@enabled
|
67
|
+
end
|
68
|
+
|
69
|
+
def clear_subscribers!
|
70
|
+
@subscribers.clear
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def notify_subscribers(event, payload)
|
76
|
+
return unless @enabled
|
77
|
+
|
78
|
+
@subscribers[event].each do |subscription|
|
79
|
+
subscription.call(payload)
|
80
|
+
rescue StandardError => e
|
81
|
+
# Log but don't let subscriber errors break the flow
|
82
|
+
NatsWork::Logger.error('Instrumentation subscriber error',
|
83
|
+
event: event,
|
84
|
+
error: e.message)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Also notify wildcard subscribers
|
88
|
+
@subscribers['*'].each do |subscription|
|
89
|
+
subscription.call(payload)
|
90
|
+
rescue StandardError => e
|
91
|
+
NatsWork::Logger.error('Wildcard instrumentation subscriber error',
|
92
|
+
event: event,
|
93
|
+
error: e.message)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Subscription
|
98
|
+
attr_reader :event, :callback
|
99
|
+
|
100
|
+
def initialize(event, callback)
|
101
|
+
@event = event.to_s
|
102
|
+
@callback = callback
|
103
|
+
end
|
104
|
+
|
105
|
+
def call(payload)
|
106
|
+
@callback.call(payload)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# ActiveSupport::Notifications compatibility layer
|
111
|
+
module ActiveSupportCompatibility
|
112
|
+
def self.included(base)
|
113
|
+
base.extend(ClassMethods)
|
114
|
+
end
|
115
|
+
|
116
|
+
module ClassMethods
|
117
|
+
def instrument(event, payload = {}, &block)
|
118
|
+
if defined?(ActiveSupport::Notifications)
|
119
|
+
ActiveSupport::Notifications.instrument("natswork.#{event}", payload, &block)
|
120
|
+
else
|
121
|
+
NatsWork::Instrumentation.instrument(event, payload, &block)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def subscribe(pattern, &block)
|
126
|
+
if defined?(ActiveSupport::Notifications)
|
127
|
+
ActiveSupport::Notifications.subscribe(/^natswork\.#{pattern}/) do |name, started, finished, _id, payload|
|
128
|
+
payload[:event] = name.sub('natswork.', '')
|
129
|
+
payload[:started_at] = started
|
130
|
+
payload[:finished_at] = finished
|
131
|
+
payload[:duration] = (finished - started) * 1000 if finished && started
|
132
|
+
block.call(payload)
|
133
|
+
end
|
134
|
+
else
|
135
|
+
NatsWork::Instrumentation.subscribe(pattern, &block)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,271 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'logger'
|
4
|
+
require_relative 'metrics'
|
5
|
+
require_relative 'instrumentation'
|
6
|
+
require_relative 'health_check'
|
7
|
+
|
8
|
+
module NatsWork
|
9
|
+
class JobExecutor
|
10
|
+
attr_reader :worker_name, :logger, :metrics
|
11
|
+
|
12
|
+
def initialize(worker_name, options = {})
|
13
|
+
@worker_name = worker_name
|
14
|
+
@logger = options[:logger] || Logger.new(namespace: "worker.#{worker_name}")
|
15
|
+
@metrics = options[:metrics] || Metrics.global
|
16
|
+
@middleware = options[:middleware] || []
|
17
|
+
@error_handlers = []
|
18
|
+
@job_filters = {
|
19
|
+
before: [],
|
20
|
+
after: [],
|
21
|
+
around: []
|
22
|
+
}
|
23
|
+
@skip_instrumentation = options[:skip_instrumentation] || false
|
24
|
+
|
25
|
+
setup_instrumentation unless @skip_instrumentation
|
26
|
+
end
|
27
|
+
|
28
|
+
def execute_job(job_message)
|
29
|
+
job_id = job_message['job_id']
|
30
|
+
job_class = job_message['job_class']
|
31
|
+
arguments = job_message['arguments'] || []
|
32
|
+
|
33
|
+
context = {
|
34
|
+
job_id: job_id,
|
35
|
+
job_class: job_class,
|
36
|
+
worker: @worker_name,
|
37
|
+
queue: job_message['queue'],
|
38
|
+
retry_count: job_message['retry_count'] || 0
|
39
|
+
}
|
40
|
+
|
41
|
+
filtered_args = filter_sensitive_data(arguments)
|
42
|
+
|
43
|
+
@logger.info('Starting job', context.merge(
|
44
|
+
arguments: filtered_args,
|
45
|
+
enqueued_at: job_message['created_at']
|
46
|
+
))
|
47
|
+
|
48
|
+
@metrics.increment('jobs.started', 1,
|
49
|
+
queue: job_message['queue'],
|
50
|
+
job_class: job_class)
|
51
|
+
|
52
|
+
result = nil
|
53
|
+
start_time = Time.now
|
54
|
+
|
55
|
+
begin
|
56
|
+
# Instrument the job execution
|
57
|
+
Instrumentation.instrument('job.execute', context) do
|
58
|
+
# Run before filters
|
59
|
+
run_filters(:before, job_message, context)
|
60
|
+
|
61
|
+
# Execute the actual job
|
62
|
+
result = @metrics.time('job.execution_time',
|
63
|
+
queue: job_message['queue'],
|
64
|
+
job_class: job_class) do
|
65
|
+
perform_job(job_class, arguments)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Run after filters
|
69
|
+
run_filters(:after, job_message, context.merge(result: result))
|
70
|
+
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
75
|
+
|
76
|
+
@logger.info('Completed job', context.merge(
|
77
|
+
duration_ms: duration_ms,
|
78
|
+
result_class: result.class.name
|
79
|
+
))
|
80
|
+
|
81
|
+
@metrics.increment('jobs.completed', 1,
|
82
|
+
queue: job_message['queue'],
|
83
|
+
job_class: job_class,
|
84
|
+
status: 'success')
|
85
|
+
|
86
|
+
@metrics.histogram('job.duration', duration_ms,
|
87
|
+
queue: job_message['queue'],
|
88
|
+
job_class: job_class)
|
89
|
+
|
90
|
+
result
|
91
|
+
rescue StandardError => e
|
92
|
+
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
93
|
+
|
94
|
+
error_context = context.merge(
|
95
|
+
duration_ms: duration_ms,
|
96
|
+
error_class: e.class.name,
|
97
|
+
error_message: e.message,
|
98
|
+
backtrace: e.backtrace&.first(10)
|
99
|
+
)
|
100
|
+
|
101
|
+
@logger.error('Job failed', error_context)
|
102
|
+
|
103
|
+
@metrics.increment('jobs.failed', 1,
|
104
|
+
queue: job_message['queue'],
|
105
|
+
job_class: job_class,
|
106
|
+
error_class: e.class.name)
|
107
|
+
|
108
|
+
@metrics.histogram('job.duration', duration_ms,
|
109
|
+
queue: job_message['queue'],
|
110
|
+
job_class: job_class,
|
111
|
+
status: 'error')
|
112
|
+
|
113
|
+
# Run error handlers
|
114
|
+
handle_job_error(e, job_message, context)
|
115
|
+
|
116
|
+
raise e
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def add_middleware(middleware)
|
121
|
+
@middleware << middleware
|
122
|
+
end
|
123
|
+
|
124
|
+
def add_error_handler(&block)
|
125
|
+
@error_handlers << block if block_given?
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_job_filter(type, &block)
|
129
|
+
@job_filters[type.to_sym] << block if block_given?
|
130
|
+
end
|
131
|
+
|
132
|
+
def on_job_retry(&block)
|
133
|
+
add_job_filter(:retry, &block)
|
134
|
+
end
|
135
|
+
|
136
|
+
def on_job_failure(&block)
|
137
|
+
add_error_handler(&block)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def setup_instrumentation
|
143
|
+
# Subscribe to job events for additional logging
|
144
|
+
Instrumentation.subscribe('job.execute') do |payload|
|
145
|
+
if payload[:error]
|
146
|
+
logger.error('Job instrumentation error',
|
147
|
+
event: payload[:event],
|
148
|
+
job_id: payload[:job_id],
|
149
|
+
error: payload[:exception])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Subscribe to metrics for health monitoring
|
154
|
+
Instrumentation.subscribe('job.execute.error') do |payload|
|
155
|
+
# Track error patterns
|
156
|
+
metrics.increment('errors.by_class', 1,
|
157
|
+
error_class: payload[:exception]&.first,
|
158
|
+
job_class: payload[:job_class])
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def perform_job(job_class_name, arguments)
|
163
|
+
# This is where the actual job class would be instantiated and performed
|
164
|
+
# For now, simulate job execution
|
165
|
+
job_class = begin
|
166
|
+
Object.const_get(job_class_name)
|
167
|
+
rescue StandardError
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
|
171
|
+
# Ensure arguments are symbolized if it's a hash
|
172
|
+
args = if arguments.is_a?(Hash)
|
173
|
+
arguments.transform_keys(&:to_sym)
|
174
|
+
elsif arguments.is_a?(Array) && arguments.length == 1 && arguments[0].is_a?(Hash)
|
175
|
+
# If single hash argument in array, extract and symbolize
|
176
|
+
arguments[0].transform_keys(&:to_sym)
|
177
|
+
else
|
178
|
+
arguments
|
179
|
+
end
|
180
|
+
|
181
|
+
if job_class.respond_to?(:perform_async)
|
182
|
+
# Handle NatsWork::Job classes
|
183
|
+
job_instance = job_class.new
|
184
|
+
job_instance.perform(args)
|
185
|
+
elsif job_class.respond_to?(:perform)
|
186
|
+
# Handle plain Ruby classes with perform method
|
187
|
+
job_class.perform(args)
|
188
|
+
else
|
189
|
+
raise ArgumentError, "Unknown job class or missing perform method: #{job_class_name}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def run_filters(type, job_message, context)
|
194
|
+
@job_filters[type].each do |filter|
|
195
|
+
filter.call(job_message, context)
|
196
|
+
rescue StandardError => e
|
197
|
+
@logger.warn('Job filter error',
|
198
|
+
filter_type: type,
|
199
|
+
job_id: context[:job_id],
|
200
|
+
error: e.message)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def handle_job_error(error, job_message, context)
|
205
|
+
@error_handlers.each do |handler|
|
206
|
+
handler.call(error, job_message, context)
|
207
|
+
rescue StandardError => e
|
208
|
+
@logger.error('Error handler failed',
|
209
|
+
job_id: context[:job_id],
|
210
|
+
handler_error: e.message)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Emit error tracking event
|
214
|
+
Instrumentation.instrument('job.error', context.merge(
|
215
|
+
error: error,
|
216
|
+
error_class: error.class.name,
|
217
|
+
error_message: error.message
|
218
|
+
))
|
219
|
+
end
|
220
|
+
|
221
|
+
def filter_sensitive_data(arguments)
|
222
|
+
# Filter out sensitive data from job arguments for logging
|
223
|
+
case arguments
|
224
|
+
when Hash
|
225
|
+
filter_hash(arguments)
|
226
|
+
when Array
|
227
|
+
arguments.map do |arg|
|
228
|
+
case arg
|
229
|
+
when Hash
|
230
|
+
filter_hash(arg)
|
231
|
+
when String
|
232
|
+
arg.length > 1000 ? "#{arg[0, 100]}...[truncated]" : arg
|
233
|
+
else
|
234
|
+
arg
|
235
|
+
end
|
236
|
+
end
|
237
|
+
else
|
238
|
+
arguments
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def filter_hash(hash)
|
243
|
+
filtered = {}
|
244
|
+
|
245
|
+
hash.each do |key, value|
|
246
|
+
key_str = key.to_s.downcase
|
247
|
+
|
248
|
+
filtered[key] = if sensitive_key?(key_str)
|
249
|
+
'[FILTERED]'
|
250
|
+
elsif value.is_a?(Hash)
|
251
|
+
filter_hash(value)
|
252
|
+
elsif value.is_a?(String) && value.length > 1000
|
253
|
+
"#{value[0, 100]}...[truncated]"
|
254
|
+
else
|
255
|
+
value
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
filtered
|
260
|
+
end
|
261
|
+
|
262
|
+
def sensitive_key?(key)
|
263
|
+
sensitive_patterns = %w[
|
264
|
+
password passwd pass secret token key credential auth
|
265
|
+
ssn social_security credit_card cvv pin private_key
|
266
|
+
]
|
267
|
+
|
268
|
+
sensitive_patterns.any? { |pattern| key.include?(pattern) }
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NatsWork
|
4
|
+
class JobHooks
|
5
|
+
VALID_HOOKS = %i[before after retry failure success].freeze
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def global
|
9
|
+
@global ||= new
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@hooks = {}
|
15
|
+
VALID_HOOKS.each { |hook| @hooks[hook] = [] }
|
16
|
+
end
|
17
|
+
|
18
|
+
def register(type, &block)
|
19
|
+
raise ArgumentError, "Invalid hook type: #{type}" unless VALID_HOOKS.include?(type)
|
20
|
+
|
21
|
+
@hooks[type] << block
|
22
|
+
end
|
23
|
+
|
24
|
+
def before(&block)
|
25
|
+
register(:before, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def after(&block)
|
29
|
+
register(:after, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def on_retry(&block)
|
33
|
+
register(:retry, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_failure(&block)
|
37
|
+
register(:failure, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_success(&block)
|
41
|
+
register(:success, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
def run(type, job, job_message, *args)
|
45
|
+
return unless @hooks[type]
|
46
|
+
|
47
|
+
@hooks[type].each do |hook|
|
48
|
+
hook.call(job, job_message, *args)
|
49
|
+
rescue StandardError
|
50
|
+
# Log error but don't fail job execution
|
51
|
+
# In production, use proper logging
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def clear(type = nil)
|
56
|
+
if type
|
57
|
+
@hooks[type] = [] if @hooks[type]
|
58
|
+
else
|
59
|
+
@hooks.keys.each { |key| @hooks[key] = [] }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'json'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
module NatsWork
|
8
|
+
class Logger
|
9
|
+
LEVELS = {
|
10
|
+
debug: ::Logger::DEBUG,
|
11
|
+
info: ::Logger::INFO,
|
12
|
+
warn: ::Logger::WARN,
|
13
|
+
error: ::Logger::ERROR,
|
14
|
+
fatal: ::Logger::FATAL
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
attr_reader :logger, :namespace, :structured
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
@namespace = options[:namespace] || 'natswork'
|
21
|
+
@structured = options.fetch(:structured, false)
|
22
|
+
@output = options[:output] || $stdout
|
23
|
+
@level = options[:level] || :info
|
24
|
+
@formatter = options[:formatter]
|
25
|
+
|
26
|
+
setup_logger
|
27
|
+
end
|
28
|
+
|
29
|
+
def debug(message, context = {}, &block)
|
30
|
+
log(:debug, message, context, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def info(message, context = {}, &block)
|
34
|
+
log(:info, message, context, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def warn(message, context = {}, &block)
|
38
|
+
log(:warn, message, context, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def error(message, context = {}, &block)
|
42
|
+
log(:error, message, context, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def fatal(message, context = {}, &block)
|
46
|
+
log(:fatal, message, context, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_namespace(namespace)
|
50
|
+
self.class.new(
|
51
|
+
namespace: "#{@namespace}.#{namespace}",
|
52
|
+
structured: @structured,
|
53
|
+
output: @output,
|
54
|
+
level: @level,
|
55
|
+
formatter: @formatter
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def level=(level)
|
60
|
+
@level = level
|
61
|
+
@logger.level = LEVELS[level] || ::Logger::INFO
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def setup_logger
|
67
|
+
@logger = ::Logger.new(@output)
|
68
|
+
@logger.level = LEVELS[@level] || ::Logger::INFO
|
69
|
+
|
70
|
+
@logger.formatter = if @formatter
|
71
|
+
@formatter
|
72
|
+
elsif @structured
|
73
|
+
method(:json_formatter)
|
74
|
+
else
|
75
|
+
method(:text_formatter)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def log(level, message, context = {})
|
80
|
+
context = context.merge(
|
81
|
+
namespace: @namespace,
|
82
|
+
timestamp: Time.now.iso8601,
|
83
|
+
pid: Process.pid,
|
84
|
+
thread_id: Thread.current.object_id
|
85
|
+
)
|
86
|
+
|
87
|
+
if block_given?
|
88
|
+
start_time = Time.now
|
89
|
+
begin
|
90
|
+
result = yield
|
91
|
+
context[:duration] = ((Time.now - start_time) * 1000).round(2)
|
92
|
+
@logger.send(level, [message, context])
|
93
|
+
result
|
94
|
+
rescue StandardError => e
|
95
|
+
context[:duration] = ((Time.now - start_time) * 1000).round(2)
|
96
|
+
context[:error] = {
|
97
|
+
class: e.class.name,
|
98
|
+
message: e.message,
|
99
|
+
backtrace: e.backtrace&.first(10)
|
100
|
+
}
|
101
|
+
@logger.error([message, context])
|
102
|
+
raise
|
103
|
+
end
|
104
|
+
else
|
105
|
+
@logger.send(level, [message, context])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def json_formatter(severity, datetime, _progname, msg)
|
110
|
+
data = if msg.is_a?(Hash)
|
111
|
+
msg
|
112
|
+
elsif msg.is_a?(Array) && msg.size == 2
|
113
|
+
message, context = msg
|
114
|
+
context.merge(message: message)
|
115
|
+
else
|
116
|
+
{ message: msg.to_s }
|
117
|
+
end
|
118
|
+
|
119
|
+
data[:severity] = severity
|
120
|
+
data[:time] = datetime.iso8601
|
121
|
+
|
122
|
+
"#{JSON.dump(data)}\n"
|
123
|
+
end
|
124
|
+
|
125
|
+
def text_formatter(severity, datetime, _progname, msg)
|
126
|
+
if msg.is_a?(Array) && msg.size == 2
|
127
|
+
message, context = msg
|
128
|
+
context_str = context.reject { |k, _| %i[namespace timestamp pid thread_id].include?(k) }
|
129
|
+
.map { |k, v| "#{k}=#{v.inspect}" }
|
130
|
+
.join(' ')
|
131
|
+
|
132
|
+
"[#{datetime.iso8601}] [#{severity}] [#{context[:namespace]}] #{message}#{context_str.empty? ? '' : " #{context_str}"}\n"
|
133
|
+
else
|
134
|
+
"[#{datetime.iso8601}] [#{severity}] [#{@namespace}] #{msg}\n"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class << self
|
139
|
+
def global
|
140
|
+
@global ||= new
|
141
|
+
end
|
142
|
+
|
143
|
+
def configure(options = {})
|
144
|
+
@global = new(options)
|
145
|
+
end
|
146
|
+
|
147
|
+
def method_missing(method, *args, &block)
|
148
|
+
if global.respond_to?(method)
|
149
|
+
global.send(method, *args, &block)
|
150
|
+
else
|
151
|
+
super
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def respond_to_missing?(method, include_private = false)
|
156
|
+
global.respond_to?(method, include_private) || super
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# LoggerProxy allows for lazy initialization and swappable loggers
|
162
|
+
class LoggerProxy
|
163
|
+
def initialize(target = nil)
|
164
|
+
@target = target
|
165
|
+
end
|
166
|
+
|
167
|
+
attr_writer :target
|
168
|
+
|
169
|
+
def method_missing(method, *args, &block)
|
170
|
+
target.send(method, *args, &block)
|
171
|
+
end
|
172
|
+
|
173
|
+
def respond_to_missing?(method, include_private = false)
|
174
|
+
target.respond_to?(method, include_private) || super
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def target
|
180
|
+
@target ||= Logger.global
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|