Skip to content

Commit 8cded16

Browse files
committed
feat(stats): Add performance metrics tracking and update functionality
1 parent 7cff28e commit 8cded16

2 files changed

Lines changed: 200 additions & 17 deletions

File tree

cogs/Stats.py

Lines changed: 198 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import logging
44
import time
55
import json
6+
import psutil
7+
import os
68
from datetime import datetime
79
from discord.ext import commands, tasks
810
from typing import Dict, List, Optional
@@ -28,6 +30,7 @@ def __init__(self, bot):
2830

2931
# Start background tasks
3032
self.send_stats_task.start()
33+
self.send_performance_update_task.start()
3134
self.reset_daily_stats_task.start()
3235
self.load_stats_task.start()
3336

@@ -36,6 +39,7 @@ def __init__(self, bot):
3639
def cog_unload(self):
3740
"""Clean up when cog is unloaded"""
3841
self.send_stats_task.cancel()
42+
self.send_performance_update_task.cancel()
3943
self.reset_daily_stats_task.cancel()
4044
self.load_stats_task.cancel()
4145
# Save stats before unloading
@@ -72,7 +76,12 @@ async def load_stats_from_mongodb(self):
7276
self.daily_commands = stats_doc.get("daily_commands", 0)
7377
self.command_types = stats_doc.get("command_types", {})
7478
self.last_stats_update = stats_doc.get("last_update", 0)
75-
logging.info(f"Loaded stats from MongoDB: {self.command_count} commands")
79+
80+
# Load uptime start if available, otherwise use current time
81+
if "uptime_start" in stats_doc:
82+
self.start_time = datetime.fromtimestamp(stats_doc["uptime_start"])
83+
84+
logging.info(f"Loaded stats from MongoDB: {self.command_count} commands, {len(self.command_types)} unique commands")
7685
else:
7786
logging.info("No stats found in MongoDB, using default values")
7887
except Exception as e:
@@ -85,13 +94,47 @@ async def save_stats_to_mongodb(self):
8594
return False
8695

8796
try:
88-
# Prepare stats document
97+
# Get system performance metrics
98+
try:
99+
process = psutil.Process(os.getpid())
100+
memory_usage = process.memory_info().rss / 1024 / 1024 # MB
101+
cpu_usage = process.cpu_percent()
102+
except (psutil.NoSuchProcess, psutil.AccessDenied):
103+
memory_usage = 0
104+
cpu_usage = 0
105+
106+
# Calculate total user count
107+
total_users = sum(guild.member_count or 0 for guild in self.bot.guilds)
108+
109+
# Prepare comprehensive stats document
89110
stats_doc = {
90111
"command_count": self.command_count,
91112
"daily_commands": self.daily_commands,
92113
"command_types": self.command_types,
93114
"last_update": time.time(),
94-
"updated_at": datetime.now()
115+
"updated_at": datetime.now(),
116+
117+
# Performance metrics
118+
"user_count": total_users,
119+
"latency": round(self.bot.latency * 1000, 2),
120+
"shard_count": self.bot.shard_count or 1,
121+
"memory_usage": round(memory_usage, 2),
122+
"cpu_usage": round(cpu_usage, 2),
123+
"cached_users": len(self.bot.users),
124+
125+
# Guild information
126+
"guild_count": len(self.bot.guilds),
127+
"guild_list": [str(guild.id) for guild in self.bot.guilds],
128+
129+
# Uptime information
130+
"uptime_start": self.start_time.timestamp(),
131+
"uptime_seconds": (datetime.now() - self.start_time).total_seconds(),
132+
133+
# System information
134+
"python_version": f"{os.sys.version_info.major}.{os.sys.version_info.minor}.{os.sys.version_info.micro}",
135+
"discord_py_version": discord.__version__,
136+
"platform": os.name,
137+
"process_id": os.getpid()
95138
}
96139

97140
# Update or insert the global stats document
@@ -127,6 +170,54 @@ async def before_send_stats_task(self):
127170
"""Wait until the bot is ready before starting the stats update loop"""
128171
await self.bot.wait_until_ready()
129172

