smart_message 0.0.7 → 0.0.9
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 +4 -4
- data/.gitignore +1 -0
- data/.irbrc +24 -0
- data/CHANGELOG.md +143 -0
- data/Gemfile.lock +6 -1
- data/README.md +289 -15
- data/docs/README.md +3 -1
- data/docs/addressing.md +119 -13
- data/docs/architecture.md +68 -0
- data/docs/dead_letter_queue.md +673 -0
- data/docs/dispatcher.md +87 -0
- data/docs/examples.md +59 -1
- data/docs/getting-started.md +8 -1
- data/docs/logging.md +382 -326
- data/docs/message_filtering.md +451 -0
- data/examples/01_point_to_point_orders.rb +54 -53
- data/examples/02_publish_subscribe_events.rb +14 -10
- data/examples/03_many_to_many_chat.rb +16 -8
- data/examples/04_redis_smart_home_iot.rb +20 -10
- data/examples/05_proc_handlers.rb +12 -11
- data/examples/06_custom_logger_example.rb +95 -100
- data/examples/07_error_handling_scenarios.rb +4 -2
- data/examples/08_entity_addressing_basic.rb +18 -6
- data/examples/08_entity_addressing_with_filtering.rb +27 -9
- data/examples/09_dead_letter_queue_demo.rb +559 -0
- data/examples/09_regex_filtering_microservices.rb +407 -0
- data/examples/10_header_block_configuration.rb +263 -0
- data/examples/11_global_configuration_example.rb +219 -0
- data/examples/README.md +102 -0
- data/examples/dead_letters.jsonl +12 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
- data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
- data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
- data/examples/performance_metrics/compare_benchmarks.rb +519 -0
- data/examples/performance_metrics/dead_letters.jsonl +3100 -0
- data/examples/performance_metrics/performance_benchmark.rb +344 -0
- data/examples/show_logger.rb +367 -0
- data/examples/show_me.rb +145 -0
- data/examples/temp.txt +94 -0
- data/examples/tmux_chat/bot_agent.rb +4 -2
- data/examples/tmux_chat/human_agent.rb +4 -2
- data/examples/tmux_chat/room_monitor.rb +4 -2
- data/examples/tmux_chat/shared_chat_system.rb +6 -3
- data/lib/smart_message/addressing.rb +259 -0
- data/lib/smart_message/base.rb +121 -599
- data/lib/smart_message/circuit_breaker.rb +23 -6
- data/lib/smart_message/configuration.rb +199 -0
- data/lib/smart_message/dead_letter_queue.rb +361 -0
- data/lib/smart_message/dispatcher.rb +90 -49
- data/lib/smart_message/header.rb +5 -0
- data/lib/smart_message/logger/base.rb +21 -1
- data/lib/smart_message/logger/default.rb +88 -138
- data/lib/smart_message/logger/lumberjack.rb +324 -0
- data/lib/smart_message/logger/null.rb +81 -0
- data/lib/smart_message/logger.rb +17 -9
- data/lib/smart_message/messaging.rb +100 -0
- data/lib/smart_message/plugins.rb +132 -0
- data/lib/smart_message/serializer/base.rb +25 -8
- data/lib/smart_message/serializer/json.rb +5 -4
- data/lib/smart_message/subscription.rb +193 -0
- data/lib/smart_message/transport/base.rb +84 -53
- data/lib/smart_message/transport/memory_transport.rb +7 -5
- data/lib/smart_message/transport/redis_transport.rb +15 -45
- data/lib/smart_message/transport/stdout_transport.rb +18 -8
- data/lib/smart_message/transport.rb +1 -34
- data/lib/smart_message/utilities.rb +142 -0
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message/versioning.rb +85 -0
- data/lib/smart_message/wrapper.rb.bak +132 -0
- data/lib/smart_message.rb +74 -27
- data/smart_message.gemspec +3 -0
- metadata +77 -3
- data/lib/smart_message/serializer.rb +0 -10
- data/lib/smart_message/wrapper.rb +0 -43
@@ -15,15 +15,28 @@ module SmartMessage
|
|
15
15
|
# TODO: setup forwardable for some @router_pool methods
|
16
16
|
|
17
17
|
def initialize(circuit_breaker_options = {})
|
18
|
-
@subscribers = Hash.new
|
18
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
19
19
|
@router_pool = Concurrent::CachedThreadPool.new
|
20
|
-
|
20
|
+
|
21
21
|
# Configure circuit breakers
|
22
22
|
configure_circuit_breakers(circuit_breaker_options)
|
23
23
|
at_exit do
|
24
24
|
shutdown_pool
|
25
25
|
end
|
26
|
+
|
27
|
+
logger.debug { "[SmartMessage::Dispatcher] Initialized with circuit breaker options: #{circuit_breaker_options}" }
|
28
|
+
rescue => e
|
29
|
+
logger.error { "[SmartMessage] Error in dispatcher initialization: #{e.class.name} - #{e.message}" }
|
30
|
+
raise
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def logger
|
36
|
+
@logger ||= SmartMessage::Logger.default
|
26
37
|
end
|
38
|
+
|
39
|
+
public
|
27
40
|
|
28
41
|
|
29
42
|
def what_can_i_do?
|
@@ -84,20 +97,20 @@ module SmartMessage
|
|
84
97
|
|
85
98
|
def add(message_class, process_method_as_string, filter_options = {})
|
86
99
|
klass = String(message_class)
|
87
|
-
|
100
|
+
|
88
101
|
# Create subscription entry with filter options
|
89
102
|
subscription = {
|
90
103
|
process_method: process_method_as_string,
|
91
104
|
filters: filter_options
|
92
105
|
}
|
93
|
-
|
106
|
+
|
94
107
|
# Check if this exact subscription already exists
|
95
108
|
existing_subscription = @subscribers[klass].find do |sub|
|
96
109
|
sub[:process_method] == process_method_as_string && sub[:filters] == filter_options
|
97
110
|
end
|
98
|
-
|
111
|
+
|
99
112
|
unless existing_subscription
|
100
|
-
@subscribers[klass]
|
113
|
+
@subscribers[klass] << subscription
|
101
114
|
end
|
102
115
|
end
|
103
116
|
|
@@ -117,25 +130,27 @@ module SmartMessage
|
|
117
130
|
|
118
131
|
# complete reset all subscriptions
|
119
132
|
def drop_all!
|
120
|
-
@subscribers = Hash.new
|
133
|
+
@subscribers = Hash.new { |h, k| h[k] = [] }
|
121
134
|
end
|
122
135
|
|
123
136
|
|
124
|
-
#
|
125
|
-
#
|
126
|
-
|
127
|
-
|
137
|
+
# Route a decoded message to appropriate message processors
|
138
|
+
# @param decoded_message [SmartMessage::Base] The decoded message instance
|
139
|
+
def route(decoded_message)
|
140
|
+
message_header = decoded_message._sm_header
|
128
141
|
message_klass = message_header.message_class
|
129
|
-
|
130
|
-
|
142
|
+
logger.debug { "[SmartMessage::Dispatcher] Routing message #{message_klass} to #{@subscribers[message_klass]&.size || 0} subscribers" }
|
143
|
+
logger.debug { "[SmartMessage::Dispatcher] Available subscribers: #{@subscribers.keys}" }
|
144
|
+
return nil if @subscribers[message_klass].nil? || @subscribers[message_klass].empty?
|
145
|
+
|
131
146
|
@subscribers[message_klass].each do |subscription|
|
132
147
|
# Extract subscription details
|
133
148
|
message_processor = subscription[:process_method]
|
134
149
|
filters = subscription[:filters]
|
135
|
-
|
150
|
+
|
136
151
|
# Check if message matches filters
|
137
152
|
next unless message_matches_filters?(message_header, filters)
|
138
|
-
|
153
|
+
|
139
154
|
SS.add(message_klass, message_processor, 'routed' )
|
140
155
|
@router_pool.post do
|
141
156
|
# Use circuit breaker to protect message processing
|
@@ -143,21 +158,21 @@ module SmartMessage
|
|
143
158
|
# Check if this is a proc handler or a regular method call
|
144
159
|
if proc_handler?(message_processor)
|
145
160
|
# Call the proc handler via SmartMessage::Base
|
146
|
-
SmartMessage::Base.call_proc_handler(message_processor,
|
161
|
+
SmartMessage::Base.call_proc_handler(message_processor, decoded_message)
|
147
162
|
else
|
148
|
-
#
|
163
|
+
# Method call logic with decoded message
|
149
164
|
parts = message_processor.split('.')
|
150
165
|
target_klass = parts[0]
|
151
166
|
class_method = parts[1]
|
152
167
|
target_klass.constantize
|
153
168
|
.method(class_method)
|
154
|
-
.call(
|
169
|
+
.call(decoded_message)
|
155
170
|
end
|
156
171
|
end
|
157
|
-
|
172
|
+
|
158
173
|
# Handle circuit breaker fallback responses
|
159
174
|
if circuit_result.is_a?(Hash) && circuit_result[:circuit_breaker]
|
160
|
-
handle_circuit_breaker_fallback(circuit_result,
|
175
|
+
handle_circuit_breaker_fallback(circuit_result, decoded_message, message_processor)
|
161
176
|
end
|
162
177
|
end
|
163
178
|
end
|
@@ -167,7 +182,7 @@ module SmartMessage
|
|
167
182
|
# @return [Hash] Circuit breaker statistics
|
168
183
|
def circuit_breaker_stats
|
169
184
|
stats = {}
|
170
|
-
|
185
|
+
|
171
186
|
begin
|
172
187
|
if respond_to?(:circuit)
|
173
188
|
breaker = circuit(:message_processor)
|
@@ -186,7 +201,7 @@ module SmartMessage
|
|
186
201
|
rescue => e
|
187
202
|
stats[:error] = "Failed to get circuit breaker stats: #{e.message}"
|
188
203
|
end
|
189
|
-
|
204
|
+
|
190
205
|
stats
|
191
206
|
end
|
192
207
|
|
@@ -204,7 +219,7 @@ module SmartMessage
|
|
204
219
|
# Shutdown the router pool with timeout and fallback
|
205
220
|
def shutdown_pool
|
206
221
|
@router_pool.shutdown
|
207
|
-
|
222
|
+
|
208
223
|
# Wait for graceful shutdown, force kill if timeout
|
209
224
|
unless @router_pool.wait_for_termination(3)
|
210
225
|
@router_pool.kill
|
@@ -218,26 +233,46 @@ module SmartMessage
|
|
218
233
|
def message_matches_filters?(message_header, filters)
|
219
234
|
# If no filters specified, accept all messages (backward compatibility)
|
220
235
|
return true if filters.nil? || filters.empty? || filters.values.all?(&:nil?)
|
221
|
-
|
236
|
+
|
222
237
|
# Check from filter
|
223
238
|
if filters[:from]
|
224
|
-
from_match = filters[:from]
|
239
|
+
from_match = filter_value_matches?(message_header.from, filters[:from])
|
225
240
|
return false unless from_match
|
226
241
|
end
|
227
|
-
|
242
|
+
|
228
243
|
# Check to/broadcast filters (OR logic between them)
|
229
244
|
if filters[:broadcast] || filters[:to]
|
230
245
|
broadcast_match = filters[:broadcast] && message_header.to.nil?
|
231
|
-
to_match = filters[:to] && filters[:to]
|
232
|
-
|
246
|
+
to_match = filters[:to] && filter_value_matches?(message_header.to, filters[:to])
|
247
|
+
|
233
248
|
# If either broadcast or to filter is specified, at least one must match
|
234
249
|
combined_match = (broadcast_match || to_match)
|
235
250
|
return false unless combined_match
|
236
251
|
end
|
237
|
-
|
252
|
+
|
238
253
|
true
|
239
254
|
end
|
240
255
|
|
256
|
+
# Check if a value matches any of the filter criteria
|
257
|
+
# Supports both exact string matching and regex pattern matching
|
258
|
+
# @param value [String, nil] The value to match against
|
259
|
+
# @param filter_array [Array] Array of strings and/or regexps to match against
|
260
|
+
# @return [Boolean] True if the value matches any filter in the array
|
261
|
+
def filter_value_matches?(value, filter_array)
|
262
|
+
return false if value.nil? || filter_array.nil?
|
263
|
+
|
264
|
+
filter_array.any? do |filter|
|
265
|
+
case filter
|
266
|
+
when String
|
267
|
+
filter == value
|
268
|
+
when Regexp
|
269
|
+
filter.match?(value)
|
270
|
+
else
|
271
|
+
false
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
241
276
|
# Check if a message processor is a proc handler
|
242
277
|
# @param message_processor [String] The message processor identifier
|
243
278
|
# @return [Boolean] True if this is a proc handler
|
@@ -250,19 +285,19 @@ module SmartMessage
|
|
250
285
|
def configure_circuit_breakers(options = {})
|
251
286
|
# Ensure CircuitBreaker module is available
|
252
287
|
return unless defined?(SmartMessage::CircuitBreaker::DEFAULT_CONFIGS)
|
253
|
-
|
288
|
+
|
254
289
|
# Configure message processor circuit breaker
|
255
290
|
default_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:message_processor]
|
256
291
|
return unless default_config
|
257
|
-
|
292
|
+
|
258
293
|
processor_config = default_config.merge(options[:message_processor] || {})
|
259
|
-
|
294
|
+
|
260
295
|
# Define the circuit using the class-level DSL
|
261
296
|
self.class.circuit :message_processor do
|
262
|
-
threshold failures: processor_config[:threshold][:failures],
|
297
|
+
threshold failures: processor_config[:threshold][:failures],
|
263
298
|
within: processor_config[:threshold][:within].seconds
|
264
299
|
reset_after processor_config[:reset_after].seconds
|
265
|
-
|
300
|
+
|
266
301
|
# Configure storage backend
|
267
302
|
case processor_config[:storage]
|
268
303
|
when :redis
|
@@ -281,7 +316,7 @@ module SmartMessage
|
|
281
316
|
else
|
282
317
|
storage BreakerMachines::Storage::Memory.new
|
283
318
|
end
|
284
|
-
|
319
|
+
|
285
320
|
# Default fallback for message processing failures
|
286
321
|
fallback do |exception|
|
287
322
|
{
|
@@ -296,27 +331,33 @@ module SmartMessage
|
|
296
331
|
}
|
297
332
|
end
|
298
333
|
end
|
299
|
-
|
334
|
+
|
300
335
|
end
|
301
336
|
|
302
337
|
# Handle circuit breaker fallback responses
|
303
338
|
# @param circuit_result [Hash] The circuit breaker fallback result
|
304
|
-
# @param
|
305
|
-
# @param message_payload [String] The message payload
|
339
|
+
# @param decoded_message [SmartMessage::Base] The decoded message instance
|
306
340
|
# @param message_processor [String] The processor that failed
|
307
|
-
def handle_circuit_breaker_fallback(circuit_result,
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
341
|
+
def handle_circuit_breaker_fallback(circuit_result, decoded_message, message_processor)
|
342
|
+
message_header = decoded_message._sm_header
|
343
|
+
|
344
|
+
# Always log circuit breaker activation for debugging
|
345
|
+
error_msg = circuit_result[:circuit_breaker][:error]
|
346
|
+
logger.error { "[SmartMessage::Dispatcher] Circuit breaker activated for processor: #{message_processor}" }
|
347
|
+
logger.error { "[SmartMessage::Dispatcher] Error: #{error_msg}" }
|
348
|
+
logger.error { "[SmartMessage::Dispatcher] Message: #{message_header.message_class} from #{message_header.from}" }
|
349
|
+
|
350
|
+
# Send to dead letter queue
|
351
|
+
SmartMessage::DeadLetterQueue.default.enqueue(decoded_message,
|
352
|
+
error: circuit_result[:circuit_breaker][:error],
|
353
|
+
retry_count: 0,
|
354
|
+
transport: 'circuit_breaker'
|
355
|
+
)
|
356
|
+
|
315
357
|
# TODO: Integrate with structured logging when implemented
|
316
|
-
# TODO: Send to dead letter queue when implemented
|
317
358
|
# TODO: Emit metrics/events for monitoring
|
318
|
-
|
319
|
-
#
|
359
|
+
|
360
|
+
# Record the failure in simple stats
|
320
361
|
SS.add(message_header.message_class, message_processor, 'circuit_breaker_fallback')
|
321
362
|
end
|
322
363
|
|
data/lib/smart_message/header.rb
CHANGED
@@ -57,5 +57,10 @@ module SmartMessage
|
|
57
57
|
property :reply_to,
|
58
58
|
required: false,
|
59
59
|
description: "Optional unique identifier of the entity that should receive replies to this message. Defaults to 'from' entity if not specified"
|
60
|
+
|
61
|
+
# Serialization tracking for wrapper architecture
|
62
|
+
property :serializer,
|
63
|
+
required: false,
|
64
|
+
description: "Class name of the serializer used to encode the payload (e.g., 'SmartMessage::Serializer::Json'). Used by DLQ and cross-serializer gateway patterns"
|
60
65
|
end
|
61
66
|
end
|
@@ -4,5 +4,25 @@
|
|
4
4
|
|
5
5
|
module SmartMessage::Logger
|
6
6
|
class Base
|
7
|
+
# Standard logging methods that subclasses should implement
|
8
|
+
def debug(message = nil, &block)
|
9
|
+
raise NotImplementedError, "Subclass must implement #debug"
|
10
|
+
end
|
11
|
+
|
12
|
+
def info(message = nil, &block)
|
13
|
+
raise NotImplementedError, "Subclass must implement #info"
|
14
|
+
end
|
15
|
+
|
16
|
+
def warn(message = nil, &block)
|
17
|
+
raise NotImplementedError, "Subclass must implement #warn"
|
18
|
+
end
|
19
|
+
|
20
|
+
def error(message = nil, &block)
|
21
|
+
raise NotImplementedError, "Subclass must implement #error"
|
22
|
+
end
|
23
|
+
|
24
|
+
def fatal(message = nil, &block)
|
25
|
+
raise NotImplementedError, "Subclass must implement #fatal"
|
26
|
+
end
|
7
27
|
end
|
8
|
-
end # module SmartMessage::Logger
|
28
|
+
end # module SmartMessage::Logger
|
@@ -9,170 +9,141 @@ require 'stringio'
|
|
9
9
|
module SmartMessage
|
10
10
|
module Logger
|
11
11
|
# Default logger implementation for SmartMessage
|
12
|
-
#
|
13
|
-
# This logger
|
14
|
-
#
|
15
|
-
#
|
12
|
+
#
|
13
|
+
# This logger provides a simple Ruby Logger wrapper with enhanced formatting.
|
14
|
+
# Applications can easily configure Rails.logger or other loggers through
|
15
|
+
# the global configuration system instead.
|
16
16
|
#
|
17
17
|
# Usage:
|
18
|
-
# #
|
19
|
-
#
|
20
|
-
# logger SmartMessage::Logger::Default.new
|
18
|
+
# # Use default file logging
|
19
|
+
# SmartMessage.configure do |config|
|
20
|
+
# config.logger = SmartMessage::Logger::Default.new
|
21
21
|
# end
|
22
22
|
#
|
23
|
-
# #
|
24
|
-
#
|
25
|
-
# logger SmartMessage::Logger::Default.new(
|
26
|
-
# log_file: 'custom/path.log',
|
23
|
+
# # Use custom options
|
24
|
+
# SmartMessage.configure do |config|
|
25
|
+
# config.logger = SmartMessage::Logger::Default.new(
|
26
|
+
# log_file: 'custom/path.log',
|
27
27
|
# level: Logger::DEBUG
|
28
28
|
# )
|
29
29
|
# end
|
30
30
|
#
|
31
|
-
# #
|
32
|
-
#
|
33
|
-
# logger SmartMessage::Logger::Default.new(
|
34
|
-
# log_file: STDOUT,
|
31
|
+
# # Log to STDOUT
|
32
|
+
# SmartMessage.configure do |config|
|
33
|
+
# config.logger = SmartMessage::Logger::Default.new(
|
34
|
+
# log_file: STDOUT,
|
35
35
|
# level: Logger::INFO
|
36
36
|
# )
|
37
37
|
# end
|
38
|
+
#
|
39
|
+
# # Use Rails logger instead
|
40
|
+
# SmartMessage.configure do |config|
|
41
|
+
# config.logger = Rails.logger
|
42
|
+
# end
|
38
43
|
class Default < Base
|
39
44
|
attr_reader :logger, :log_file, :level
|
40
|
-
|
45
|
+
|
41
46
|
def initialize(log_file: nil, level: nil)
|
42
|
-
@log_file = log_file ||
|
43
|
-
@level = level ||
|
44
|
-
|
47
|
+
@log_file = log_file || 'log/smart_message.log'
|
48
|
+
@level = level || ::Logger::INFO
|
49
|
+
|
45
50
|
@logger = setup_logger
|
46
51
|
end
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
def log_message_created(message)
|
51
|
-
logger.debug { "[SmartMessage] Created: #{message.class.name} - #{message_summary(message)}" }
|
52
|
-
end
|
53
|
-
|
54
|
-
def log_message_published(message, transport)
|
55
|
-
logger.info { "[SmartMessage] Published: #{message.class.name} via #{transport.class.name.split('::').last}" }
|
56
|
-
end
|
57
|
-
|
58
|
-
def log_message_received(message_class, payload)
|
59
|
-
logger.info { "[SmartMessage] Received: #{message_class.name} (#{payload.bytesize} bytes)" }
|
60
|
-
end
|
61
|
-
|
62
|
-
def log_message_processed(message_class, result)
|
63
|
-
logger.info { "[SmartMessage] Processed: #{message_class.name} - #{truncate(result.to_s, 100)}" }
|
64
|
-
end
|
65
|
-
|
66
|
-
def log_message_subscribe(message_class, handler = nil)
|
67
|
-
handler_desc = handler ? " with handler: #{handler}" : ""
|
68
|
-
logger.info { "[SmartMessage] Subscribed: #{message_class.name}#{handler_desc}" }
|
69
|
-
end
|
70
|
-
|
71
|
-
def log_message_unsubscribe(message_class)
|
72
|
-
logger.info { "[SmartMessage] Unsubscribed: #{message_class.name}" }
|
73
|
-
end
|
74
|
-
|
75
|
-
# Error logging
|
76
|
-
|
77
|
-
def log_error(context, error)
|
78
|
-
logger.error { "[SmartMessage] Error in #{context}: #{error.class.name} - #{error.message}" }
|
79
|
-
logger.debug { "[SmartMessage] Backtrace:\n#{error.backtrace.join("\n")}" } if error.backtrace
|
80
|
-
end
|
81
|
-
|
82
|
-
def log_warning(message)
|
83
|
-
logger.warn { "[SmartMessage] Warning: #{message}" }
|
84
|
-
end
|
85
|
-
|
52
|
+
|
53
|
+
|
86
54
|
# General purpose logging methods matching Ruby's Logger interface
|
87
|
-
|
55
|
+
# These methods capture caller information and embed it in the log message
|
56
|
+
|
88
57
|
def debug(message = nil, &block)
|
89
|
-
|
58
|
+
enhanced_log(:debug, message, caller_locations(1, 1).first, &block)
|
90
59
|
end
|
91
|
-
|
60
|
+
|
92
61
|
def info(message = nil, &block)
|
93
|
-
|
62
|
+
enhanced_log(:info, message, caller_locations(1, 1).first, &block)
|
94
63
|
end
|
95
|
-
|
64
|
+
|
96
65
|
def warn(message = nil, &block)
|
97
|
-
|
66
|
+
enhanced_log(:warn, message, caller_locations(1, 1).first, &block)
|
98
67
|
end
|
99
|
-
|
68
|
+
|
100
69
|
def error(message = nil, &block)
|
101
|
-
|
70
|
+
enhanced_log(:error, message, caller_locations(1, 1).first, &block)
|
102
71
|
end
|
103
|
-
|
72
|
+
|
104
73
|
def fatal(message = nil, &block)
|
105
|
-
|
74
|
+
enhanced_log(:fatal, message, caller_locations(1, 1).first, &block)
|
106
75
|
end
|
107
|
-
|
76
|
+
|
108
77
|
private
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
78
|
+
|
79
|
+
# Enhanced logging method that embeds caller information
|
80
|
+
def enhanced_log(level, message, caller_location, &block)
|
81
|
+
if caller_location
|
82
|
+
file_path = caller_location.path
|
83
|
+
line_number = caller_location.lineno
|
84
|
+
|
85
|
+
# If a block is provided, call it to get the message
|
86
|
+
if block_given?
|
87
|
+
actual_message = block.call
|
88
|
+
else
|
89
|
+
actual_message = message
|
90
|
+
end
|
91
|
+
|
92
|
+
# Embed caller info in the message
|
93
|
+
enhanced_message = "[#{file_path}:#{line_number}] #{actual_message}"
|
94
|
+
logger.send(level, enhanced_message)
|
114
95
|
else
|
115
|
-
#
|
116
|
-
|
96
|
+
# Fallback if caller information is not available
|
97
|
+
if block_given?
|
98
|
+
logger.send(level, &block)
|
99
|
+
else
|
100
|
+
logger.send(level, message)
|
101
|
+
end
|
117
102
|
end
|
118
103
|
end
|
119
|
-
|
120
|
-
def
|
121
|
-
# Wrap Rails.logger to ensure our messages are properly tagged
|
122
|
-
RailsLoggerWrapper.new(Rails.logger, level: @level)
|
123
|
-
end
|
124
|
-
|
125
|
-
def setup_ruby_logger
|
104
|
+
|
105
|
+
def setup_logger
|
126
106
|
# Handle IO objects (STDOUT, STDERR) vs file paths
|
127
107
|
if @log_file.is_a?(IO) || @log_file.is_a?(StringIO)
|
128
108
|
# For STDOUT/STDERR, don't use rotation
|
129
109
|
ruby_logger = ::Logger.new(@log_file)
|
130
110
|
else
|
131
111
|
# For file paths, ensure directory exists and use rotation
|
132
|
-
|
133
|
-
|
112
|
+
log_dir = File.dirname(@log_file)
|
113
|
+
FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
|
114
|
+
|
134
115
|
ruby_logger = ::Logger.new(
|
135
116
|
@log_file,
|
136
117
|
10, # Keep 10 old log files
|
137
118
|
10_485_760 # Rotate when file reaches 10MB
|
138
119
|
)
|
139
120
|
end
|
140
|
-
|
121
|
+
|
141
122
|
ruby_logger.level = @level
|
142
|
-
|
143
|
-
# Set a
|
123
|
+
|
124
|
+
# Set a formatter that includes file and line number
|
144
125
|
ruby_logger.formatter = proc do |severity, datetime, progname, msg|
|
145
126
|
timestamp = datetime.strftime('%Y-%m-%d %H:%M:%S.%3N')
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
def default_log_level
|
161
|
-
if defined?(Rails) && Rails.respond_to?(:env)
|
162
|
-
case Rails.env
|
163
|
-
when 'production'
|
164
|
-
::Logger::INFO
|
165
|
-
when 'test'
|
166
|
-
::Logger::ERROR
|
127
|
+
|
128
|
+
# Extract caller information if it's embedded in the message
|
129
|
+
if msg.is_a?(String) && msg.match(/\A\[(.+?):(\d+)\] (.+)\z/)
|
130
|
+
file_path = $1
|
131
|
+
line_number = $2
|
132
|
+
actual_msg = $3
|
133
|
+
|
134
|
+
# Get just the filename from the full path
|
135
|
+
filename = File.basename(file_path)
|
136
|
+
|
137
|
+
"[#{timestamp}] #{severity.ljust(5)} -- #{filename}:#{line_number} : #{actual_msg}\n"
|
167
138
|
else
|
168
|
-
|
139
|
+
"[#{timestamp}] #{severity.ljust(5)} -- : #{msg}\n"
|
169
140
|
end
|
170
|
-
else
|
171
|
-
# Default to INFO for non-Rails environments
|
172
|
-
::Logger::INFO
|
173
141
|
end
|
142
|
+
|
143
|
+
ruby_logger
|
174
144
|
end
|
175
|
-
|
145
|
+
|
146
|
+
|
176
147
|
def message_summary(message)
|
177
148
|
# Create a brief summary of the message for logging
|
178
149
|
if message.respond_to?(:to_h)
|
@@ -185,33 +156,12 @@ module SmartMessage
|
|
185
156
|
truncate(message.inspect, 200)
|
186
157
|
end
|
187
158
|
end
|
188
|
-
|
159
|
+
|
189
160
|
def truncate(string, max_length)
|
190
161
|
return string if string.length <= max_length
|
191
162
|
"#{string[0...max_length]}..."
|
192
163
|
end
|
193
|
-
|
194
|
-
# Internal wrapper for Rails.logger to handle tagged logging
|
195
|
-
class RailsLoggerWrapper
|
196
|
-
def initialize(rails_logger, level: nil)
|
197
|
-
@rails_logger = rails_logger
|
198
|
-
@rails_logger.level = level if level
|
199
|
-
end
|
200
|
-
|
201
|
-
def method_missing(method, *args, &block)
|
202
|
-
if @rails_logger.respond_to?(:tagged)
|
203
|
-
@rails_logger.tagged('SmartMessage') do
|
204
|
-
@rails_logger.send(method, *args, &block)
|
205
|
-
end
|
206
|
-
else
|
207
|
-
@rails_logger.send(method, *args, &block)
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def respond_to_missing?(method, include_private = false)
|
212
|
-
@rails_logger.respond_to?(method, include_private)
|
213
|
-
end
|
214
|
-
end
|
164
|
+
|
215
165
|
end
|
216
166
|
end
|
217
|
-
end
|
167
|
+
end
|