Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] New option to track system performance stats (CPU, RAM) for current system #92

Merged
merged 3 commits into from
Jun 4, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -125,6 +125,9 @@ You will need to set the following environment variables:
| TC_DISCORD_CHANNEL_NAME | No | Channel name for updates | "Tautulli Status" |
| TC_DISCORD_NITRO | No | Whether the Discord server has a Nitro subscription (bot will upload custom emojis) | "False" |
| TC_ALLOW_ANALYTICS | No | Allow Anonymous Crash Analytics? | "True" |
| TC_VC_PERFORMANCE_CATEGORY_NAME | No | Name of the performance voice channel category | "Performance" |
| TC_MONITOR_CPU | No | Whether to monitor CPU performance (see [Performance Monitoring](#performance-monitoring)) | "False" |
| TC_MONITOR_MEMORY | No | Whether to monitor RAM performance (see [Performance Monitoring](#performance-monitoring)) | "False" |
| TZ | No | Timezone that your server is in | "America/New_York" |

You can also set these variables via a configuration file:
@@ -178,6 +181,16 @@ Tauticord uses Google Analytics to collect statistics such as common errors that
- Any data from Tautulli
- Anything typed in Discord.

# Performance Monitoring

Tauticord will attempt to query the system it is running on for CPU and RAM usage every 5 minutes.

Tautulli does not currently offer a way to query performance statistics from its API. As a result, this data is **not Tautulli-specific performance data**, but rather **the performance of the system that Tauticord is running on**.

If Tauticord is running on the same system as Tautulli, then this data may reflect the performance of Tautulli (+ Tauticord and all other processes running on the system).

If Tauticord is running on a different system than Tautulli, or is running isolated in a Docker container, then this data will not reflect the performance of Tautulli.

# Development

This bot is still a work in progress. If you have any ideas for improving or adding to Tauticord, please open an issue
5 changes: 5 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
@@ -36,6 +36,8 @@ Tautulli:
TVEpisodeCount: true
MusicArtistCount: true
MusicTrackCount: true
Performance:
CategoryName: "Performance"
Anonymize:
HideUsernames: false
HidePlayerNames: false
@@ -62,3 +64,6 @@ Discord:
Extras:
# See README.md for details
Analytics: true
Performance:
CPU: true
Memory: true
30 changes: 30 additions & 0 deletions modules/config_parser.py
Original file line number Diff line number Diff line change
@@ -289,6 +289,15 @@ def _anonymize_hide_eta(self) -> bool:
env_name_override="TC_HIDE_ETA")
return _extract_bool(value)

@property
def _performance_voice_channel_settings(self) -> ConfigSection:
return self._voice_channels._get_subsection(key="Performance")

@property
def _performance_voice_channel_name(self) -> str:
return self._performance_voice_channel_settings._get_value(key="CategoryName", default="Performance",
env_name_override="TC_VC_PERFORMANCE_CATEGORY_NAME")

@property
def text_manager(self) -> TextManager:
anonymous_rules = {
@@ -354,6 +363,22 @@ def allow_analytics(self) -> bool:
env_name_override="TC_ALLOW_ANALYTICS")
return _extract_bool(value)

@property
def _performance(self) -> ConfigSection:
return self._get_subsection(key="Performance")

@property
def _performance_monitor_cpu(self) -> bool:
value = self._performance._get_value(key="CPU", default=False,
env_name_override="TC_MONITOR_CPU")
return _extract_bool(value)

@property
def _performance_monitor_memory(self) -> bool:
value = self._performance._get_value(key="Memory", default=False,
env_name_override="TC_MONITOR_MEMORY")
return _extract_bool(value)


class Config:
def __init__(self, app_name: str, config_path: str, fallback_to_env: bool = True):
@@ -372,6 +397,11 @@ def __init__(self, app_name: str, config_path: str, fallback_to_env: bool = True
self.tautulli = TautulliConfig(self.config, self.pull_from_env)
self.discord = DiscordConfig(self.config, self.pull_from_env)
self.extras = ExtrasConfig(self.config, self.pull_from_env)
self.performance = {
statics.KEY_PERFORMANCE_CATEGORY_NAME: self.tautulli._performance_voice_channel_name,
statics.KEY_PERFORMANCE_MONITOR_CPU: self.extras._performance_monitor_cpu,
statics.KEY_PERFORMANCE_MONITOR_MEMORY: self.extras._performance_monitor_memory,
}

def __repr__(self) -> str:
raw_yaml_data = self.config.dump()
47 changes: 47 additions & 0 deletions modules/discord_connector.py
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
import modules.logs as logging
import modules.statics as statics
import modules.tautulli_connector
import modules.system_stats as system_stats
from modules import emojis
from modules.emojis import EmojiManager
from modules.settings_transports import LibraryVoiceChannelsVisibilities
@@ -186,6 +187,7 @@ def __init__(self,
display_live_stats: bool,
display_library_stats: bool,
nitro: bool,
performance_monitoring: dict,
analytics):
self.token = token
self.guild_id = guild_id
@@ -201,6 +203,11 @@ def __init__(self,
self.tautulli_stats_voice_category: discord.CategoryChannel = None
self.tautulli_libraries_voice_category: discord.CategoryChannel = None
self.tautulli = tautulli_connector
self.performance_monitoring = performance_monitoring
self.enable_performance_monitoring = performance_monitoring.get(statics.KEY_PERFORMANCE_MONITOR_CPU,
False) or performance_monitoring.get(
statics.KEY_PERFORMANCE_MONITOR_MEMORY, False)
self.performance_voice_category: discord.CategoryChannel = None
self.analytics = analytics

intents = discord.Intents.default()
@@ -226,6 +233,10 @@ def stats_voice_category_name(self) -> str:
def libraries_voice_category_name(self) -> str:
return self.voice_channel_settings.get(statics.KEY_LIBRARIES_CATEGORY_NAME, "Tautulli Libraries")

@property
def performance_category_name(self) -> str:
return self.performance_monitoring.get(statics.KEY_PERFORMANCE_CATEGORY_NAME, "Performance")

async def on_ready(self) -> None:
logging.info('Connected to Discord.')
await self.client.change_presence(
@@ -252,6 +263,9 @@ async def on_ready(self) -> None:
if self.display_library_stats:
self.tautulli_libraries_voice_category = await self.collect_discord_voice_category(
category_name=self.libraries_voice_category_name)
if self.enable_performance_monitoring:
self.performance_voice_category = await self.collect_discord_voice_category(
category_name=self.performance_category_name)

logging.info("Loading Tautulli summary message service...")
# minimum 5-second sleep time hard-coded, trust me, don't DDoS your server
@@ -262,6 +276,11 @@ async def on_ready(self) -> None:
# minimum 5-minute sleep time hard-coded, trust me, don't DDoS your server
asyncio.create_task(self.run_library_stats_service(refresh_time=max([5 * 60, self.library_refresh_time])))

if self.enable_performance_monitoring:
logging.info("Starting performance monitoring service...")
asyncio.create_task(
self.run_performance_monitoring_service(refresh_time=5 * 60)) # Hard-coded 5-minute refresh time

async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None:
emoji = payload.emoji
user_id = payload.user_id
@@ -328,6 +347,16 @@ async def run_library_stats_service(self, refresh_time: int):
except Exception:
exit(1) # Die on any unhandled exception for this subprocess (i.e. internet connection loss)

async def run_performance_monitoring_service(self, refresh_time: int):
if not self.performance_voice_category:
return # No performance voice category set, so don't bother
while True:
try:
await self.update_performance_voice_channels()
await asyncio.sleep(refresh_time)
except Exception:
exit(1) # Die on any unhandled exception for this subprocess (i.e. internet connection loss)

def is_me(self, message) -> bool:
return message.author == self.client.user

@@ -536,3 +565,21 @@ async def update_library_stats_voice_channels(self) -> None:
await self.edit_stat_voice_channel(channel_name=channel_name,
stat=stat_value,
category=self.tautulli_libraries_voice_category)

async def update_performance_voice_channels(self) -> None:
logging.info("Updating performance stats...")
if self.performance_monitoring.get(statics.KEY_PERFORMANCE_MONITOR_CPU, False):
cpu_percent = "{:0.2f}%".format(system_stats.cpu_usage())
logging.info(f"Updating CPU voice channel with new CPU percent: {cpu_percent}")
await self.edit_stat_voice_channel(channel_name="CPU",
stat=cpu_percent,
category=self.performance_voice_category)

if self.performance_monitoring.get(statics.KEY_PERFORMANCE_MONITOR_MEMORY, False):
memory_percent = "{:0.2f} GB".format(system_stats.ram_usage())
logging.info(f"Updating Memory voice channel with new Memory percent: {memory_percent}")
await self.edit_stat_voice_channel(channel_name="Memory",
stat=memory_percent,
category=self.performance_voice_category)


6 changes: 4 additions & 2 deletions modules/statics.py
Original file line number Diff line number Diff line change
@@ -29,7 +29,6 @@
KEY_SHOW_MUSIC_ARTISTS = "show_music_artists"
KEY_SHOW_MUSIC_TRACKS = "show_music_tracks"

KEY_ANONYMOUS_SETTINGS = "anonymous_settings"
KEY_HIDE_USERNAMES = "hide_usernames"
KEY_HIDE_PLATFORMS = "hide_platforms"
KEY_HIDE_PLAYER_NAMES = "anonymize_players"
@@ -38,7 +37,10 @@
KEY_HIDE_TRANSCODING = "hide_transcoding"
KEY_HIDE_PROGRESS = "hide_progress"
KEY_HIDE_ETA = "hide_eta"
KEY_DISABLE_TERMINATION = "hide_termination"

KEY_PERFORMANCE_CATEGORY_NAME = "performance_category_name"
KEY_PERFORMANCE_MONITOR_CPU = "performance_monitor_cpu"
KEY_PERFORMANCE_MONITOR_MEMORY = "performance_monitor_memory"


MAX_STREAM_COUNT = 36
52 changes: 52 additions & 0 deletions modules/system_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import enum

import psutil
import os

class CPUTimeFrame(enum.Enum):
INSTANT = 0
ONE_MINUTE = 1
FIVE_MINUTES = 5
FIFTEEN_MINUTES = 15

def cpu_usage(timeframe: CPUTimeFrame = CPUTimeFrame.INSTANT) -> float:
"""
Get the current CPU usage percentage

:param timeframe: (Optional) Timeframe to get CPU usage for
:type timeframe: CPUTimeFrame, optional
:return: CPU usage percentage
:rtype: float
"""
match timeframe:
case CPUTimeFrame.INSTANT:
return psutil.cpu_percent(interval=1) # 1 second
case CPUTimeFrame.ONE_MINUTE:
load, _, _ = psutil.getloadavg()
return load/os.cpu_count() * 100
case CPUTimeFrame.FIVE_MINUTES:
_, load, _ = psutil.getloadavg()
return load/os.cpu_count() * 100
case CPUTimeFrame.FIFTEEN_MINUTES:
_, _, load = psutil.getloadavg()
return load/os.cpu_count() * 100
case _:
raise ValueError("Invalid timeframe")

def ram_usage_percentage() -> float:
"""
Get the current RAM usage percentage

:return: RAM usage percentage
:rtype: float
"""
return psutil.virtual_memory()[2]

def ram_usage() -> float:
"""
Get the current RAM usage in GB

:return: RAM usage in GB
:rtype: float
"""
return psutil.virtual_memory()[3]/1000000000
1 change: 1 addition & 0 deletions run.py
Original file line number Diff line number Diff line change
@@ -74,6 +74,7 @@
display_live_stats=config.tautulli.any_live_stats_channels_enabled,
display_library_stats=config.tautulli.any_library_stats_channels_enabled,
nitro=config.discord.has_discord_nitro,
performance_monitoring=config.performance,
analytics=analytics,
)

3 changes: 3 additions & 0 deletions templates/tauticord.xml
Original file line number Diff line number Diff line change
@@ -55,6 +55,9 @@
<Config Name="Discord admin IDs" Target="TC_DISCORD_ADMIN_IDS" Default="" Description="Comma-separated list of IDs of Discord users with bot admin privileges" Type="Variable" Display="always" Required="false" Mask="false" />
<Config Name="Stream details text channel" Target="TC_DISCORD_CHANNEL_NAME" Default="tauticord" Description="Name of Discord text channel where the bot will post" Type="Variable" Display="always" Required="true" Mask="false">tauticord</Config>
<Config Name="Allow analytics" Target="TC_ALLOW_ANALYTICS" Default="True" Description="Whether to allow anonymous analytics collection" Type="Variable" Display="always" Required="false" Mask="false">True</Config>
<Config Name="Performance stats category name" Target="TC_VC_PERFORMANCE_CATEGORY_NAME" Default="Performance" Description="Name of the performance stats voice channel category" Type="Variable" Display="always" Required="false" Mask="false">Performance</Config>
<Config Name="Monitor CPU performance" Target="TC_MONITOR_CPU" Default="False" Description="Whether to monitor Tauticord Docker CPU performance" Type="Variable" Display="always" Required="false" Mask="false">False</Config>
<Config Name="Monitor memory performance" Target="TC_MONITOR_MEMORY" Default="False" Description="Whether to monitor Tauticord Docker memory performance" Type="Variable" Display="always" Required="false" Mask="false">False</Config>
<Config Name="Timezone" Target="TZ" Default="UTC" Description="Timezone for the server" Type="Variable" Display="always" Required="false" Mask="false">UTC</Config>
<Config Name="Config Path" Target="/config" Default="/mnt/user/appdata/tauticord/config" Mode="rw" Description="Where optional config file will be stored" Type="Path" Display="advanced" Required="true" Mask="false">/mnt/user/appdata/tauticord/config</Config>
<Config Name="Log Path" Target="/logs" Default="/mnt/user/appdata/tauticord/logs" Mode="rw" Description="Where debug logs will be stored" Type="Path" Display="advanced" Required="true" Mask="false">/mnt/user/appdata/tauticord/logs</Config>