smart_message 0.0.8 → 0.0.10

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +119 -0
  5. data/Gemfile.lock +6 -1
  6. data/README.md +389 -17
  7. data/docs/README.md +3 -1
  8. data/docs/addressing.md +119 -13
  9. data/docs/architecture.md +184 -46
  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_deduplication.md +488 -0
  16. data/docs/message_filtering.md +451 -0
  17. data/examples/01_point_to_point_orders.rb +54 -53
  18. data/examples/02_publish_subscribe_events.rb +14 -10
  19. data/examples/03_many_to_many_chat.rb +16 -8
  20. data/examples/04_redis_smart_home_iot.rb +20 -10
  21. data/examples/05_proc_handlers.rb +12 -11
  22. data/examples/06_custom_logger_example.rb +95 -100
  23. data/examples/07_error_handling_scenarios.rb +4 -2
  24. data/examples/08_entity_addressing_basic.rb +18 -6
  25. data/examples/08_entity_addressing_with_filtering.rb +27 -9
  26. data/examples/09_dead_letter_queue_demo.rb +559 -0
  27. data/examples/09_regex_filtering_microservices.rb +407 -0
  28. data/examples/10_header_block_configuration.rb +263 -0
  29. data/examples/10_message_deduplication.rb +209 -0
  30. data/examples/11_global_configuration_example.rb +219 -0
  31. data/examples/README.md +102 -0
  32. data/examples/dead_letters.jsonl +12 -0
  33. data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
  34. data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
  35. data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
  36. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
  37. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
  38. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
  39. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
  40. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
  41. data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
  42. data/examples/performance_metrics/compare_benchmarks.rb +519 -0
  43. data/examples/performance_metrics/dead_letters.jsonl +3100 -0
  44. data/examples/performance_metrics/performance_benchmark.rb +344 -0
  45. data/examples/show_logger.rb +367 -0
  46. data/examples/show_me.rb +145 -0
  47. data/examples/temp.txt +94 -0
  48. data/examples/tmux_chat/bot_agent.rb +4 -2
  49. data/examples/tmux_chat/human_agent.rb +4 -2
  50. data/examples/tmux_chat/room_monitor.rb +4 -2
  51. data/examples/tmux_chat/shared_chat_system.rb +6 -3
  52. data/lib/smart_message/addressing.rb +259 -0
  53. data/lib/smart_message/base.rb +123 -599
  54. data/lib/smart_message/circuit_breaker.rb +2 -1
  55. data/lib/smart_message/configuration.rb +199 -0
  56. data/lib/smart_message/ddq/base.rb +71 -0
  57. data/lib/smart_message/ddq/memory.rb +109 -0
  58. data/lib/smart_message/ddq/redis.rb +168 -0
  59. data/lib/smart_message/ddq.rb +31 -0
  60. data/lib/smart_message/dead_letter_queue.rb +27 -10
  61. data/lib/smart_message/deduplication.rb +174 -0
  62. data/lib/smart_message/dispatcher.rb +259 -61
  63. data/lib/smart_message/header.rb +5 -0
  64. data/lib/smart_message/logger/base.rb +21 -1
  65. data/lib/smart_message/logger/default.rb +88 -138
  66. data/lib/smart_message/logger/lumberjack.rb +324 -0
  67. data/lib/smart_message/logger/null.rb +81 -0
  68. data/lib/smart_message/logger.rb +17 -9
  69. data/lib/smart_message/messaging.rb +100 -0
  70. data/lib/smart_message/plugins.rb +132 -0
  71. data/lib/smart_message/serializer/base.rb +25 -8
  72. data/lib/smart_message/serializer/json.rb +5 -4
  73. data/lib/smart_message/subscription.rb +196 -0
  74. data/lib/smart_message/transport/base.rb +72 -41
  75. data/lib/smart_message/transport/memory_transport.rb +7 -5
  76. data/lib/smart_message/transport/redis_transport.rb +15 -45
  77. data/lib/smart_message/transport/stdout_transport.rb +18 -8
  78. data/lib/smart_message/transport.rb +1 -34
  79. data/lib/smart_message/utilities.rb +142 -0
  80. data/lib/smart_message/version.rb +1 -1
  81. data/lib/smart_message/versioning.rb +85 -0
  82. data/lib/smart_message/wrapper.rb.bak +132 -0
  83. data/lib/smart_message.rb +74 -28
  84. data/smart_message.gemspec +3 -0
  85. metadata +83 -3
  86. data/lib/smart_message/serializer.rb +0 -10
  87. data/lib/smart_message/wrapper.rb +0 -43
