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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +38 -0
- data/.gitignore +5 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile.lock +35 -4
- data/README.md +169 -71
- data/Rakefile +29 -4
- data/docs/assets/images/ddq_architecture.svg +130 -0
- data/docs/assets/images/dlq_architecture.svg +115 -0
- data/docs/assets/images/enhanced-dual-publishing.svg +136 -0
- data/docs/assets/images/enhanced-fluent-api.svg +149 -0
- data/docs/assets/images/enhanced-microservices-routing.svg +115 -0
- data/docs/assets/images/enhanced-pattern-matching.svg +107 -0
- data/docs/assets/images/fluent-api-demo.svg +59 -0
- data/docs/assets/images/performance-comparison.svg +161 -0
- data/docs/assets/images/redis-basic-architecture.svg +53 -0
- data/docs/assets/images/redis-enhanced-architecture.svg +88 -0
- data/docs/assets/images/redis-queue-architecture.svg +101 -0
- data/docs/assets/images/smart_message.jpg +0 -0
- data/docs/assets/images/smart_message_walking.jpg +0 -0
- data/docs/assets/images/smartmessage_architecture_overview.svg +173 -0
- data/docs/assets/images/transport-comparison-matrix.svg +171 -0
- data/docs/assets/javascripts/mathjax.js +17 -0
- data/docs/assets/stylesheets/extra.css +51 -0
- data/docs/{addressing.md → core-concepts/addressing.md} +5 -7
- data/docs/{architecture.md → core-concepts/architecture.md} +78 -138
- data/docs/{dispatcher.md → core-concepts/dispatcher.md} +21 -21
- data/docs/{message_filtering.md → core-concepts/message-filtering.md} +2 -3
- data/docs/{message_processing.md → core-concepts/message-processing.md} +17 -17
- data/docs/{troubleshooting.md → development/troubleshooting.md} +7 -7
- data/docs/{examples.md → getting-started/examples.md} +115 -89
- data/docs/{getting-started.md → getting-started/quick-start.md} +47 -18
- data/docs/guides/redis-queue-getting-started.md +697 -0
- data/docs/guides/redis-queue-patterns.md +889 -0
- data/docs/guides/redis-queue-production.md +1091 -0
- data/docs/index.md +64 -0
- data/docs/{dead_letter_queue.md → reference/dead-letter-queue.md} +2 -3
- data/docs/{logging.md → reference/logging.md} +1 -1
- data/docs/{message_deduplication.md → reference/message-deduplication.md} +1 -0
- data/docs/{proc_handlers_summary.md → reference/proc-handlers.md} +7 -6
- data/docs/{serializers.md → reference/serializers.md} +3 -5
- data/docs/{transports.md → reference/transports.md} +133 -11
- data/docs/transports/memory-transport.md +374 -0
- data/docs/transports/redis-enhanced-transport.md +524 -0
- data/docs/transports/redis-queue-transport.md +1304 -0
- data/docs/transports/redis-transport-comparison.md +496 -0
- data/docs/transports/redis-transport.md +509 -0
- data/examples/README.md +98 -5
- data/examples/city_scenario/911_emergency_call_flow.svg +99 -0
- data/examples/city_scenario/README.md +515 -0
- data/examples/city_scenario/ai_visitor_intelligence_flow.svg +108 -0
- data/examples/city_scenario/citizen.rb +195 -0
- data/examples/city_scenario/city_diagram.svg +125 -0
- data/examples/city_scenario/common/health_monitor.rb +80 -0
- data/examples/city_scenario/common/logger.rb +30 -0
- data/examples/city_scenario/emergency_dispatch_center.rb +270 -0
- data/examples/city_scenario/fire_department.rb +446 -0
- data/examples/city_scenario/fire_emergency_flow.svg +95 -0
- data/examples/city_scenario/health_department.rb +100 -0
- data/examples/city_scenario/health_monitoring_system.svg +130 -0
- data/examples/city_scenario/house.rb +244 -0
- data/examples/city_scenario/local_bank.rb +217 -0
- data/examples/city_scenario/messages/emergency_911_message.rb +81 -0
- data/examples/city_scenario/messages/emergency_resolved_message.rb +43 -0
- data/examples/city_scenario/messages/fire_dispatch_message.rb +43 -0
- data/examples/city_scenario/messages/fire_emergency_message.rb +45 -0
- data/examples/city_scenario/messages/health_check_message.rb +22 -0
- data/examples/city_scenario/messages/health_status_message.rb +35 -0
- data/examples/city_scenario/messages/police_dispatch_message.rb +46 -0
- data/examples/city_scenario/messages/silent_alarm_message.rb +38 -0
- data/examples/city_scenario/police_department.rb +316 -0
- data/examples/city_scenario/redis_monitor.rb +129 -0
- data/examples/city_scenario/redis_stats.rb +743 -0
- data/examples/city_scenario/room_for_improvement.md +240 -0
- data/examples/city_scenario/security_emergency_flow.svg +95 -0
- data/examples/city_scenario/service_internal_architecture.svg +154 -0
- data/examples/city_scenario/smart_message_ai_agent.rb +364 -0
- data/examples/city_scenario/start_demo.sh +236 -0
- data/examples/city_scenario/stop_demo.sh +106 -0
- data/examples/city_scenario/visitor.rb +631 -0
- data/examples/{10_message_deduplication.rb → memory/01_message_deduplication_demo.rb} +1 -1
- data/examples/{09_dead_letter_queue_demo.rb → memory/02_dead_letter_queue_demo.rb} +13 -40
- data/examples/{01_point_to_point_orders.rb → memory/03_point_to_point_orders.rb} +1 -1
- data/examples/{02_publish_subscribe_events.rb → memory/04_publish_subscribe_events.rb} +2 -2
- data/examples/{03_many_to_many_chat.rb → memory/05_many_to_many_chat.rb} +4 -4
- data/examples/{show_me.rb → memory/06_pretty_print_demo.rb} +1 -1
- data/examples/{05_proc_handlers.rb → memory/07_proc_handlers_demo.rb} +2 -2
- data/examples/{06_custom_logger_example.rb → memory/08_custom_logger_demo.rb} +17 -14
- data/examples/{07_error_handling_scenarios.rb → memory/09_error_handling_demo.rb} +4 -4
- data/examples/{08_entity_addressing_basic.rb → memory/10_entity_addressing_basic.rb} +8 -8
- data/examples/{08_entity_addressing_with_filtering.rb → memory/11_entity_addressing_with_filtering.rb} +6 -6
- data/examples/{09_regex_filtering_microservices.rb → memory/12_regex_filtering_microservices.rb} +2 -2
- data/examples/{10_header_block_configuration.rb → memory/13_header_block_configuration.rb} +6 -6
- data/examples/{11_global_configuration_example.rb → memory/14_global_configuration_demo.rb} +19 -8
- data/examples/{show_logger.rb → memory/15_logger_demo.rb} +1 -1
- data/examples/memory/README.md +163 -0
- data/examples/memory/memory_transport_architecture.svg +90 -0
- data/examples/memory/point_to_point_pattern.svg +94 -0
- data/examples/memory/publish_subscribe_pattern.svg +125 -0
- data/examples/{04_redis_smart_home_iot.rb → redis/01_smart_home_iot_demo.rb} +5 -5
- data/examples/redis/README.md +230 -0
- data/examples/redis/alert_system_flow.svg +127 -0
- data/examples/redis/dashboard_status_flow.svg +107 -0
- data/examples/redis/device_command_flow.svg +113 -0
- data/examples/redis/redis_transport_architecture.svg +115 -0
- data/examples/{smart_home_iot_dataflow.md → redis/smart_home_iot_dataflow.md} +4 -116
- data/examples/redis/smart_home_system_architecture.svg +133 -0
- data/examples/redis_enhanced/README.md +319 -0
- data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_enhanced/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/01_basic_messaging.rb +221 -0
- data/examples/redis_queue/01_comprehensive_examples.rb +508 -0
- data/examples/redis_queue/02_pattern_routing.rb +405 -0
- data/examples/redis_queue/03_fluent_api.rb +422 -0
- data/examples/redis_queue/04_load_balancing.rb +486 -0
- data/examples/redis_queue/05_microservices.rb +735 -0
- data/examples/redis_queue/06_emergency_alerts.rb +777 -0
- data/examples/redis_queue/07_queue_management.rb +587 -0
- data/examples/redis_queue/README.md +366 -0
- data/examples/redis_queue/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_queue/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_queue/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_queue/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/redis_queue_architecture.svg +148 -0
- data/ideas/README.md +41 -0
- data/ideas/agents.md +1001 -0
- data/ideas/database_transport.md +980 -0
- data/ideas/improvement.md +359 -0
- data/ideas/meshage.md +1788 -0
- data/ideas/message_discovery.md +178 -0
- data/ideas/message_schema.md +1381 -0
- data/lib/smart_message/.idea/.gitignore +8 -0
- data/lib/smart_message/.idea/markdown.xml +6 -0
- data/lib/smart_message/.idea/misc.xml +4 -0
- data/lib/smart_message/.idea/modules.xml +8 -0
- data/lib/smart_message/.idea/smart_message.iml +16 -0
- data/lib/smart_message/.idea/vcs.xml +6 -0
- data/lib/smart_message/addressing.rb +15 -0
- data/lib/smart_message/base.rb +0 -2
- data/lib/smart_message/configuration.rb +1 -1
- data/lib/smart_message/logger.rb +15 -4
- data/lib/smart_message/plugins.rb +5 -2
- data/lib/smart_message/serializer.rb +14 -0
- data/lib/smart_message/transport/redis_enhanced_transport.rb +399 -0
- data/lib/smart_message/transport/redis_queue_transport.rb +555 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +34 -1
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message.rb +5 -52
- data/mkdocs.yml +184 -0
- data/p2p_plan.md +326 -0
- data/p2p_roadmap.md +287 -0
- data/smart_message.gemspec +2 -0
- data/smart_message.svg +51 -0
- metadata +170 -44
- data/docs/README.md +0 -57
- data/examples/dead_letters.jsonl +0 -12
- data/examples/temp.txt +0 -94
- data/examples/tmux_chat/README.md +0 -283
- data/examples/tmux_chat/bot_agent.rb +0 -278
- data/examples/tmux_chat/human_agent.rb +0 -199
- data/examples/tmux_chat/room_monitor.rb +0 -160
- data/examples/tmux_chat/shared_chat_system.rb +0 -328
- data/examples/tmux_chat/start_chat_demo.sh +0 -190
- data/examples/tmux_chat/stop_chat_demo.sh +0 -22
- /data/docs/{properties.md → core-concepts/properties.md} +0 -0
- /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
|