smart_message 0.0.2 → 0.0.4

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.
@@ -0,0 +1,217 @@
1
+ # lib/smart_message/logger/default.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'logger'
6
+ require 'fileutils'
7
+ require 'stringio'
8
+
9
+ module SmartMessage
10
+ module Logger
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
16
+ #
17
+ # Usage:
18
+ # # In your message class
19
+ # config do
20
+ # logger SmartMessage::Logger::Default.new
21
+ # end
22
+ #
23
+ # # Or with custom options
24
+ # config do
25
+ # logger SmartMessage::Logger::Default.new(
26
+ # log_file: 'custom/path.log', # File path
27
+ # level: Logger::DEBUG
28
+ # )
29
+ # end
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
35
+ # level: Logger::INFO
36
+ # )
37
+ # end
38
+ class Default < Base
39
+ attr_reader :logger, :log_file, :level
40
+
41
+ def initialize(log_file: nil, level: nil)
42
+ @log_file = log_file || default_log_file
43
+ @level = level || default_log_level
44
+
45
+ @logger = setup_logger
46
+ 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
+
86
+ # General purpose logging methods matching Ruby's Logger interface
87
+
88
+ def debug(message = nil, &block)
89
+ logger.debug(message, &block)
90
+ end
91
+
92
+ def info(message = nil, &block)
93
+ logger.info(message, &block)
94
+ end
95
+
96
+ def warn(message = nil, &block)
97
+ logger.warn(message, &block)
98
+ end
99
+
100
+ def error(message = nil, &block)
101
+ logger.error(message, &block)
102
+ end
103
+
104
+ def fatal(message = nil, &block)
105
+ logger.fatal(message, &block)
106
+ end
107
+
108
+ 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
114
+ else
115
+ # Use standard Ruby logger
116
+ setup_ruby_logger
117
+ end
118
+ 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
126
+ # Handle IO objects (STDOUT, STDERR) vs file paths
127
+ if @log_file.is_a?(IO) || @log_file.is_a?(StringIO)
128
+ # For STDOUT/STDERR, don't use rotation
129
+ ruby_logger = ::Logger.new(@log_file)
130
+ else
131
+ # For file paths, ensure directory exists and use rotation
132
+ FileUtils.mkdir_p(File.dirname(@log_file))
133
+
134
+ ruby_logger = ::Logger.new(
135
+ @log_file,
136
+ 10, # Keep 10 old log files
137
+ 10_485_760 # Rotate when file reaches 10MB
138
+ )
139
+ end
140
+
141
+ ruby_logger.level = @level
142
+
143
+ # Set a clean formatter
144
+ ruby_logger.formatter = proc do |severity, datetime, progname, msg|
145
+ 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
167
+ else
168
+ ::Logger::DEBUG
169
+ end
170
+ else
171
+ # Default to INFO for non-Rails environments
172
+ ::Logger::INFO
173
+ end
174
+ end
175
+
176
+ def message_summary(message)
177
+ # Create a brief summary of the message for logging
178
+ if message.respond_to?(:to_h)
179
+ data = message.to_h
180
+ # Remove internal header for cleaner logs
181
+ data.delete(:_sm_header)
182
+ data.delete('_sm_header')
183
+ truncate(data.inspect, 200)
184
+ else
185
+ truncate(message.inspect, 200)
186
+ end
187
+ end
188
+
189
+ def truncate(string, max_length)
190
+ return string if string.length <= max_length
191
+ "#{string[0...max_length]}..."
192
+ 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
215
+ end
216
+ end
217
+ end
@@ -3,5 +3,13 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  module SmartMessage::Logger
6
- # TODO: write this
6
+ # Logger module provides logging capabilities for SmartMessage
7
+ # The Default logger automatically uses Rails.logger if available,
8
+ # otherwise falls back to a standard Ruby Logger
7
9
  end # module SmartMessage::Logger