@@ -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
@@ -0,0 +1,324 @@
1
+ # lib/smart_message/logger/lumberjack.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'lumberjack'
6
+ require 'fileutils'
7
+ require 'colorize'
8
+
9
+ module SmartMessage
10
+ module Logger
11
+ # Lumberjack-based logger implementation for SmartMessage
12
+ #
13
+ # This logger uses the Lumberjack gem to provide enhanced structured logging
14
+ # with automatic source location tracking, structured data support, and
15
+ # better performance characteristics.
16
+ #
17
+ # Features:
18
+ # - Automatic source location tracking (file:line) for every log entry
19
+ # - Structured data logging for message headers and payloads
20
+ # - Better performance than standard Ruby Logger
21
+ # - JSON output support for machine-readable logs
22
+ # - Customizable formatters and devices
23
+ #
24
+ # Usage:
25
+ # # Basic usage with file logging
26
+ # config do
27
+ # logger SmartMessage::Logger::Lumberjack.new
28
+ # end
29
+ #
30
+ # # Custom configuration
31
+ # config do
32
+ # logger SmartMessage::Logger::Lumberjack.new(
33
+ # log_file: 'custom/smart_message.log',
34
+ # level: :debug,
35
+ # format: :json, # :text or :json
36
+ # include_source: true, # Include source location
37
+ # structured_data: true, # Log structured message data
38
+ # colorize: true, # Enable colorized output (console only)
39
+ # roll_by_date: true, # Enable date-based log rolling
40
+ # max_file_size: 50 * 1024 * 1024 # Max file size before rolling (50 MB)
41
+ # )
42
+ # end
43
+ #
44
+ # # Log to STDOUT with colorized JSON format
45
+ # config do
46
+ # logger SmartMessage::Logger::Lumberjack.new(
47
+ # log_file: STDOUT,
48
+ # format: :json,
49
+ # colorize: true
50
+ # )
51
+ # end
52
+ class Lumberjack < Base
53
+ attr_reader :logger, :log_file, :level, :format, :include_source, :structured_data, :colorize
54
+
55
+ def initialize(log_file: nil, level: nil, format: :text, include_source: true, structured_data: true, colorize: false, **options)
56
+ @log_file = log_file || default_log_file
57
+ @level = level || default_log_level
58
+ @format = format
59
+ @include_source = include_source
60
+ @structured_data = structured_data
61
+ @options = options
62
+
63
+ # Set colorize after @log_file is set so console_output? works correctly
64
+ @colorize = colorize && console_output?
65
+
66
+ @logger = setup_lumberjack_logger
67
+ end
68
+
69
+
70
+ # General purpose logging methods matching Ruby's Logger interface
71
+ # These methods capture caller information and add structured data
72
+
73
+ def debug(message = nil, **structured_data, &block)
74
+ return unless message || block_given?
75
+ structured_data[:message] = message if message
76
+ log_with_caller(:debug, 1, structured_data, &block)
77
+ end
78
+
79
+ def info(message = nil, **structured_data, &block)
80
+ return unless message || block_given?
81
+ structured_data[:message] = message if message
82
+ log_with_caller(:info, 1, structured_data, &block)
83
+ end
84
+
85
+ def warn(message = nil, **structured_data, &block)
86
+ return unless message || block_given?
87
+ structured_data[:message] = message if message
88
+ log_with_caller(:warn, 1, structured_data, &block)
89
+ end
90
+
91
+ def error(message = nil, **structured_data, &block)
92
+ return unless message || block_given?
93
+ structured_data[:message] = message if message
94
+ log_with_caller(:error, 1, structured_data, &block)
95
+ end
96
+
97
+ def fatal(message = nil, **structured_data, &block)
98
+ return unless message || block_given?
99
+ structured_data[:message] = message if message
100
+ log_with_caller(:fatal, 1, structured_data, &block)
101
+ end
102
+
103
+ # Check if output is going to console (STDOUT/STDERR)
104
+ def console_output?
105
+ @log_file == STDOUT || @log_file == STDERR
106
+ end
107
+
108
+ private
109
+
110
+ def setup_lumberjack_logger
111
+ # Create log directory if needed
112
+ if @log_file.is_a?(String) && !@log_file.start_with?('/dev/')
113
+ log_dir = File.dirname(@log_file)
114
+ FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
115
+ end
116
+
117
+ # For STDOUT/STDERR, use Device::Writer with template for colorization
118
+ if console_output?
119
+ device = ::Lumberjack::Device::Writer.new(@log_file, template: create_template)
120
+ ::Lumberjack::Logger.new(device, level: normalize_level(@level))
121
+ else
122
+ # Build Lumberjack options for file-based logging
123
+ lumberjack_options = {
124
+ level: normalize_level(@level),
125
+ formatter: create_formatter,
126
+ buffer_size: 0 # Disable buffering for immediate output
127
+ }
128
+
129
+ # Add log rolling options if specified
130
+ if @options[:roll_by_date]
131
+ lumberjack_options[:date_pattern] = @options[:date_pattern] || '%Y-%m-%d'
132
+ end
133
+
134
+ if @options[:roll_by_size]
135
+ lumberjack_options[:max_size] = @options[:max_file_size] || (10 * 1024 * 1024) # 10 MB
136
+ lumberjack_options[:keep] = @options[:keep_files] || 5
137
+ end
138
+
139
+ # Configure the Lumberjack logger
140
+ ::Lumberjack::Logger.new(@log_file, lumberjack_options)
141
+ end
142
+ end
143
+
144
+ def create_template
145
+ # Template for Device::Writer (used for console output)
146
+ colorize_enabled = @colorize
147
+ include_source = @include_source
148
+
149
+ case @format
150
+ when :json
151
+ # JSON template - return lambda that formats as JSON
152
+ lambda do |entry|
153
+ data = {
154
+ timestamp: entry.time.strftime('%Y-%m-%d %H:%M:%S.%3N'),
155
+ level: entry.severity_label,
156
+ message: entry.message
157
+ }
158
+
159
+ # Add source location if available
160
+ if include_source && entry.tags[:source]
161
+ data[:source] = entry.tags[:source]
162
+ end
163
+
164
+ # Add any structured data
165
+ entry.tags.each do |key, value|
166
+ next if key == :source
167
+ data[key] = value
168
+ end
169
+
170
+ data.to_json + "\n"
171
+ end
172
+ else
173
+ # Text template with optional colorization
174
+ lambda do |entry|
175
+ require 'colorize' if colorize_enabled
176
+
177
+ timestamp = entry.time.strftime('%Y-%m-%d %H:%M:%S.%3N')
178
+ level = entry.severity_label.ljust(5)
179
+ source_info = include_source && entry.tags[:source] ? " #{entry.tags[:source]}" : ""
180
+
181
+ line = "[#{timestamp}] #{level} --#{source_info} : #{entry.message}"
182
+
183
+ if colorize_enabled
184
+ # Apply colorization with custom color scheme
185
+ case entry.severity_label.downcase.to_sym
186
+ when :debug
187
+ # Debug: dark green background, white foreground, bold
188
+ line.white.bold.on_green
189
+ when :info
190
+ # Info: bright white foreground (no background)
191
+ line.light_white
192
+ when :warn
193
+ # Warn: yellow background, white foreground, bold
194
+ line.white.bold.on_yellow
195
+ when :error
196
+ # Error: red background, white foreground, bold
197
+ line.white.bold.on_red
198
+ when :fatal
199
+ # Fatal: bright red background, yellow foreground, bold
200
+ line.yellow.bold.on_light_red
201
+ else
202
+ line
203
+ end
204
+ else
205
+ line
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ def create_formatter
212
+ case @format
213
+ when :json
214
+ # JSON formatter with structured data support
215
+ ::Lumberjack::Formatter.new do |entry|
216
+ data = {
217
+ timestamp: entry.time.strftime('%Y-%m-%d %H:%M:%S.%3N'),
218
+ level: entry.severity_label,
219
+ message: entry.message
220
+ }
221
+
222
+ # Add source location if available
223
+ if @include_source && entry.tags[:source]
224
+ data[:source] = entry.tags[:source]
225
+ end
226
+
227
+ # Add any structured data
228
+ entry.tags.each do |key, value|
229
+ next if key == :source
230
+ data[key] = value
231
+ end
232
+
233
+ data.to_json + "\n"
234
+ end
235
+ else
236
+ # Text formatter for file output (no colorization)
237
+ ::Lumberjack::Formatter.new do |entry|
238
+ timestamp = entry.time.strftime('%Y-%m-%d %H:%M:%S.%3N')
239
+ level = entry.severity_label.ljust(5)
240
+ source_info = @include_source && entry.tags[:source] ? " #{entry.tags[:source]}" : ""
241
+
242
+ "[#{timestamp}] #{level} --#{source_info} : #{entry.message}\n"
243
+ end
244
+ end
245
+ end
246
+
247
+ def log_with_caller(level, caller_depth, structured_data = {}, &block)
248
+ return unless @logger.send("#{level}?")
249
+
250
+ caller_info = get_caller_info(caller_depth + 1)
251
+ tags = structured_data.dup
252
+ tags[:source] = caller_info if @include_source && caller_info
253
+
254
+ if block_given?
255
+ message = block.call(caller_info)
256
+ else
257
+ message = structured_data.delete(:message) || ""
258
+ end
259
+
260
+ if tags.empty?
261
+ @logger.send(level, message)
262
+ else
263
+ @logger.tag(tags) do
264
+ @logger.send(level, message)
265
+ end
266
+ end
267
+ end
268
+
269
+
270
+ def get_caller_info(depth)
271
+ return nil unless @include_source
272
+
273
+ caller_location = caller_locations(depth + 1, 1).first
274
+ return nil unless caller_location
275
+
276
+ filename = File.basename(caller_location.path)
277
+ line_number = caller_location.lineno
278
+ "#{filename}:#{line_number}"
279
+ end
280
+
281
+ def normalize_level(level)
282
+ case level
283
+ when Symbol
284
+ case level
285
+ when :debug then ::Lumberjack::Severity::DEBUG
286
+ when :info then ::Lumberjack::Severity::INFO
287
+ when :warn then ::Lumberjack::Severity::WARN
288
+ when :error then ::Lumberjack::Severity::ERROR
289
+ when :fatal then ::Lumberjack::Severity::FATAL
290
+ else ::Lumberjack::Severity::INFO
291
+ end
292
+ when Integer
293
+ level
294
+ else
295
+ ::Lumberjack::Severity::INFO
296
+ end
297
+ end
298
+
299
+ def default_log_file
300
+ if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
301
+ Rails.root.join('log', 'smart_message.log').to_s
302
+ else
303
+ 'log/smart_message.log'
304
+ end
305
+ end
306
+
307
+ def default_log_level
308
+ if defined?(Rails) && Rails.respond_to?(:env)
309
+ case Rails.env
310
+ when 'development', 'test'
311
+ :info
312
+ when 'production'
313
+ :info
314
+ else
315
+ :info
316
+ end
317
+ else
318
+ :info
319
+ end
320
+ end
321
+
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,81 @@
1
+ # lib/smart_message/logger/null.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage
6
+ module Logger
7
+ # Null logger implementation that discards all log messages
8
+ #
9
+ # This logger provides a no-op implementation of the standard logging
10
+ # interface. All log messages are silently discarded, making it useful
11
+ # for applications that want to completely disable SmartMessage logging.
12
+ #
13
+ # Usage:
14
+ # # Disable all SmartMessage logging
15
+ # SmartMessage.configure do |config|
16
+ # config.logger = SmartMessage::Logger::Null.new
17
+ # end
18
+ #
19
+ # # Or set logger to nil (framework will use Null logger automatically)
20
+ # SmartMessage.configure do |config|
21
+ # config.logger = nil
22
+ # end
23
+ class Null < Base
24
+ def initialize
25
+ # No setup needed for null logger
26
+ end
27
+
28
+ # All logging methods are no-ops that accept any arguments
29
+ # and return nil immediately without processing
30
+
31
+ def debug(*args, &block)
32
+ # Silently discard
33
+ nil
34
+ end
35
+
36
+ def info(*args, &block)
37
+ # Silently discard
38
+ nil
39
+ end
40
+
41
+ def warn(*args, &block)
42
+ # Silently discard
43
+ nil
44
+ end
45
+
46
+ def error(*args, &block)
47
+ # Silently discard
48
+ nil
49
+ end
50
+
51
+ def fatal(*args, &block)
52
+ # Silently discard
53
+ nil
54
+ end
55
+
56
+ # Additional methods that might be called on loggers
57
+
58
+ def level
59
+ ::Logger::FATAL + 1 # Higher than FATAL to ensure nothing logs
60
+ end
61
+
62
+ def level=(value)
63
+ # Ignore level changes
64
+ end
65
+
66
+ def close
67
+ # Nothing to close
68
+ end
69
+
70
+ def respond_to_missing?(method_name, include_private = false)
71
+ # Pretend to respond to any logging method
72
+ true
73
+ end
74
+
75
+ def method_missing(method_name, *args, &block)
76
+ # Silently handle any other logging calls
77
+ nil
78
+ end
79
+ end
80
+ end
81
+ end