173+
@tasks.loop(minutes=1)
174+
async def send_performance_update_task(self):
175+
"""Send performance metrics update every minute for real-time monitoring"""
176+
try:
177+
await self.send_performance_update()
178+
except Exception as e:
179+
logging.debug(f"Error in performance update task: {e}")
180+
181+
@send_performance_update_task.before_loop
182+
async def before_send_performance_update_task(self):
183+
"""Wait until the bot is ready before starting the performance update loop"""
184+
await self.bot.wait_until_ready()
185+
186+
async def send_performance_update(self):
187+
"""Send lightweight performance update to dashboard"""
188+
try:
189+
process = psutil.Process(os.getpid())
190+
memory_usage = process.memory_info().rss / 1024 / 1024 # MB
191+
cpu_usage = process.cpu_percent()
192+
except (psutil.NoSuchProcess, psutil.AccessDenied):
193+
memory_usage = 0
194+
cpu_usage = 0
195+
196+
performance_data = {
197+
"type": "performance_update",
198+
"latency": round(self.bot.latency * 1000, 2),
199+
"memory_usage": round(memory_usage, 2),
200+
"cpu_usage": round(cpu_usage, 2),
201+
"guild_count": len(self.bot.guilds),
202+
"user_count": sum(guild.member_count or 0 for guild in self.bot.guilds),
203+
"cached_users": len(self.bot.users),
204+
"timestamp": time.time()
205+
}
206+
207+
try:
208+
async with aiohttp.ClientSession() as session:
209+
async with session.post(
210+
f"{self.dashboard_url}/api/stats/performance",
211+
json=performance_data,
212+
timeout=5
213+
) as response:
214+
if response.status == 200:
215+
logging.debug("✅ Performance update sent successfully")
216+
else:
217+
logging.debug(f"❌ Failed to send performance update: {response.status}")
218+
except Exception as e:
219+
logging.debug(f"❌ Error sending performance update: {e}")
220+
130221
@tasks.loop(hours=24)
131222
async def reset_daily_stats_task(self):
132223
"""Reset daily command count at midnight"""
@@ -146,6 +237,40 @@ async def send_comprehensive_stats(self):
146237
"""Send comprehensive stats to dashboard"""
147238
uptime = datetime.now() - self.start_time
148239

