99import traceback
1010import signal
1111import atexit
12+ import requests
1213from discord .ext import commands , tasks
1314from typing import Dict , List , Tuple
1415from os import system
1516import logging
17+ from datetime import datetime
1618from utils .command_tracker import usage_tracker
1719from utils .tos_handler import check_tos_acceptance , prompt_tos_acceptance
1820from utils .scalability import initialize_scalability
1921
2022# Set up logging
2123logging .basicConfig (level = logging .INFO )
2224
25+ class StatsTracker :
26+ def __init__ (self , bot , dashboard_url ):
27+ self .bot = bot
28+ self .dashboard_url = dashboard_url .rstrip ('/' )
29+ self .start_time = datetime .now ()
30+ self .command_count = 0
31+ self .daily_commands = 0
32+ self .command_types = {}
33+
34+ async def send_stats (self ):
35+ """Send comprehensive stats to dashboard"""
36+ uptime = datetime .now () - self .start_time
37+
38+ stats = {
39+ "uptime" : {
40+ "days" : uptime .days ,
41+ "hours" : uptime .seconds // 3600 ,
42+ "minutes" : (uptime .seconds % 3600 ) // 60 ,
43+ "total_seconds" : uptime .total_seconds (),
44+ "start_time" : self .start_time .timestamp ()
45+ },
46+ "guilds" : {
47+ "count" : len (self .bot .guilds ),
48+ "list" : [str (guild .id ) for guild in self .bot .guilds ],
49+ "detailed" : [
50+ {
51+ "id" : str (guild .id ),
52+ "name" : guild .name ,
53+ "member_count" : guild .member_count
54+ } for guild in self .bot .guilds
55+ ]
56+ },
57+ "performance" : {
58+ "user_count" : sum (guild .member_count for guild in self .bot .guilds if guild .member_count ),
59+ "latency" : round (self .bot .latency * 1000 , 2 ),
60+ "shard_count" : self .bot .shard_count or 1
61+ },
62+ "commands" : {
63+ "total_executed" : self .command_count ,
64+ "daily_count" : self .daily_commands ,
65+ "command_types" : self .command_types .copy ()
66+ }
67+ }
68+
69+ try :
70+ # Use aiohttp for async HTTP requests
71+ async with aiohttp .ClientSession () as session :
72+ async with session .post (
73+ f"{ self .dashboard_url } /api/stats" ,
74+ json = stats ,
75+ timeout = 10
76+ ) as response :
77+ if response .status == 200 :
78+ logging .debug ("✅ Stats sent to dashboard successfully" )
79+ else :
80+ logging .warning (f"❌ Failed to send stats: { response .status } " )
81+ except Exception as e :
82+ logging .error (f"❌ Error sending stats: { e } " )
83+
84+ async def send_command_update (self , command_name ):
85+ """Send real-time command execution update"""
86+ self .command_count += 1
87+ self .daily_commands += 1
88+ self .command_types [command_name ] = self .command_types .get (command_name , 0 ) + 1
89+
90+ update = {
91+ "type" : "command_executed" ,
92+ "command" : command_name ,
93+ "total_commands" : self .command_count ,
94+ "timestamp" : time .time ()
95+ }
96+
97+ try :
98+ # Use aiohttp for async HTTP requests
99+ async with aiohttp .ClientSession () as session :
100+ async with session .post (
101+ f"{ self .dashboard_url } /api/stats/realtime" ,
102+ json = update ,
103+ timeout = 5
104+ ) as response :
105+ if response .status != 200 :
106+ logging .debug (f"Failed to send real-time update: { response .status } " )
107+ except Exception as e :
108+ logging .debug (f"Failed to send real-time update: { e } " )
109+
23110def cleanup_resources ():
24111 """Cleanup resources on shutdown"""
25112 try :
@@ -103,116 +190,6 @@ async def load_cog_with_timing(self, cog_name: str) -> Tuple[bool, float]:
103190 self .cog_load_times [cog_name ] = load_time
104191 return False , load_time
105192
106- @tasks .loop (seconds = 10 ) # More frequent updates for better real-time feel
107- async def update_stats (self ):
108- """Update bot stats and send to dashboard"""
109- try :
110- # Calculate uptime
111- current_time = time .time ()
112- uptime_seconds = int (current_time - self .start_time )
113- uptime_days = uptime_seconds // 86400
114- uptime_hours = (uptime_seconds % 86400 ) // 3600
115- uptime_minutes = (uptime_seconds % 3600 ) // 60
116-
117- # Get command metrics from tracker if available
118- command_stats = {}
119- daily_commands = 0
120- total_commands = 0
121- session_stats = {}
122-
123- try :
124- # Try to get command stats from tracker
125- if hasattr (usage_tracker , 'get_daily_stats' ):
126- daily_commands = usage_tracker .get_daily_stats ()
127- if hasattr (usage_tracker , 'get_total_commands' ):
128- total_commands = usage_tracker .get_total_commands ()
129- if hasattr (usage_tracker , 'get_command_breakdown' ):
130- command_stats = usage_tracker .get_command_breakdown ()
131- if hasattr (usage_tracker , 'get_session_stats' ):
132- session_stats = usage_tracker .get_session_stats ()
133- except :
134- pass
135-
136- stats = {
137- 'uptime' : {
138- 'days' : uptime_days ,
139- 'hours' : uptime_hours ,
140- 'minutes' : uptime_minutes ,
141- 'total_seconds' : uptime_seconds ,
142- 'start_time' : self .start_time
143- },
144- 'guilds' : {
145- 'count' : len (self .guilds ),
146- 'list' : [str (g .id ) for g in self .guilds ],
147- 'detailed' : [
148- {
149- 'id' : str (g .id ),
150- 'name' : g .name ,
151- 'member_count' : g .member_count or 0
152- } for g in self .guilds
153- ]
154- },
155- 'commands' : {
156- 'daily_count' : daily_commands ,
157- 'total_executed' : total_commands ,
158- 'command_types' : command_stats ,
159- 'session' : session_stats
160- },
161- 'performance' : {
162- 'latency' : round (self .latency * 1000 , 2 ),
163- 'user_count' : sum ((g .member_count or 0 ) for g in self .guilds ),
164- 'shard_count' : self .shard_count or 1
165- },
166- 'timestamp' : current_time
167- }
168-
169- # Store stats based on environment
170- if dev :
171- # Development: Store in JSON file
172- with open ('data/stats.json' , 'w' ) as f :
173- json .dump (stats , f , indent = 2 )
174- logging .debug ("Stats saved to local JSON file" )
175- else :
176- # Production: Send to database via dashboard API
177- async with aiohttp .ClientSession () as session :
178- dashboard_urls = ['https://bronxbot.onrender.com/api/stats/update' ]
179-
180- for url in dashboard_urls :
181- try :
182- async with session .post (url , json = stats , timeout = 10 ) as resp :
183- if resp .status == 200 :
184- result = await resp .json ()
185- logging .debug (f"Stats updated successfully to { url } " )
186- else :
187- error_text = await resp .text ()
188- logging .warning (f"Failed to update stats to { url } : { resp .status } - { error_text } " )
189-
190- except asyncio .TimeoutError :
191- logging .warning (f"Timeout updating stats to { url } " )
192- except Exception as e :
193- logging .error (f"Error updating stats to { url } : { e } " )
194-
195- # Always try to update localhost if available (for development testing)
196- if dev :
197- try :
198- async with aiohttp .ClientSession () as session :
199- async with session .post ('http://localhost:5000/api/stats/update' ,
200- json = stats , timeout = 5 ) as resp :
201- if resp .status == 200 :
202- logging .debug ("Stats updated to localhost dashboard" )
203- except :
204- pass # Localhost might not be running
205-
206- except Exception as e :
207- logging .error (f"Error in update_stats loop: { e } " )
208- import traceback
209- traceback .print_exc ()
210-
211- @update_stats .before_loop
212- async def before_update_stats (self ):
213- """Wait until the bot is ready before starting the stats update loop"""
214- await self .wait_until_ready ()
215-
216193 @tasks .loop (minutes = 5 ) # Check every 5 minutes, no need to do it as frequently as stats
217194 async def update_guilds (self ):
218195 """Update guild list for the web interface"""
@@ -231,6 +208,33 @@ async def update_guilds(self):
231208 async def before_update_guilds (self ):
232209 await self .wait_until_ready ()
233210
211+ async def send_realtime_command_update (self , command_name : str , user_id : int , guild_id : int = None , execution_time : float = 0 , error : bool = False ):
212+ """Send real-time command update to dashboard"""
213+ try :
214+ update_data = {
215+ 'type' : 'command_update' ,
216+ 'command' : command_name ,
217+ 'user_id' : str (user_id ),
218+ 'guild_id' : str (guild_id ) if guild_id else None ,
219+ 'execution_time' : execution_time ,
220+ 'error' : error ,
221+ 'timestamp' : time .time ()
222+ }
223+
224+ dashboard_urls = ['https://bronxbot.onrender.com/api/realtime' ] if not dev else ['http://localhost:5000/api/realtime' ]
225+
226+ async with aiohttp .ClientSession () as session :
227+ for url in dashboard_urls :
228+ try :
229+ async with session .post (url , json = update_data , timeout = 5 ) as resp :
230+ if resp .status == 200 :
231+ logging .debug ("Real-time command update sent successfully" )
232+ break
233+ except Exception as e :
234+ logging .debug (f"Failed to send real-time update to { url } : { e } " )
235+ except Exception as e :
236+ logging .debug (f"Error sending real-time command update: { e } " )
237+
234238 async def close (self ):
235239 """Gracefully close bot connections"""
236240 logging .info ("Shutting down bot..." )
@@ -244,6 +248,21 @@ async def close(self):
244248 self .update_guilds .stop ()
245249 logging .info ("Stopped guild update loop" )
246250
251+ # Stop additional stats tasks
252+ try :
253+ if additional_stats_update .is_running ():
254+ additional_stats_update .stop ()
255+ logging .info ("Stopped additional stats update loop" )
256+ except :
257+ pass
258+
259+ try :
260+ if reset_daily_stats .is_running ():
261+ reset_daily_stats .stop ()
262+ logging .info ("Stopped daily stats reset loop" )
263+ except :
264+ pass
265+
247266 # Shutdown scalability manager
248267 if hasattr (self , 'scalability_manager' ) and self .scalability_manager :
249268 await self .scalability_manager .shutdown ()
@@ -274,6 +293,10 @@ async def close(self):
274293)
275294bot .remove_command ('help' )
276295
296+ # Initialize stats tracker
297+ dashboard_url = "https://bronxbot.onrender.com" if not dev else "http://localhost:5000"
298+ stats_tracker = StatsTracker (bot , dashboard_url )
299+
277300# loading config
278301COG_DATA = {
279302 "cogs" : {
@@ -513,6 +536,44 @@ async def on_ready():
513536 name = f"with { len (bot .guilds )} servers | .help"
514537 )
515538 await bot .change_presence (activity = activity )
539+
540+ # Start additional stats tracker
541+ if not additional_stats_update .is_running ():
542+ additional_stats_update .start ()
543+ logging .info ("Started additional stats update loop" )
544+
545+ if not reset_daily_stats .is_running ():
546+ reset_daily_stats .start ()
547+ logging .info ("Started daily stats reset loop" )
548+
549+ # Additional stats tasks
550+ @tasks .loop (minutes = 5 ) # Send stats every 5 minutes
551+ async def additional_stats_update ():
552+ """Send comprehensive stats to dashboard"""
553+ try :
554+ await stats_tracker .send_stats ()
555+ except Exception as e :
556+ logging .error (f"Error in additional stats update: { e } " )
557+
558+ @additional_stats_update .before_loop
559+ async def before_additional_stats_update ():
560+ """Wait until the bot is ready before starting the additional stats update loop"""
561+ await bot .wait_until_ready ()
562+
563+ # Reset daily commands at midnight
564+ @tasks .loop (hours = 24 )
565+ async def reset_daily_stats ():
566+ """Reset daily command count"""
567+ try :
568+ stats_tracker .daily_commands = 0
569+ logging .info ("Daily stats reset completed" )
570+ except Exception as e :
571+ logging .error (f"Error resetting daily stats: { e } " )
572+
573+ @reset_daily_stats .before_loop
574+ async def before_reset_daily_stats ():
575+ """Wait until the bot is ready before starting the daily reset loop"""
576+ await bot .wait_until_ready ()
516577
517578@bot .event
518579async def on_guild_join (guild ):
@@ -584,12 +645,37 @@ async def on_command_completion(ctx):
584645 """Track successful command completion"""
585646 execution_time = time .time () - getattr (ctx , 'command_start_time' , time .time ())
586647 usage_tracker .track_command (ctx , ctx .command .qualified_name , execution_time , error = False )
648+
649+ # Track command with stats tracker
650+ try :
651+ await stats_tracker .send_command_update (ctx .command .qualified_name )
652+ except Exception as e :
653+ logging .debug (f"Error tracking command with stats tracker: { e } " )
654+
655+ # Send real-time update to dashboard
656+ await bot .send_realtime_command_update (
657+ command_name = ctx .command .qualified_name ,
658+ user_id = ctx .author .id ,
659+ guild_id = ctx .guild .id if ctx .guild else None ,
660+ execution_time = execution_time ,
661+ error = False
662+ )
587663
588664@bot .event
589665async def on_command_error (ctx , error ):
590666 """Track command errors and let Error cog handle the rest"""
591667 execution_time = time .time () - getattr (ctx , 'command_start_time' , time .time ())
592- usage_tracker .track_command (ctx , ctx .command .qualified_name if ctx .command else 'unknown' , execution_time , error = True )
668+ command_name = ctx .command .qualified_name if ctx .command else 'unknown'
669+ usage_tracker .track_command (ctx , command_name , execution_time , error = True )
670+
671+ # Send real-time error update to dashboard
672+ await bot .send_realtime_command_update (
673+ command_name = command_name ,
674+ user_id = ctx .author .id ,
675+ guild_id = ctx .guild .id if ctx .guild else None ,
676+ execution_time = execution_time ,
677+ error = True
678+ )
593679
594680 # Let the Error cog handle most errors first
595681 if hasattr (bot , 'get_cog' ) and bot .get_cog ('Error' ):
0 commit comments