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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +143 -0
  5. data/Gemfile.lock +6 -1
  6. data/README.md +289 -15
  7. data/docs/README.md +3 -1
  8. data/docs/addressing.md +119 -13
  9. data/docs/architecture.md +68 -0
  10. data/docs/dead_letter_queue.md +673 -0
  11. data/docs/dispatcher.md +87 -0
  12. data/docs/examples.md +59 -1
  13. data/docs/getting-started.md +8 -1
  14. data/docs/logging.md +382 -326
  15. data/docs/message_filtering.md +451 -0
  16. data/examples/01_point_to_point_orders.rb +54 -53
  17. data/examples/02_publish_subscribe_events.rb +14 -10
  18. data/examples/03_many_to_many_chat.rb +16 -8
  19. data/examples/04_redis_smart_home_iot.rb +20 -10
  20. data/examples/05_proc_handlers.rb +12 -11
  21. data/examples/06_custom_logger_example.rb +95 -100
  22. data/examples/07_error_handling_scenarios.rb +4 -2
  23. data/examples/08_entity_addressing_basic.rb +18 -6
  24. data/examples/08_entity_addressing_with_filtering.rb +27 -9
  25. data/examples/09_dead_letter_queue_demo.rb +559 -0
  26. data/examples/09_regex_filtering_microservices.rb +407 -0
  27. data/examples/10_header_block_configuration.rb +263 -0
  28. data/examples/11_global_configuration_example.rb +219 -0
  29. data/examples/README.md +102 -0
  30. data/examples/dead_letters.jsonl +12 -0
  31. data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
  32. data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
  33. data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
  34. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
  35. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
  36. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
  37. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
  38. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
  39. data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
  40. data/examples/performance_metrics/compare_benchmarks.rb +519 -0
  41. data/examples/performance_metrics/dead_letters.jsonl +3100 -0
  42. data/examples/performance_metrics/performance_benchmark.rb +344 -0
  43. data/examples/show_logger.rb +367 -0
  44. data/examples/show_me.rb +145 -0
  45. data/examples/temp.txt +94 -0
  46. data/examples/tmux_chat/bot_agent.rb +4 -2
  47. data/examples/tmux_chat/human_agent.rb +4 -2
  48. data/examples/tmux_chat/room_monitor.rb +4 -2
  49. data/examples/tmux_chat/shared_chat_system.rb +6 -3
  50. data/lib/smart_message/addressing.rb +259 -0
  51. data/lib/smart_message/base.rb +121 -599
  52. data/lib/smart_message/circuit_breaker.rb +23 -6
  53. data/lib/smart_message/configuration.rb +199 -0
  54. data/lib/smart_message/dead_letter_queue.rb +361 -0
  55. data/lib/smart_message/dispatcher.rb +90 -49
  56. data/lib/smart_message/header.rb +5 -0
  57. data/lib/smart_message/logger/base.rb +21 -1
  58. data/lib/smart_message/logger/default.rb +88 -138
  59. data/lib/smart_message/logger/lumberjack.rb +324 -0
  60. data/lib/smart_message/logger/null.rb +81 -0
  61. data/lib/smart_message/logger.rb +17 -9
  62. data/lib/smart_message/messaging.rb +100 -0
  63. data/lib/smart_message/plugins.rb +132 -0
  64. data/lib/smart_message/serializer/base.rb +25 -8
  65. data/lib/smart_message/serializer/json.rb +5 -4
  66. data/lib/smart_message/subscription.rb +193 -0
  67. data/lib/smart_message/transport/base.rb +84 -53
  68. data/lib/smart_message/transport/memory_transport.rb +7 -5
  69. data/lib/smart_message/transport/redis_transport.rb +15 -45
  70. data/lib/smart_message/transport/stdout_transport.rb +18 -8
  71. data/lib/smart_message/transport.rb +1 -34
  72. data/lib/smart_message/utilities.rb +142 -0
  73. data/lib/smart_message/version.rb +1 -1
  74. data/lib/smart_message/versioning.rb +85 -0
  75. data/lib/smart_message/wrapper.rb.bak +132 -0
  76. data/lib/smart_message.rb +74 -27
  77. data/smart_message.gemspec +3 -0
  78. metadata +77 -3
  79. data/lib/smart_message/serializer.rb +0 -10
  80. 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(Array.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] += [subscription]
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(Array.new)
133
+ @subscribers = Hash.new { |h, k| h[k] = [] }
121
134
  end
