smart_message 0.0.10 → 0.0.12

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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +38 -0
  3. data/.gitignore +5 -0
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile.lock +35 -4
  6. data/README.md +169 -71
  7. data/Rakefile +29 -4
  8. data/docs/assets/images/ddq_architecture.svg +130 -0
  9. data/docs/assets/images/dlq_architecture.svg +115 -0
  10. data/docs/assets/images/enhanced-dual-publishing.svg +136 -0
  11. data/docs/assets/images/enhanced-fluent-api.svg +149 -0
  12. data/docs/assets/images/enhanced-microservices-routing.svg +115 -0
  13. data/docs/assets/images/enhanced-pattern-matching.svg +107 -0
  14. data/docs/assets/images/fluent-api-demo.svg +59 -0
  15. data/docs/assets/images/performance-comparison.svg +161 -0
  16. data/docs/assets/images/redis-basic-architecture.svg +53 -0
  17. data/docs/assets/images/redis-enhanced-architecture.svg +88 -0
  18. data/docs/assets/images/redis-queue-architecture.svg +101 -0
  19. data/docs/assets/images/smart_message.jpg +0 -0
  20. data/docs/assets/images/smart_message_walking.jpg +0 -0
  21. data/docs/assets/images/smartmessage_architecture_overview.svg +173 -0
  22. data/docs/assets/images/transport-comparison-matrix.svg +171 -0
  23. data/docs/assets/javascripts/mathjax.js +17 -0
  24. data/docs/assets/stylesheets/extra.css +51 -0
  25. data/docs/{addressing.md → core-concepts/addressing.md} +5 -7
  26. data/docs/{architecture.md → core-concepts/architecture.md} +78 -138
  27. data/docs/{dispatcher.md → core-concepts/dispatcher.md} +21 -21
  28. data/docs/{message_filtering.md → core-concepts/message-filtering.md} +2 -3
  29. data/docs/{message_processing.md → core-concepts/message-processing.md} +17 -17
  30. data/docs/{troubleshooting.md → development/troubleshooting.md} +7 -7
  31. data/docs/{examples.md → getting-started/examples.md} +115 -89
  32. data/docs/{getting-started.md → getting-started/quick-start.md} +47 -18
  33. data/docs/guides/redis-queue-getting-started.md +697 -0
  34. data/docs/guides/redis-queue-patterns.md +889 -0
  35. data/docs/guides/redis-queue-production.md +1091 -0
  36. data/docs/index.md +64 -0
  37. data/docs/{dead_letter_queue.md → reference/dead-letter-queue.md} +2 -3
  38. data/docs/{logging.md → reference/logging.md} +1 -1
  39. data/docs/{message_deduplication.md → reference/message-deduplication.md} +1 -0
  40. data/docs/{proc_handlers_summary.md → reference/proc-handlers.md} +7 -6
  41. data/docs/{serializers.md → reference/serializers.md} +3 -5
  42. data/docs/{transports.md → reference/transports.md} +133 -11
  43. data/docs/transports/memory-transport.md +374 -0
  44. data/docs/transports/redis-enhanced-transport.md +524 -0
  45. data/docs/transports/redis-queue-transport.md +1304 -0
  46. data/docs/transports/redis-transport-comparison.md +496 -0
  47. data/docs/transports/redis-transport.md +509 -0
  48. data/examples/README.md +98 -5
  49. data/examples/city_scenario/911_emergency_call_flow.svg +99 -0
  50. data/examples/city_scenario/README.md +515 -0
  51. data/examples/city_scenario/ai_visitor_intelligence_flow.svg +108 -0
  52. data/examples/city_scenario/citizen.rb +195 -0
  53. data/examples/city_scenario/city_diagram.svg +125 -0
  54. data/examples/city_scenario/common/health_monitor.rb +80 -0
  55. data/examples/city_scenario/common/logger.rb +30 -0
  56. data/examples/city_scenario/emergency_dispatch_center.rb +270 -0
  57. data/examples/city_scenario/fire_department.rb +446 -0
  58. data/examples/city_scenario/fire_emergency_flow.svg +95 -0
  59. data/examples/city_scenario/health_department.rb +100 -0
  60. data/examples/city_scenario/health_monitoring_system.svg +130 -0
  61. data/examples/city_scenario/house.rb +244 -0
  62. data/examples/city_scenario/local_bank.rb +217 -0
  63. data/examples/city_scenario/messages/emergency_911_message.rb +81 -0
  64. data/examples/city_scenario/messages/emergency_resolved_message.rb +43 -0
  65. data/examples/city_scenario/messages/fire_dispatch_message.rb +43 -0
  66. data/examples/city_scenario/messages/fire_emergency_message.rb +45 -0
  67. data/examples/city_scenario/messages/health_check_message.rb +22 -0
  68. data/examples/city_scenario/messages/health_status_message.rb +35 -0
  69. data/examples/city_scenario/messages/police_dispatch_message.rb +46 -0
  70. data/examples/city_scenario/messages/silent_alarm_message.rb +38 -0
  71. data/examples/city_scenario/police_department.rb +316 -0
  72. data/examples/city_scenario/redis_monitor.rb +129 -0
  73. data/examples/city_scenario/redis_stats.rb +743 -0
  74. data/examples/city_scenario/room_for_improvement.md +240 -0
  75. data/examples/city_scenario/security_emergency_flow.svg +95 -0
  76. data/examples/city_scenario/service_internal_architecture.svg +154 -0
  77. data/examples/city_scenario/smart_message_ai_agent.rb +364 -0
  78. data/examples/city_scenario/start_demo.sh +236 -0
  79. data/examples/city_scenario/stop_demo.sh +106 -0
  80. data/examples/city_scenario/visitor.rb +631 -0
  81. data/examples/{10_message_deduplication.rb → memory/01_message_deduplication_demo.rb} +1 -1
  82. data/examples/{09_dead_letter_queue_demo.rb → memory/02_dead_letter_queue_demo.rb} +13 -40
  83. data/examples/{01_point_to_point_orders.rb → memory/03_point_to_point_orders.rb} +1 -1
  84. data/examples/{02_publish_subscribe_events.rb → memory/04_publish_subscribe_events.rb} +2 -2
  85. data/examples/{03_many_to_many_chat.rb → memory/05_many_to_many_chat.rb} +4 -4
  86. data/examples/{show_me.rb → memory/06_pretty_print_demo.rb} +1 -1
  87. data/examples/{05_proc_handlers.rb → memory/07_proc_handlers_demo.rb} +2 -2
  88. data/examples/{06_custom_logger_example.rb → memory/08_custom_logger_demo.rb} +17 -14
  89. data/examples/{07_error_handling_scenarios.rb → memory/09_error_handling_demo.rb} +4 -4
  90. data/examples/{08_entity_addressing_basic.rb → memory/10_entity_addressing_basic.rb} +8 -8
  91. data/examples/{08_entity_addressing_with_filtering.rb → memory/11_entity_addressing_with_filtering.rb} +6 -6
  92. data/examples/{09_regex_filtering_microservices.rb → memory/12_regex_filtering_microservices.rb} +2 -2
  93. data/examples/{10_header_block_configuration.rb → memory/13_header_block_configuration.rb} +6 -6
  94. data/examples/{11_global_configuration_example.rb → memory/14_global_configuration_demo.rb} +19 -8
  95. data/examples/{show_logger.rb → memory/15_logger_demo.rb} +1 -1
  96. data/examples/memory/README.md +163 -0
  97. data/examples/memory/memory_transport_architecture.svg +90 -0
  98. data/examples/memory/point_to_point_pattern.svg +94 -0
  99. data/examples/memory/publish_subscribe_pattern.svg +125 -0
  100. data/examples/{04_redis_smart_home_iot.rb → redis/01_smart_home_iot_demo.rb} +5 -5
  101. data/examples/redis/README.md +230 -0
  102. data/examples/redis/alert_system_flow.svg +127 -0
  103. data/examples/redis/dashboard_status_flow.svg +107 -0
  104. data/examples/redis/device_command_flow.svg +113 -0
  105. data/examples/redis/redis_transport_architecture.svg +115 -0
  106. data/examples/{smart_home_iot_dataflow.md → redis/smart_home_iot_dataflow.md} +4 -116
  107. data/examples/redis/smart_home_system_architecture.svg +133 -0
  108. data/examples/redis_enhanced/README.md +319 -0
  109. data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +233 -0
  110. data/examples/redis_enhanced/enhanced_02_fluent_api.rb +331 -0
  111. data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +281 -0
  112. data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +419 -0
  113. data/examples/redis_queue/01_basic_messaging.rb +221 -0
  114. data/examples/redis_queue/01_comprehensive_examples.rb +508 -0
  115. data/examples/redis_queue/02_pattern_routing.rb +405 -0
  116. data/examples/redis_queue/03_fluent_api.rb +422 -0
  117. data/examples/redis_queue/04_load_balancing.rb +486 -0
  118. data/examples/redis_queue/05_microservices.rb +735 -0
  119. data/examples/redis_queue/06_emergency_alerts.rb +777 -0
  120. data/examples/redis_queue/07_queue_management.rb +587 -0
  121. data/examples/redis_queue/README.md +366 -0
  122. data/examples/redis_queue/enhanced_01_basic_patterns.rb +233 -0
  123. data/examples/redis_queue/enhanced_02_fluent_api.rb +331 -0
  124. data/examples/redis_queue/enhanced_03_dual_publishing.rb +281 -0
  125. data/examples/redis_queue/enhanced_04_advanced_routing.rb +419 -0
  126. data/examples/redis_queue/redis_queue_architecture.svg +148 -0
  127. data/ideas/README.md +41 -0
  128. data/ideas/agents.md +1001 -0
  129. data/ideas/database_transport.md +980 -0
  130. data/ideas/improvement.md +359 -0
  131. data/ideas/meshage.md +1788 -0
  132. data/ideas/message_discovery.md +178 -0
  133. data/ideas/message_schema.md +1381 -0
  134. data/lib/smart_message/.idea/.gitignore +8 -0
  135. data/lib/smart_message/.idea/markdown.xml +6 -0
  136. data/lib/smart_message/.idea/misc.xml +4 -0
  137. data/lib/smart_message/.idea/modules.xml +8 -0
  138. data/lib/smart_message/.idea/smart_message.iml +16 -0
  139. data/lib/smart_message/.idea/vcs.xml +6 -0
  140. data/lib/smart_message/addressing.rb +15 -0
  141. data/lib/smart_message/base.rb +0 -2
  142. data/lib/smart_message/configuration.rb +1 -1
  143. data/lib/smart_message/logger.rb +15 -4
  144. data/lib/smart_message/plugins.rb +5 -2
  145. data/lib/smart_message/serializer.rb +14 -0
  146. data/lib/smart_message/transport/redis_enhanced_transport.rb +399 -0
  147. data/lib/smart_message/transport/redis_queue_transport.rb +555 -0
  148. data/lib/smart_message/transport/registry.rb +1 -0
  149. data/lib/smart_message/transport.rb +34 -1
  150. data/lib/smart_message/version.rb +1 -1
  151. data/lib/smart_message.rb +5 -52
  152. data/mkdocs.yml +184 -0
  153. data/p2p_plan.md +326 -0
  154. data/p2p_roadmap.md +287 -0
  155. data/smart_message.gemspec +2 -0
  156. data/smart_message.svg +51 -0
  157. metadata +170 -44
  158. data/docs/README.md +0 -57
  159. data/examples/dead_letters.jsonl +0 -12
  160. data/examples/temp.txt +0 -94
  161. data/examples/tmux_chat/README.md +0 -283
  162. data/examples/tmux_chat/bot_agent.rb +0 -278
  163. data/examples/tmux_chat/human_agent.rb +0 -199
  164. data/examples/tmux_chat/room_monitor.rb +0 -160
  165. data/examples/tmux_chat/shared_chat_system.rb +0 -328
  166. data/examples/tmux_chat/start_chat_demo.sh +0 -190
  167. data/examples/tmux_chat/stop_chat_demo.sh +0 -22
  168. /data/docs/{properties.md → core-concepts/properties.md} +0 -0
  169. /data/docs/{ideas_to_think_about.md → development/ideas.md} +0 -0