10
+
11
+ # Load the base class first
12
+ require_relative 'logger/base'
13
+
14
+ # Load the default logger implementation
15
+ require_relative 'logger/default'
@@ -0,0 +1,190 @@
1
+ # lib/smart_message/transport/redis_transport.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'redis'
6
+ require 'securerandom'
7
+ require 'set'
8
+
9
+ module SmartMessage
10
+ module Transport
11
+ # Redis pub/sub transport for SmartMessage
12
+ # Uses message class name as the Redis channel name
13
+ class RedisTransport < Base
14
+ attr_reader :redis_pub, :redis_sub, :subscriber_thread
15
+
16
+ def default_options
17
+ {
18
+ url: 'redis://localhost:6379',
19
+ db: 0,
20
+ auto_subscribe: true,
21
+ reconnect_attempts: 5,
22
+ reconnect_delay: 1
23
+ }
24
+ end
25
+
26
+ def configure
27
+ @redis_pub = Redis.new(url: @options[:url], db: @options[:db])
28
+ @redis_sub = Redis.new(url: @options[:url], db: @options[:db])
29
+ @subscribed_channels = Set.new
30
+ @subscriber_thread = nil
31
+ @running = false
32
+ @mutex = Mutex.new
33
+
34
+ start_subscriber if @options[:auto_subscribe]
35
+ end
36
+
37
+ # Publish message to Redis channel using message class name
38
+ def publish(message_header, message_payload)
39
+ channel = message_header.message_class
40
+
41
+ begin
42
+ @redis_pub.publish(channel, message_payload)
43
+ rescue Redis::ConnectionError
44
+ retry_with_reconnect('publish') { @redis_pub.publish(channel, message_payload) }
45
+ end
46
+ end
47
+
48
+ # Subscribe to a message class (Redis channel)
49
+ def subscribe(message_class, process_method)
50
+ super(message_class, process_method)
51
+
52
+ @mutex.synchronize do
53
+ @subscribed_channels.add(message_class)
54
+ restart_subscriber if @running
55
+ end
56
+ end
57
+
58
+ # Unsubscribe from a specific message class and process method
59
+ def unsubscribe(message_class, process_method)
60
+ super(message_class, process_method)
61
+
62
+ @mutex.synchronize do
63
+ # If no more subscribers for this message class, unsubscribe from channel
64
+ if @dispatcher.subscribers[message_class].nil? || @dispatcher.subscribers[message_class].empty?
65
+ @subscribed_channels.delete(message_class)
66
+ restart_subscriber if @running
67
+ end
68
+ end
69
+ end
70
+
71
+ # Unsubscribe from all process methods for a message class
72
+ def unsubscribe!(message_class)
73
+ super(message_class)
74
+
75
+ @mutex.synchronize do
76
+ @subscribed_channels.delete(message_class)
77
+ restart_subscriber if @running
78
+ end
79
+ end
80
+
81
+ def connected?
82
+ begin
83
+ @redis_pub.ping == 'PONG' && @redis_sub.ping == 'PONG'
84
+ rescue Redis::ConnectionError
85
+ false
86
+ end
87
+ end
88
+
89
+ def connect
90
+ @redis_pub.ping
91
+ @redis_sub.ping
92
+ start_subscriber unless @running
93
+ end
94
+
95
+ def disconnect
96
+ stop_subscriber
97
+ @redis_pub.quit if @redis_pub
98
+ @redis_sub.quit if @redis_sub
99
+ end
100
+
101
+ private
102
+
103
+ def start_subscriber
104
+ return if @running
105
+
106
+ @running = true
107
+ @subscriber_thread = Thread.new do
108
+ begin
109
+ subscribe_to_channels
110
+ rescue => e
111
+ # Log error but don't crash the thread
112
+ puts "Redis subscriber error: #{e.message}" if @options[:debug]
113
+ retry_subscriber
114
+ end
115
+ end
116
+ end
117
+
118
+ def stop_subscriber
119
+ @running = false
120
+
121
+ if @subscriber_thread
122
+ @subscriber_thread.kill
123
+ @subscriber_thread.join(5) # Wait up to 5 seconds
124
+ @subscriber_thread = nil
125
+ end
126
+ end
127
+
128
+ def restart_subscriber
129
+ stop_subscriber
130
+ start_subscriber if @subscribed_channels.any?
131
+ end
132
+
133
+ def subscribe_to_channels
134
+ return unless @subscribed_channels.any?
135
+
136
+ begin
137
+ @redis_sub.subscribe(*@subscribed_channels) do |on|
138
+ on.message do |channel, message_payload|
139
+ # Create a header with the channel as message_class
140
+ message_header = SmartMessage::Header.new(
141
+ message_class: channel,
142
+ uuid: SecureRandom.uuid,
143
+ published_at: Time.now,
144
+ publisher_pid: 'redis_subscriber'
145
+ )
146
+
147
+ receive(message_header, message_payload)
148
+ end
149
+
150
+ on.subscribe do |channel, subscriptions|
151
+ puts "Subscribed to Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
152
+ end
153
+
154
+ on.unsubscribe do |channel, subscriptions|
155
+ puts "Unsubscribed from Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
156
+ end
157
+ end
158
+ rescue => e
159
+ # Silently handle connection errors during subscription
160
+ puts "Redis subscription error: #{e.class.name}" if @options[:debug]
161
+ retry_subscriber if @running
162
+ end
163
+ end
164
+
165
+ def retry_subscriber
166
+ return unless @running
167
+
168
+ sleep(@options[:reconnect_delay])
169
+ subscribe_to_channels if @running
170
+ end
171
+
172
+ def retry_with_reconnect(operation)
173
+ attempts = 0
174
+ begin
175
+ yield
176
+ rescue Redis::ConnectionError => e
177
+ attempts += 1
178
+ if attempts <= @options[:reconnect_attempts]
179
+ sleep(@options[:reconnect_delay])
180
+ # Reconnect
181
+ @redis_pub = Redis.new(url: @options[:url], db: @options[:db])
182
+ retry
183
+ else
184
+ raise e
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -53,6 +53,7 @@ module SmartMessage
53
53
  # Register built-in transports
