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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -1
- data/Gemfile.lock +6 -1
- data/README.md +92 -14
- data/docs/README.md +1 -0
- data/docs/architecture.md +41 -8
- data/docs/dispatcher.md +52 -16
- data/docs/getting-started.md +64 -2
- data/docs/logging.md +452 -0
- data/docs/message_processing.md +423 -0
- data/docs/proc_handlers_summary.md +247 -0
- data/docs/transports.md +202 -8
- data/examples/.gitignore +2 -0
- data/examples/04_redis_smart_home_iot.rb +649 -0
- data/examples/05_proc_handlers.rb +181 -0
- data/examples/06_custom_logger_example.rb +620 -0
- data/examples/README.md +118 -3
- data/examples/smart_home_iot_dataflow.md +257 -0
- data/lib/smart_message/base.rb +94 -4
- data/lib/smart_message/dispatcher.rb +22 -6
- data/lib/smart_message/logger/default.rb +217 -0
- data/lib/smart_message/logger.rb +9 -1
- data/lib/smart_message/transport/redis_transport.rb +190 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +1 -0
- data/lib/smart_message/version.rb +1 -1
- data/smart_message.gemspec +1 -0
- metadata +25 -1
@@ -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
|
data/lib/smart_message/logger.rb
CHANGED
@@ -3,5 +3,13 @@
|
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
5
|
module SmartMessage::Logger
|
6
|
-
#
|
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
|
@@ -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
|
data/smart_message.gemspec
CHANGED
@@ -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.
|
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
|