@@ -0,0 +1,743 @@
1
+ #!/usr/bin/env ruby
2
+ # Redis Statistics Monitor for SmartMessage Demo
3
+ # Shows comprehensive pub/sub and performance statistics
4
+
5
+ require 'redis'
6
+ require 'json'
7
+ require 'io/console'
8
+
9
+ class RedisStats
10
+ def initialize
11
+ @redis = Redis.new
12
+ @start_time = Time.now
13
+ @message_counts = Hash.new(0)
14
+ @channel_stats = Hash.new { |h, k| h[k] = { last_count: 0, rate: 0, total_messages: 0 } }
15
+ @previous_stats = {}
16
+ @previous_command_stats = {}
17
+ @max_publish_rate = 0.0 # Track maximum publish rate seen
18
+ @max_rates = Hash.new(0.0) # Track max rates for each command
19
+ @baseline_messages = nil # Track starting message count
20
+ @session_messages = 0 # Messages since monitor started
21
+ @baseline_commands = {} # Track starting command counts
22
+ @session_commands = {} # Commands since monitor started
23
+ @refresh_rate = 2
24
+
25
+ # Get terminal size with fallback
26
+ if IO.console
27
+ @terminal_height, @terminal_width = IO.console.winsize
28
+ else
29
+ # Fallback for non-TTY environments
30
+ @terminal_height = (ENV['LINES'] || 24).to_i
31
+ @terminal_width = (ENV['COLUMNS'] || 80).to_i
32
+ end
33
+
34
+ # Initialize terminal
35
+ setup_terminal
36
+ show_startup_message
37
+ setup_signal_handlers
38
+ end
39
+
40
+ def setup_terminal
41
+ # Hide cursor, enable alternate screen buffer
42
+ print "\e[?25l" # Hide cursor
43
+ print "\e[?1049h" # Enable alternate screen buffer
44
+ print "\e[2J" # Clear screen
45
+ print "\e[H" # Move cursor to home position
46
+ end
47
+
48
+ def restore_terminal
49
+ # Show cursor, disable alternate screen buffer
50
+ print "\e[?1049l" # Disable alternate screen buffer
51
+ print "\e[?25h" # Show cursor
52
+ print "\e[0m" # Reset all attributes
53
+ print "\e[2J" # Clear screen
54
+ print "\e[H" # Move cursor to home position
55
+ system("stty echo 2>/dev/null") # Re-enable echo
56
+ end
57
+
58
+ def show_startup_message
59
+ clear_screen
60
+ center_text("📊 Redis Statistics Monitor for SmartMessage")
61
+ center_text("Monitoring Redis pub/sub and performance metrics...")
62
+ center_text("Press 'q' to quit, 'r' to refresh, '+/-' to change refresh rate")
63
+ center_text("")
64
+ center_text("Loading... please wait")
65
+ sleep(1)
66
+ end
67
+
68
+ def setup_signal_handlers
69
+ %w[INT TERM].each do |signal|
70
+ Signal.trap(signal) do
71
+ cleanup_and_exit
72
+ end
73
+ end
74
+ end
75
+
76
+ def cleanup_and_exit
77
+ restore_terminal
78
+ system("stty sane 2>/dev/null") # Reset terminal settings
79
+ system("tput reset 2>/dev/null") # Full terminal reset
80
+ puts "\n📊 Redis statistics monitor shutting down..."
81
+ show_final_summary
82
+ exit(0)
83
+ end
84
+
85
+ def start_monitoring
86
+ # Only set up interactive mode if we have a console
87
+ interactive_mode = IO.console && STDIN.tty?
88
+
89
+ if interactive_mode
90
+ # Make stdin non-blocking for key detection
91
+ STDIN.echo = false
92
+ STDIN.raw!
93
+ end
94
+
95
+ loop do
96
+ # Update terminal size if console is available
97
+ if IO.console
98
+ @terminal_height, @terminal_width = IO.console.winsize
99
+ end
100
+
101
+ # Check for keyboard input (non-blocking) only in interactive mode
102
+ if interactive_mode && IO.select([STDIN], nil, nil, 0)
103
+ key = STDIN.getc
104
+ case key
105
+ when 'q', 'Q', "\e" # q, Q, or Escape to quit
106
+ cleanup_and_exit
107
+ when 'r', 'R' # r or R to refresh immediately
108
+ # Just continue the loop to refresh
109
+ when '+' # Increase refresh rate
110
+ @refresh_rate = [@refresh_rate - 0.5, 0.5].max
111
+ show_status_message("Refresh rate: #{@refresh_rate}s") if interactive_mode
112
+ when '-' # Decrease refresh rate
113
+ @refresh_rate = [@refresh_rate + 0.5, 10.0].min
114
+ show_status_message("Refresh rate: #{@refresh_rate}s") if interactive_mode
115
+ when 'h', 'H', '?' # Help
116
+ show_help_overlay if interactive_mode
117
+ end
118
+ end
119
+
120
+ render_dashboard
121
+ sleep(@refresh_rate)
122
+ end
123
+ rescue => e
124
+ restore_terminal
125
+ system("stty sane 2>/dev/null")
126
+ system("tput reset 2>/dev/null")
127
+ puts "❌ Error monitoring Redis stats: #{e.message}"
128
+ puts "#{e.backtrace.first(3).join("\n")}"
129
+ exit(1)
130
+ ensure
131
+ restore_terminal
132
+ system("stty sane 2>/dev/null")
133
+ system("tput reset 2>/dev/null")
134
+ end
135
+
136
+ def render_dashboard
137
+ clear_screen
138
+
139
+ # Calculate available space
140
+ content_lines = []
141
+ content_lines << build_header
142
+ content_lines << build_separator
143
+ content_lines += build_pubsub_overview
144
+ content_lines << ""
145
+ content_lines += build_channel_analytics
146
+ content_lines << ""
147
+ content_lines += build_command_statistics
148
+ content_lines << ""
149
+ content_lines += build_latency_metrics
150
+ content_lines << ""
151
+ content_lines += build_network_stats
152
+ content_lines << ""
153
+ content_lines += build_connection_stats
154
+ content_lines << ""
155
+ content_lines << build_footer
156
+
157
+ # Display content with proper positioning
158
+ content_lines.each_with_index do |line, index|
159
+ if index < @terminal_height - 1 # Leave space for status line
160
+ move_cursor(index + 1, 1)
161
+ print line.ljust(@terminal_width)[0, @terminal_width]
162
+ end
163
+ end
164
+
165
+ # Status line at bottom
166
+ move_cursor(@terminal_height, 1)
167
+ status_line = "Last: #{Time.now.strftime('%H:%M:%S')} | Rate: #{@refresh_rate}s | Press 'h' for help, 'q' to quit"
168
+ print "\e[7m#{status_line.ljust(@terminal_width)[0, @terminal_width]}\e[0m" # Reverse video
169
+ end
170
+
171
+ private
172
+
173
+ def clear_screen
174
+ print "\e[2J\e[H"
175
+ end
176
+
177
+ def move_cursor(row, col)
178
+ print "\e[#{row};#{col}H"
179
+ end
180
+
181
+ def center_text(text)
182
+ padding = (@terminal_width - text.length) / 2
183
+ puts " " * [padding, 0].max + text
184
+ end
185
+
186
+ def show_status_message(message)
187
+ move_cursor(@terminal_height - 1, 1)
188
+ print "\e[K" # Clear line
189
+ print "\e[33m#{message}\e[0m" # Yellow text
190
+ sleep(1)
191
+ end
192
+
193
+ def show_help_overlay
194
+ help_lines = [
195
+ "┌─ HELP ─────────────────────────────────────────┐",
196
+ "│ │",
197
+ "│ Controls: │",
198
+ "│ q/Q/Esc : Quit │",
199
+ "│ r/R : Refresh immediately │",
200
+ "│ + : Increase refresh rate │",
201
+ "│ - : Decrease refresh rate │",
202
+ "│ h/H/? : Show this help │",
203
+ "│ │",
204
+ "│ Dashboard shows: │",
205
+ "│ • Active Redis pub/sub channels │",
206
+ "│ • Real-time performance metrics │",
207
+ "│ • SmartMessage activity breakdown │",
208
+ "│ • Connection and memory statistics │",
209
+ "│ │",
210
+ "│ Press any key to continue... │",
211
+ "└────────────────────────────────────────────────┘"
212
+ ]
213
+
214
+ start_row = (@terminal_height - help_lines.length) / 2
215
+ help_lines.each_with_index do |line, index|
216
+ move_cursor(start_row + index, (@terminal_width - 50) / 2)
217
+ print "\e[44m\e[37m#{line}\e[0m" # Blue background, white text
218
+ end
219
+
220
+ STDIN.getc # Wait for any key
221
+ end
222
+
223
+ def build_header
224
+ uptime = (Time.now - @start_time).to_i
225
+ title = "📊 Redis Statistics Dashboard"
226
+ subtitle = "Session: #{format_duration(uptime)} | Messages: #{@session_messages}"
227
+
228
+ # Center text within terminal width
229
+ title_line = title.center(@terminal_width)
230
+ subtitle_line = subtitle.center(@terminal_width)
231
+
232
+ "#{title_line}\n#{subtitle_line}"
233
+ end
234
+
235
+ def build_separator
236
+ "=" * [@terminal_width, 80].min
237
+ end
238
+
239
+ def build_footer
240
+ "=" * [@terminal_width, 80].min
241
+ end
242
+
243
+ def build_pubsub_overview
244
+ lines = []
245
+ lines << "🔀 PUB/SUB OVERVIEW:"
246
+ lines << "-" * 40
247
+
248
+ # Get active channels and subscriber info
249
+ channels = @redis.pubsub("channels")
250
+ total_subscribers = 0
251
+
252
+ stats_info = @redis.info("stats")
253
+ pubsub_channels = stats_info["pubsub_channels"] || 0
254
+ pubsub_patterns = stats_info["pubsub_patterns"] || 0
255
+
256
+ # Get client info for pubsub_clients
257
+ clients_info = @redis.info("clients")
258
+ pubsub_clients = clients_info["pubsub_clients"] || 0
259
+
260
+ # Get command stats from commandstats section
261
+ cmdstats_info = @redis.info("commandstats")
262
+ publish_stats = extract_command_stats(cmdstats_info, "publish")
263
+ total_publishes = publish_stats[:calls]
264
+ publish_rate = calculate_publish_rate(publish_stats)
265
+
266
+ # Initialize baseline on first run
267
+ if @baseline_messages.nil?
268
+ @baseline_messages = total_publishes
269
+ end
270
+
271
+ # Calculate session messages
272
+ @session_messages = total_publishes - @baseline_messages
273
+
274
+ lines << " 📊 Channels: #{pubsub_channels} active, #{pubsub_patterns} patterns"
275
+ lines << " 👥 Clients: #{pubsub_clients} subscribed"
276
+ lines << " 📨 Messages: #{format_number(@session_messages)} this session, #{format_number(total_publishes)} all-time"
277
+ lines << " 📡 Rate: #{publish_rate}/sec (max) | Avg Latency: #{publish_stats[:avg_latency]}μs"
278
+
279
+ lines
280
+ end
281
+
282
+ def build_channel_analytics
283
+ lines = []
284
+ lines << "📺 CHANNEL ANALYTICS:"
285
+ lines << "-" * 40
286
+
287
+ # Get detailed channel information
288
+ channels = @redis.pubsub("channels")
289
+
290
+ if channels.empty?
291
+ lines << " No active channels"
292
+ return lines
293
+ end
294
+
295
+ # SmartMessage channels analysis
296
+ message_types = [
297
+ { name: "HealthCheck", pattern: "HealthCheckMessage", desc: "Health monitoring" },
298
+ { name: "HealthStatus", pattern: "HealthStatusMessage", desc: "Status responses" },
299
+ { name: "FireEmergency", pattern: "FireEmergencyMessage", desc: "Fire alerts" },
300
+ { name: "FireDispatch", pattern: "FireDispatchMessage", desc: "Fire dispatch" },
301
+ { name: "SilentAlarm", pattern: "SilentAlarmMessage", desc: "Bank alarms" },
302
+ { name: "PoliceDispatch", pattern: "PoliceDispatchMessage", desc: "Police dispatch" },
303
+ { name: "EmergencyResolved", pattern: "EmergencyResolvedMessage", desc: "Resolutions" }
304
+ ]
305
+
306
+ message_types.each do |msg_type|
307
+ channel = "Messages::#{msg_type[:pattern]}"
308
+ if channels.include?(channel)
309
+ subscriber_count = @redis.pubsub("numsub", channel)[1] || 0
310
+
311
+ # Calculate estimated message rate for this channel
312
+ rate = estimate_channel_message_rate(msg_type[:name])
313
+
314
+ status_icon = subscriber_count > 0 ? "🟢" : "🔴"
315
+ rate_display = rate > 0 ? "#{rate.round(1)}/sec" : "idle"
316
+
317
+ lines << " #{status_icon} #{msg_type[:name].ljust(13)} #{subscriber_count} subs | #{rate_display.ljust(8)} | #{msg_type[:desc]}"
318
+ end
319
+ end
320
+
321
+ lines
322
+ end
323
+
324
+ def build_command_statistics
325
+ lines = []
326
+ lines << "📊 COMMAND STATISTICS:"
327
+ lines << "-" * 40
328
+
329
+ # Get command stats from commandstats section
330
+ cmdstats_info = @redis.info("commandstats")
331
+
332
+ # Extract key pub/sub commands
333
+ commands = [
334
+ { name: "PUBLISH", key: "publish" },
335
+ { name: "PUBSUB", key: "pubsub|numsub" },
336
+ { name: "SUBSCRIBE", key: "subscribe" }
337
+ ]
338
+
339
+ commands.each do |cmd|
340
+ stats = extract_command_stats(cmdstats_info, cmd[:key])
341
+ next if stats[:calls] == 0
342
+
343
+ # Initialize baseline on first run
344
+ if @baseline_commands[cmd[:key]].nil?
345
+ @baseline_commands[cmd[:key]] = stats[:calls]
346
+ @session_commands[cmd[:key]] = 0
347
+ else
348
+ @session_commands[cmd[:key]] = stats[:calls] - @baseline_commands[cmd[:key]]
349
+ end
350
+
351
+ rate = calculate_command_rate(cmd[:key], stats)
352
+
353
+ # Format: session calls, all-time calls
354
+ session_calls = format_number(@session_commands[cmd[:key]])
355
+ total_calls = format_number(stats[:calls])
356
+
357
+ lines << " 📈 #{cmd[:name].ljust(12)} #{session_calls} this session, #{total_calls} all-time | #{rate}/sec (max) | #{stats[:avg_latency]}μs avg"
358
+ end
359
+
360
+ # Get total command summary from stats section
361
+ stats_info = @redis.info("stats")
362
+ total_commands = stats_info["total_commands_processed"]&.to_i || 0
363
+ current_ops = stats_info["instantaneous_ops_per_sec"]&.to_i || 0
364
+
365
+ lines << ""
366
+ lines << " 📊 Total Commands: #{format_number(total_commands)}"
367
+ lines << " ⚡ Current Rate: #{current_ops} ops/sec"
368
+
369
+ lines
370
+ end
371
+
372
+ def build_latency_metrics
373
+ lines = []
374
+ lines << "⏱️ LATENCY METRICS:"
375
+ lines << "-" * 40
376
+
377
+ # Get latency stats from commandstats section
378
+ cmdstats_info = @redis.info("commandstats")
379
+
380
+ # Extract latency percentiles for pub/sub commands
381
+ latency_data = [
382
+ { cmd: "PUBLISH", key: "latency_percentiles_usec_publish" },
383
+ { cmd: "PUBSUB", key: "latency_percentiles_usec_pubsub|numsub" }
384
+ ]
385
+
386
+ latency_data.each do |cmd_data|
387
+ if cmdstats_info[cmd_data[:key]]
388
+ percentiles = parse_latency_percentiles(cmdstats_info[cmd_data[:key]])
389
+ lines << " ⏱️ #{cmd_data[:cmd].ljust(10)} P50: #{percentiles[:p50]}μs | P99: #{percentiles[:p99]}μs | P99.9: #{percentiles[:p999]}μs"
390
+ end
391
+ end
392
+
393
+ # Show slow log if available
394
+ begin
395
+ slowlog_len = @redis.slowlog("len")
396
+ if slowlog_len > 0
397
+ lines << ""
398
+ lines << " 🐌 Slow Queries: #{slowlog_len} in log"
399
+ end
400
+ rescue
401
+ # Ignore slowlog errors
402
+ end
403
+
404
+ lines
405
+ end
406
+
407
+ def build_network_stats
408
+ lines = []
409
+ lines << "🌐 NETWORK STATISTICS:"
410
+ lines << "-" * 40
411
+
412
+ info = @redis.info("stats")
413
+
414
+ # Current network I/O
415
+ input_kbps = info["instantaneous_input_kbps"]&.to_f || 0.0
416
+ output_kbps = info["instantaneous_output_kbps"]&.to_f || 0.0
417
+
418
+ # Total network I/O
419
+ total_input = info["total_net_input_bytes"]&.to_i || 0
420
+ total_output = info["total_net_output_bytes"]&.to_i || 0
421
+
422
+ # Calculate message size estimates
423
+ publish_stats = extract_command_stats(info, "publish")
424
+ avg_message_size = calculate_avg_message_size(total_output, publish_stats[:calls])
425
+
426
+ lines << " 📥 Input: #{input_kbps.round(2)} KB/s | #{format_bytes(total_input)} total"
427
+ lines << " 📤 Output: #{output_kbps.round(2)} KB/s | #{format_bytes(total_output)} total"
428
+ lines << " 📊 Avg Message Size: #{format_bytes(avg_message_size)}"
429
+ lines << " 🔄 I/O Ratio: #{calculate_io_ratio(total_input, total_output)}"
430
+
431
+ lines
432
+ end
433
+
434
+ def build_performance_stats
435
+ lines = []
436
+ lines << "⚡ PERFORMANCE METRICS:"
437
+ lines << "-" * 40
438
+
439
+ info = @redis.info("stats")
440
+ current_stats = {
441
+ total_commands: info["total_commands_processed"]&.to_i || 0,
442
+ total_connections: info["total_connections_received"]&.to_i || 0,
443
+ ops_per_sec: info["instantaneous_ops_per_sec"]&.to_i || 0,
444
+ input_kbps: info["instantaneous_input_kbps"]&.to_f || 0.0,
445
+ output_kbps: info["instantaneous_output_kbps"]&.to_f || 0.0
446
+ }
447
+
448
+ # Calculate deltas since last check
449
+ if @previous_stats.any?
450
+ commands_delta = current_stats[:total_commands] - @previous_stats[:total_commands]
451
+ connections_delta = current_stats[:total_connections] - @previous_stats[:total_connections]
452
+
453
+ lines << " 📈 Commands Processed: #{format_number(current_stats[:total_commands])} (+#{commands_delta})"
454
+ lines << " 🔗 Total Connections: #{format_number(current_stats[:total_connections])} (+#{connections_delta})"
455
+ else
456
+ lines << " 📈 Commands Processed: #{format_number(current_stats[:total_commands])}"
457
+ lines << " 🔗 Total Connections: #{format_number(current_stats[:total_connections])}"
458
+ end
459
+
460
+ lines << " ⚡ Operations/sec: #{current_stats[:ops_per_sec]}"
461
+ lines << " 📥 Input: #{current_stats[:input_kbps].round(2)} KB/s"
462
+ lines << " 📤 Output: #{current_stats[:output_kbps].round(2)} KB/s"
463
+
464
+ @previous_stats = current_stats
465
+ lines
466
+ end
467
+
468
+ def build_message_breakdown
469
+ lines = []
470
+ lines << "📨 SMARTMESSAGE BREAKDOWN:"
471
+ lines << "-" * 40
472
+
473
+ message_types = [
474
+ { name: "HealthCheck", desc: "Health monitoring broadcasts" },
475
+ { name: "HealthStatus", desc: "Service status responses" },
476
+ { name: "FireEmergency", desc: "House fire alerts" },
477
+ { name: "FireDispatch", desc: "Fire truck dispatches" },
478
+ { name: "SilentAlarm", desc: "Bank security alerts" },
479
+ { name: "PoliceDispatch", desc: "Police unit dispatches" },
480
+ { name: "EmergencyResolved", desc: "Incident resolutions" }
481
+ ]
482
+
483
+ message_types.each do |msg|
484
+ channel = "Messages::#{msg[:name]}Message"
485
+ subscribers = @redis.pubsub("numsub", channel)[1] || 0
486
+
487
+ status_icon = subscribers > 0 ? "🟢" : "🔴"
488
+ lines << " #{status_icon} #{msg[:name].ljust(15)} #{subscribers} subs | #{msg[:desc]}"
489
+ end
490
+
491
+ lines
492
+ end
493
+
494
+ def build_connection_stats
495
+ lines = []
496
+ lines << "🔌 CONNECTION INFO:"
497
+ lines << "-" * 40
498
+
499
+ info = @redis.info("clients")
500
+ connected_clients = info["connected_clients"] || 0
501
+ blocked_clients = info["blocked_clients"] || 0
502
+ max_clients = info["maxclients"] || 0
503
+
504
+ lines << " 👥 Connected Clients: #{connected_clients}/#{max_clients}"
505
+ lines << " ⏸️ Blocked Clients: #{blocked_clients}"
506
+
507
+ # Show client list (limited)
508
+ begin
509
+ client_list = @redis.client("list")
510
+ # Handle both string and array responses
511
+ clients = client_list.is_a?(Array) ? client_list : client_list.split("\n")
512
+ lines << " 📋 Active Connections:"
513
+ clients.first(3).each do |client| # Reduced to 3 to save space
514
+ if client.include?("cmd=") && client.include?("addr=")
515
+ addr = client[/addr=([^\s]+)/, 1]
516
+ cmd = client[/cmd=([^\s]+)/, 1]
517
+ lines << " 🔸 #{addr} | #{cmd}" if addr && cmd
518
+ end
519
+ end
520
+ lines << " ... (showing first 3)" if clients.size > 3
521
+ rescue => e
522
+ lines << " 📋 Connection details unavailable: #{e.message}"
523
+ end
524
+
525
+ lines
526
+ end
527
+
528
+ def build_memory_stats
529
+ lines = []
530
+ lines << "💾 MEMORY USAGE:"
531
+ lines << "-" * 40
532
+
533
+ info = @redis.info("memory")
534
+ used_memory = info["used_memory"]&.to_i || 0
535
+ used_memory_peak = info["used_memory_peak"]&.to_i || 0
536
+ used_memory_rss = info["used_memory_rss"]&.to_i || 0
537
+
538
+ lines << " 💾 Used Memory: #{format_bytes(used_memory)}"
539
+ lines << " 📈 Peak Memory: #{format_bytes(used_memory_peak)}"
540
+ lines << " 🖥️ RSS Memory: #{format_bytes(used_memory_rss)}"
541
+
542
+ lines
543
+ end
544
+
545
+ def show_final_summary
546
+ uptime = (Time.now - @start_time).to_i
547
+ puts "\n📊 Final Summary:"
548
+ puts " Monitor ran for: #{format_duration(uptime)}"
549
+
550
+ info = @redis.info("stats")
551
+ total_commands = info["total_commands_processed"]&.to_i || 0
552
+ puts " Total commands processed: #{format_number(total_commands)}"
553
+
554
+ if uptime > 0
555
+ avg_commands_per_sec = total_commands.to_f / uptime
556
+ puts " Average commands/sec: #{avg_commands_per_sec.round(2)}"
557
+ end
558
+ end
559
+
560
+ def format_duration(seconds)
561
+ if seconds < 60
562
+ "#{seconds}s"
563
+ elsif seconds < 3600
564
+ "#{seconds / 60}m #{seconds % 60}s"
565
+ else
566
+ "#{seconds / 3600}h #{(seconds % 3600) / 60}m"
567
+ end
568
+ end
569
+
570
+ def format_number(num)
571
+ num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
572
+ end
573
+
574
+ def format_bytes(bytes)
575
+ units = ['B', 'KB', 'MB', 'GB']
576
+ size = bytes.to_f
577
+ unit_index = 0
578
+
579
+ while size >= 1024 && unit_index < units.length - 1
580
+ size /= 1024.0
581
+ unit_index += 1
582
+ end
583
+
584
+ "#{size.round(2)} #{units[unit_index]}"
585
+ end
586
+
587
+ # Advanced metrics helper methods
588
+ def extract_command_stats(info, command_key)
589
+ # Redis gem returns commandstats with keys like "publish" not "cmdstat_publish"
590
+ # The value is already parsed into a Hash
591
+ cmdstat_data = info[command_key]
592
+
593
+ if cmdstat_data && cmdstat_data.is_a?(Hash)
594
+ # Already parsed by Redis gem
595
+ {
596
+ calls: cmdstat_data["calls"].to_i,
597
+ total_usec: cmdstat_data["usec"].to_f,
598
+ avg_latency: cmdstat_data["usec_per_call"].to_f.round(2),
599
+ rejected: cmdstat_data["rejected_calls"].to_i,
600
+ failed: cmdstat_data["failed_calls"].to_i
601
+ }
602
+ elsif cmdstat_data && cmdstat_data.is_a?(String)
603
+ # Fallback for raw string format (shouldn't happen with Redis gem)
604
+ stats = {}
605
+ cmdstat_data.split(',').each do |pair|
606
+ key, value = pair.split('=')
607
+ stats[key.to_sym] = value.to_f if key && value
608
+ end
609
+
610
+ {
611
+ calls: stats[:calls]&.to_i || 0,
612
+ total_usec: stats[:usec]&.to_f || 0.0,
613
+ avg_latency: stats[:usec_per_call]&.round(2) || 0.0,
614
+ rejected: stats[:rejected_calls]&.to_i || 0,
615
+ failed: stats[:failed_calls]&.to_i || 0
616
+ }
617
+ else
618
+ {
619
+ calls: 0,
620
+ total_usec: 0.0,
621
+ avg_latency: 0.0,
622
+ rejected: 0,
623
+ failed: 0
624
+ }
625
+ end
626
+ end
627
+
628
+ def calculate_publish_rate(publish_stats)
629
+ current_calls = publish_stats[:calls]
630
+
631
+ # If we have previous stats, calculate the rate
632
+ if @previous_command_stats[:publish]
633
+ previous_calls = @previous_command_stats[:publish][:calls]
634
+ calls_delta = current_calls - previous_calls
635
+ rate = calls_delta.to_f / @refresh_rate
636
+
637
+ # Track the maximum rate we've seen
638
+ @max_publish_rate = rate if rate > @max_publish_rate
639
+ else
640
+ # First run - no rate yet but store the baseline
641
+ rate = 0.0
642
+ end
643
+
644
+ # Update previous stats for next calculation
645
+ @previous_command_stats[:publish] = publish_stats
646
+
647
+ # Always return the maximum rate we've seen
648
+ @max_publish_rate.round(1).to_s
649
+ end
650
+
651
+ def calculate_command_rate(command_key, current_stats)
652
+ current_calls = current_stats[:calls]
653
+ key_sym = command_key.to_sym
654
+
655
+ # For publish command, use the existing @max_publish_rate to keep consistency
656
+ if command_key == "publish"
657
+ return @max_publish_rate.round(1).to_s
658
+ end
659
+
660
+ # If we have previous stats for this command, calculate the rate
661
+ if @previous_command_stats[key_sym]
662
+ previous_calls = @previous_command_stats[key_sym][:calls]
663
+ calls_delta = current_calls - previous_calls
664
+ rate = calls_delta.to_f / @refresh_rate
665
+
666
+ # Track the maximum rate we've seen for this command
667
+ @max_rates[key_sym] = rate if rate > @max_rates[key_sym]
668
+ else
669
+ # First run - no rate yet but store the baseline
670
+ rate = 0.0
671
+ end
672
+
673
+ # Update previous stats for next calculation
674
+ @previous_command_stats[key_sym] = current_stats
675
+
676
+ # Always return the maximum rate we've seen
677
+ @max_rates[key_sym].round(1).to_s
678
+ end
679
+
680
+ def estimate_channel_message_rate(message_type)
681
+ # Estimate based on message type patterns
682
+ case message_type
683
+ when "HealthCheck"
684
+ # Health checks happen every 5 seconds from health dept
685
+ 0.2
686
+ when "HealthStatus"
687
+ # All services respond to health checks (5+ services * 0.2)
688
+ 1.0
689
+ when "FireEmergency"
690
+ # Fires are occasional (houses have 6% chance per 15-45 seconds)
691
+ 0.1
692
+ when "FireDispatch"
693
+ # Fire dispatch matches fire emergencies
694
+ 0.1
695
+ when "SilentAlarm"
696
+ # Bank alarms are occasional (8% chance per 10-30 seconds)
697
+ 0.05
698
+ when "PoliceDispatch"
699
+ # Police dispatch matches alarms
700
+ 0.05
701
+ when "EmergencyResolved"
702
+ # Resolutions match emergencies
703
+ 0.15
704
+ else
705
+ 0.0
706
+ end
707
+ end
708
+
709
+ def parse_latency_percentiles(percentiles_string)
710
+ # Parse: "p50=3.007,p99=26.111,p99.9=185.343"
711
+ percentiles = {}
712
+ percentiles_string.split(',').each do |pair|
713
+ key, value = pair.split('=')
714
+ case key
715
+ when 'p50'
716
+ percentiles[:p50] = value.to_f.round(1)
717
+ when 'p99'
718
+ percentiles[:p99] = value.to_f.round(1)
719
+ when 'p99.9'
720
+ percentiles[:p999] = value.to_f.round(1)
721
+ end
722
+ end
723
+ percentiles
724
+ end
725
+
726
+ def calculate_avg_message_size(total_output, total_messages)
727
+ return 0 if total_messages == 0
728
+ total_output.to_f / total_messages
729
+ end
730
+
731
+ def calculate_io_ratio(input, output)
732
+ return "N/A" if input == 0 && output == 0
733
+ return "∞" if input == 0
734
+
735
+ ratio = output.to_f / input
736
+ "#{ratio.round(2)}:1"
737
+ end
738
+ end
739
+
740
+ if __FILE__ == $0
741
+ stats = RedisStats.new
742
+ stats.start_monitoring
743
+ end