54
54
  register(:stdout, SmartMessage::Transport::StdoutTransport)
55
55
  register(:memory, SmartMessage::Transport::MemoryTransport)
56
+ register(:redis, SmartMessage::Transport::RedisTransport)
56
57
  end
57
58
  end
58
59
  end
@@ -6,6 +6,7 @@ require_relative 'transport/base'
6
6
  require_relative 'transport/registry'
7
7
  require_relative 'transport/stdout_transport'
8
8
  require_relative 'transport/memory_transport'
9
+ require_relative 'transport/redis_transport'
9
10
 
10
11
  module SmartMessage
11
12
  # Transport layer abstraction for SmartMessage
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  module SmartMessage
6
- VERSION = "0.0.2"
6
+ VERSION = '0.0.4'
7
7
  end
@@ -39,6 +39,7 @@ Gem::Specification.new do |spec|
39
39
  spec.add_dependency 'hashie'
40
40
  spec.add_dependency 'activesupport'
41
41
  spec.add_dependency 'concurrent-ruby'
42
+ spec.add_dependency 'redis'
42
43
 
43
44
  spec.add_development_dependency 'bundler'
44
45
  spec.add_development_dependency 'rake'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_message
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: redis
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: bundler
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -190,14 +204,22 @@ files:
190
204
  - docs/examples.md
191
205
  - docs/getting-started.md
192
206
  - docs/ideas_to_think_about.md
207
+ - docs/logging.md
208
+ - docs/message_processing.md
209
+ - docs/proc_handlers_summary.md
193
210
  - docs/properties.md
194
211
  - docs/serializers.md
195
212
  - docs/transports.md
196
213
  - docs/troubleshooting.md
214
+ - examples/.gitignore
197
215
  - examples/01_point_to_point_orders.rb
198
216
  - examples/02_publish_subscribe_events.rb
199
217
  - examples/03_many_to_many_chat.rb
218
+ - examples/04_redis_smart_home_iot.rb
219
+ - examples/05_proc_handlers.rb
220
+ - examples/06_custom_logger_example.rb
200
221
  - examples/README.md
222
+ - examples/smart_home_iot_dataflow.md
201
223
  - examples/tmux_chat/README.md
202
224
  - examples/tmux_chat/bot_agent.rb
203
225
  - examples/tmux_chat/human_agent.rb
@@ -214,6 +236,7 @@ files:
214
236
  - lib/smart_message/header.rb
215
237
  - lib/smart_message/logger.rb
216
238
  - lib/smart_message/logger/base.rb
239
+ - lib/smart_message/logger/default.rb
217
240
  - lib/smart_message/property_descriptions.rb
218
241
  - lib/smart_message/serializer.rb
219
242
  - lib/smart_message/serializer/base.rb
@@ -221,6 +244,7 @@ files:
221
244
  - lib/smart_message/transport.rb
222
245
  - lib/smart_message/transport/base.rb
223
246
  - lib/smart_message/transport/memory_transport.rb
247
+ - lib/smart_message/transport/redis_transport.rb
224
248
  - lib/smart_message/transport/registry.rb
225
249
  - lib/smart_message/transport/stdout_transport.rb
226
250
  - lib/smart_message/version.rb