122
135
 
123
136
 
124
- # message_header is of class SmartMessage::Header
125
- # message_payload is a string buffer that is a serialized
126
- # SmartMessage
127
- def route(message_header, message_payload)
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
- return nil if @subscribers[message_klass].empty?
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, message_header, message_payload)
161
+ SmartMessage::Base.call_proc_handler(message_processor, decoded_message)
147
162
  else
148
- # Original method call logic
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(message_header, message_payload)
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, message_header, message_payload, message_processor)
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].include?(message_header.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].include?(message_header.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 message_header [SmartMessage::Header] The message header
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, message_header, message_payload, message_processor)
308
- # Log circuit breaker activation
309
- if $DEBUG
310
- puts "Circuit breaker activated for processor: #{message_processor}"
311
- puts "Error: #{circuit_result[:circuit_breaker][:error]}"
312
- puts "Message: #{message_header.message_class} from #{message_header.from}"
313
- end
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
- # For now, record the failure in simple stats
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
 
@@ -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 automatically detects and uses the best available logging option:
14
- # - Rails.logger if running in a Rails application
15
- # - Standard Ruby Logger writing to log/smart_message.log otherwise
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
- # # In your message class
19
- # config do
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
- # # Or with custom options
24
- # config do
25
- # logger SmartMessage::Logger::Default.new(
26
- # log_file: 'custom/path.log', # File path
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
- # # To log to STDOUT instead of a file
32
- # config do
33
- # logger SmartMessage::Logger::Default.new(
34
- # log_file: STDOUT, # STDOUT or STDERR
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 || default_log_file
43
- @level = level || default_log_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
- # Message lifecycle logging methods
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
- logger.debug(message, &block)
58
+ enhanced_log(:debug, message, caller_locations(1, 1).first, &block)
90
59
  end
91
-
60
+
92
61
  def info(message = nil, &block)
93
- logger.info(message, &block)
62
+ enhanced_log(:info, message, caller_locations(1, 1).first, &block)
94
63
  end
95
-
64
+
96
65
  def warn(message = nil, &block)
97
- logger.warn(message, &block)
66
+ enhanced_log(:warn, message, caller_locations(1, 1).first, &block)
98
67
  end
99
-
68
+
100
69
  def error(message = nil, &block)
101
- logger.error(message, &block)
70
+ enhanced_log(:error, message, caller_locations(1, 1).first, &block)
102
71
  end
103
-
72
+
104
73
  def fatal(message = nil, &block)
105
- logger.fatal(message, &block)
74
+ enhanced_log(:fatal, message, caller_locations(1, 1).first, &block)
106
75
  end
107
-
76
+
108
77
  private
109
-
110
- def setup_logger
111
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
112
- # Use Rails logger if available
113
- setup_rails_logger
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
- # Use standard Ruby logger
116
- setup_ruby_logger
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 setup_rails_logger
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
- FileUtils.mkdir_p(File.dirname(@log_file))
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 clean formatter
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
- "[#{timestamp}] #{severity.ljust(5)} -- : #{msg}\n"
147
- end
148
-
149
- ruby_logger
150
- end
151
-
152
- def default_log_file
153
- if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
154
- Rails.root.join('log', 'smart_message.log').to_s
155
- else
156
- 'log/smart_message.log'
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
- ::Logger::DEBUG
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