240+
# Get system performance metrics
241+
try:
242+
process = psutil.Process(os.getpid())
243+
memory_usage = process.memory_info().rss / 1024 / 1024 # MB
244+
cpu_usage = process.cpu_percent()
245+
except (psutil.NoSuchProcess, psutil.AccessDenied):
246+
memory_usage = 0
247+
cpu_usage = 0
248+
249+
# Calculate total user count across all guilds
250+
total_users = sum(guild.member_count or 0 for guild in self.bot.guilds)
251+
252+
# Get detailed guild information
253+
guild_details = []
254+
for guild in self.bot.guilds:
255+
try:
256+
guild_details.append({
257+
"id": str(guild.id),
258+
"name": guild.name,
259+
"member_count": guild.member_count or 0,
260+
"owner_id": str(guild.owner_id) if guild.owner_id else None,
261+
"created_at": guild.created_at.isoformat() if guild.created_at else None,
262+
"features": list(guild.features) if guild.features else [],
263+
"verification_level": str(guild.verification_level) if guild.verification_level else "none",
264+
"premium_tier": guild.premium_tier if hasattr(guild, 'premium_tier') else 0
265+
})
266+
except Exception as e:
267+
logging.warning(f"Error getting details for guild {guild.id}: {e}")
268+
guild_details.append({
269+
"id": str(guild.id),
270+
"name": getattr(guild, 'name', 'Unknown'),
271+
"member_count": getattr(guild, 'member_count', 0) or 0
272+
})
273+
149274
stats = {
150275
"uptime": {
151276
"days": uptime.days,
@@ -157,23 +282,27 @@ async def send_comprehensive_stats(self):
157282
"guilds": {
158283
"count": len(self.bot.guilds),
159284
"list": [str(guild.id) for guild in self.bot.guilds],
160-
"detailed": [
161-
{
162-
"id": str(guild.id),
163-
"name": guild.name,
164-
"member_count": guild.member_count or 0
165-
} for guild in self.bot.guilds
166-
]
285+
"detailed": guild_details
167286
},
168287
"performance": {
169-
"user_count": sum(guild.member_count or 0 for guild in self.bot.guilds),
170-
"latency": round(self.bot.latency * 1000, 2),
171-
"shard_count": self.bot.shard_count or 1
288+
"user_count": total_users,
289+
"latency": round(self.bot.latency * 1000, 2), # Convert to milliseconds
290+
"shard_count": self.bot.shard_count or 1,
291+
"memory_usage": round(memory_usage, 2),
292+
"cpu_usage": round(cpu_usage, 2),
293+
"cached_users": len(self.bot.users),
294+
"cached_messages": getattr(self.bot, '_connection', {}).get('_messages', {}) and len(getattr(self.bot._connection, '_messages', {})) or 0
172295
},
173296
"commands": {
174297
"total_executed": self.command_count,
175298
"daily_count": self.daily_commands,
176299
"command_types": self.command_types.copy()
300+
},
301+
"system": {
302+
"python_version": f"{os.sys.version_info.major}.{os.sys.version_info.minor}.{os.sys.version_info.micro}",
303+
"discord_py_version": discord.__version__,
304+
"platform": os.name,
305+
"process_id": os.getpid()
177306
}
178307
}
179308

@@ -187,8 +316,12 @@ async def send_comprehensive_stats(self):
187316
if response.status == 200:
188317
logging.debug("✅ Comprehensive stats sent to dashboard successfully")
189318
self.last_stats_update = time.time()
319+
320+
# Also save to MongoDB after successful dashboard update
321+
await self.save_stats_to_mongodb()
190322
else:
191-
logging.warning(f"❌ Failed to send comprehensive stats: {response.status}")
323+
response_text = await response.text()
324+
logging.warning(f"❌ Failed to send comprehensive stats: {response.status} - {response_text}")
192325
except Exception as e:
193326
logging.error(f"❌ Error sending comprehensive stats: {e}")
194327

@@ -274,6 +407,17 @@ async def stats_status(self, ctx):
274407
uptime = datetime.now() - self.start_time
275408
last_update_ago = time.time() - self.last_stats_update if self.last_stats_update else None
276409

410+
# Get system metrics
411+
try:
412+
process = psutil.Process(os.getpid())
413+
memory_usage = process.memory_info().rss / 1024 / 1024 # MB
414+
cpu_usage = process.cpu_percent()
415+
except (psutil.NoSuchProcess, psutil.AccessDenied):
416+
memory_usage = 0
417+
cpu_usage = 0
418+
419+
total_users = sum(guild.member_count or 0 for guild in self.bot.guilds)
420+
277421
embed = discord.Embed(
278422
title="📊 Stats Cog Status",
279423
color=discord.Color.blue()
@@ -287,6 +431,22 @@ async def stats_status(self, ctx):
287431
inline=True
288432
)
289433

434+
embed.add_field(
435+
name="🌐 Bot Performance",
436+
value=f"**Guilds:** {len(self.bot.guilds):,}\n"
437+
f"**Users:** {total_users:,}\n"
438+
f"**Latency:** {round(self.bot.latency * 1000, 2)}ms",
439+
inline=True
440+
)
441+
442+
embed.add_field(
443+
name="💻 System Performance",
444+
value=f"**Memory Usage:** {memory_usage:.1f} MB\n"
445+
f"**CPU Usage:** {cpu_usage:.1f}%\n"
446+
f"**Shards:** {self.bot.shard_count or 1}",
447+
inline=True
448+
)
449+
290450
embed.add_field(
291451
name="⏱️ Timing",
292452
value=f"**Uptime:** {uptime.days}d {uptime.seconds//3600}h {(uptime.seconds%3600)//60}m\n"
@@ -300,7 +460,15 @@ async def stats_status(self, ctx):
300460
f"**Update Interval:** 5 minutes\n"
301461
f"**MongoDB Storage:** Enabled\n"
302462
f"**Tasks Running:** {self.send_stats_task.is_running()}",
303-
inline=False
463+
inline=True
464+
)
465+
466+
embed.add_field(
467+
name="📋 System Info",
468+
value=f"**Python:** {os.sys.version_info.major}.{os.sys.version_info.minor}.{os.sys.version_info.micro}\n"
469+
f"**Discord.py:** {discord.__version__}\n"
470+
f"**Platform:** {os.name}",
471+
inline=True
304472
)
305473

306474
# Top 5 commands
@@ -323,8 +491,10 @@ async def force_stats_update(self, ctx):
323491
await ctx.send("🔄 Sending stats update...")
324492

325493
try:
326-
# Update dashboard
494+
# Update dashboard with comprehensive stats
327495
await self.send_comprehensive_stats()
496+
# Send performance update as well
497+
await self.send_performance_update()
328498
# Save to MongoDB
329499
mongo_success = await self.save_stats_to_mongodb()
330500

@@ -335,6 +505,18 @@ async def force_stats_update(self, ctx):
335505
except Exception as e:
336506
await ctx.send(f"❌ Failed to send stats update: {e}")
337507

508+
@commands.command(name='testperformance', aliases=['perftest'])
509+
@commands.is_owner()
510+
async def test_performance_update(self, ctx):
511+
"""Test performance metrics update"""
512+
await ctx.send("🔄 Testing performance update...")
513+
514+
try:
515+
await self.send_performance_update()
516+
await ctx.send("✅ Performance update sent successfully!")
517+
except Exception as e:
518+
await ctx.send(f"❌ Failed to send performance update: {e}")
519+
338520
@commands.command(name='resetstats')
339521
@commands.is_owner()
340522
async def reset_stats(self, ctx):

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ aioredis==2.0.1
3131
# Utils
3232
python-dotenv==1.0.0
3333
requests==2.32.2
34-
asgiref==3.7.2
34+
asgiref==3.7.2
35+
psutil==5.9.8

0 commit comments

Comments
 (0)