Skip to content

Commit

Permalink
- Add feature to monitor disk usage for /monitor mounted path (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
nwithan8 authored Mar 4, 2024
1 parent 456314e commit 99484c3
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 26 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@ or [GitHub Packages](https://github.com/nwithan8/tauticord/pkgs/container/tautic

You will need to map the following volumes:

| Host Path | Container Path | Reason |
|------------------------|----------------|---------------------------------------------------------------------------------------------------|
| /path/to/logs/folder | /logs | Required, debug log file for bot will be generated here |
| /path/to/config/folder | /config | Optional, path to the folder containing the configuration file (override environmental variables) |
| Host Path | Container Path | Reason |
|---------------------------|----------------|---------------------------------------------------------------------------------------------------|
| /path/to/logs/folder | /logs | Required, debug log file for bot will be generated here |
| /path/to/config/folder | /config | Optional, path to the folder containing the configuration file (override environmental variables) |
| /path/to/monitored/folder | /monitor | Optional, path to a folder to monitor for disk usage statistics (e.g. your Plex library) |

### Environmental Variables

Expand Down Expand Up @@ -140,6 +141,7 @@ You will need to set the following environment variables:
| 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" |
| TC_MONITOR_DISK_SPACE | No | Whether to monitor usage of the [/monitor](#volumes) path (see [Performance Monitoring](#performance-monitoring)) | "False" |
| TC_MONITOR_TAUTULLI_USER_COUNT | No | Whether to monitor how many users have access to the Plex server | "False" |
| TZ | No | Timezone that your server is in | "America/New_York" |

Expand Down Expand Up @@ -285,6 +287,11 @@ 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.

The same applies to disk space monitoring. Tauticord's disk space monitoring feature will analyze the used and total
space of the provided folder (default: The path mounted to `/monitor` inside the Docker container). This feature can be
used, for example, to monitor the disk space of your Plex library, as long as the path to the library is mounted
to `/monitor`. This will not work if Tauticord is running on a separate system from the Plex library.

# Development

This bot is still a work in progress. If you have any ideas for improving or adding to Tauticord, please open an issue
Expand Down
1 change: 1 addition & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ Extras:
Analytics: true
Performance:
TautulliUserCount: true
DiskSpace: true
CPU: true
Memory: true
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ services:
volumes:
- /path/to/config:/config
- /path/to/logs:/logs
- /path/to/monitored/disk:/monitor
environment:
TC_DISCORD_BOT_TOKEN: my_secret_bot_token
TC_DISCORD_SERVER_ID: my_server_id
Expand Down Expand Up @@ -58,4 +59,5 @@ services:
TC_MONITOR_TAUTULLI_USER_COUNT: "False"
TC_MONITOR_CPU: "False"
TC_MONITOR_MEMORY: "False"
TC_MONITOR_DISK_SPACE: "False"
TZ: America/New_York
10 changes: 9 additions & 1 deletion modules/config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@ def _performance_monitor_tautulli_user_count(self) -> bool:
env_name_override="TC_MONITOR_TAUTULLI_USER_COUNT")
return _extract_bool(value)

@property
def _performance_monitor_disk_space(self) -> bool:
value = self._performance._get_value(key="DiskSpace", default=False,
env_name_override="TC_MONITOR_DISK_SPACE")
return _extract_bool(value)

@property
def _performance_monitor_cpu(self) -> bool:
value = self._performance._get_value(key="CPU", default=False,
Expand All @@ -488,7 +494,7 @@ def _performance_monitor_memory(self) -> bool:


class Config:
def __init__(self, app_name: str, config_path: str, fallback_to_env: bool = True):
def __init__(self, app_name: str, config_path: str, fallback_to_env: bool = True, **kwargs):
self.config = confuse.Configuration(app_name)
self.pull_from_env = False
# noinspection PyBroadException
Expand All @@ -507,6 +513,8 @@ def __init__(self, app_name: str, config_path: str, fallback_to_env: bool = True
self.performance = {
statics.KEY_PERFORMANCE_CATEGORY_NAME: self.tautulli._performance_voice_channel_name,
statics.KEY_PERFORMANCE_MONITOR_TAUTULLI_USER_COUNT: self.extras._performance_monitor_tautulli_user_count,
statics.KEY_PERFORMANCE_MONITOR_DISK_SPACE: self.extras._performance_monitor_disk_space,
statics.KEY_PERFORMANCE_MONITOR_DISK_SPACE_PATH: kwargs.get(statics.KEY_PERFORMANCE_MONITOR_DISK_SPACE_PATH, statics.MONITORED_DISK_SPACE_FOLDER),
statics.KEY_PERFORMANCE_MONITOR_CPU: self.extras._performance_monitor_cpu,
statics.KEY_PERFORMANCE_MONITOR_MEMORY: self.extras._performance_monitor_memory,
}
Expand Down
15 changes: 13 additions & 2 deletions modules/discord_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,15 +674,26 @@ async def update_performance_voice_channels(self) -> None:
await self.edit_stat_voice_channel(channel_name="Users",
stat=user_count,
category=self.performance_voice_category)
if self.performance_monitoring.get(statics.KEY_PERFORMANCE_MONITOR_DISK_SPACE, False):
path = self.performance_monitoring.get(statics.KEY_PERFORMANCE_MONITOR_DISK_SPACE_PATH, statics.MONITORED_DISK_SPACE_FOLDER)
if not system_stats.path_exists(path):
logging.error(f"Could not find {quote(path)} to monitor disk space.")
stat = "N/A"
else:
stat = system_stats.disk_usage_display(path)
logging.info(f"Updating Disk voice channel with new disk space: {stat}")
await self.edit_stat_voice_channel(channel_name="Disk",
stat=stat,
category=self.performance_voice_category)
if self.performance_monitoring.get(statics.KEY_PERFORMANCE_MONITOR_CPU, False):
cpu_percent = f"{utils.format_fraction(system_stats.cpu_usage())}%"
cpu_percent = f"{utils.format_decimal(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 = f"{utils.format_fraction(system_stats.ram_usage())} GB ({utils.format_fraction(system_stats.ram_usage_percentage())}%)"
memory_percent = f"{utils.format_decimal(system_stats.ram_usage())} GB ({utils.format_decimal(system_stats.ram_usage_percentage())}%)"
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,
Expand Down
4 changes: 4 additions & 0 deletions modules/statics.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
STANDARD_EMOJIS_FOLDER = "resources/emojis/standard"
NITRO_EMOJIS_FOLDER = "resources/emojis/nitro"

MONITORED_DISK_SPACE_FOLDER = "/monitor"

voice_channel_order = {
'count': 1,
'transcodes': 2,
Expand Down Expand Up @@ -61,6 +63,8 @@

KEY_PERFORMANCE_CATEGORY_NAME = "performance_category_name"
KEY_PERFORMANCE_MONITOR_TAUTULLI_USER_COUNT = "performance_monitor_tautulli_user_count"
KEY_PERFORMANCE_MONITOR_DISK_SPACE = "performance_monitor_disk_space"
KEY_PERFORMANCE_MONITOR_DISK_SPACE_PATH = "performance_monitor_disk_space_path"
KEY_PERFORMANCE_MONITOR_CPU = "performance_monitor_cpu"
KEY_PERFORMANCE_MONITOR_MEMORY = "performance_monitor_memory"

Expand Down
62 changes: 56 additions & 6 deletions modules/system_stats.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import enum
import os
import shutil

import psutil
import os

from modules import utils


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
Expand All @@ -20,19 +25,20 @@ def cpu_usage(timeframe: CPUTimeFrame = CPUTimeFrame.INSTANT) -> float:
"""
match timeframe:
case CPUTimeFrame.INSTANT:
return psutil.cpu_percent(interval=1) # 1 second
return psutil.cpu_percent(interval=1) # 1 second
case CPUTimeFrame.ONE_MINUTE:
load, _, _ = psutil.getloadavg()
return load/os.cpu_count() * 100
return load / os.cpu_count() * 100
case CPUTimeFrame.FIVE_MINUTES:
_, load, _ = psutil.getloadavg()
return load/os.cpu_count() * 100
return load / os.cpu_count() * 100
case CPUTimeFrame.FIFTEEN_MINUTES:
_, _, load = psutil.getloadavg()
return load/os.cpu_count() * 100
return load / os.cpu_count() * 100
case _:
raise ValueError("Invalid timeframe")


def ram_usage_percentage() -> float:
"""
Get the current RAM usage percentage
Expand All @@ -42,11 +48,55 @@ def ram_usage_percentage() -> 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
return psutil.virtual_memory()[3] / 1000000000


def path_exists(path: str) -> bool:
"""
Check if a path exists
:param path: Path to check
:type path: str
:return: True if path exists, False if not
:rtype: bool
"""
return os.path.exists(path)


def disk_space_info(path: str) -> tuple:
"""
Get the current disk usage total, used, and free in bytes
:param path: Path to get disk usage for
:type path: str
:return: Disk usage total, used, and free in bytes
:rtype: tuple
"""
total, used, free = shutil.disk_usage(path)
return total, used, free


def disk_usage_display(path: str) -> str:
"""
Get the current disk usage display
:param path: Path to get disk usage for
:type path: str
:return: Disk usage display
:rtype: str
"""
total, used, free = disk_space_info(path)

space_used = utils.human_size(used, decimal_places=1, no_zeros=True)
total_space = utils.human_size(total, decimal_places=1, no_zeros=True)
percentage_used = utils.format_decimal(used / total * 100, decimal_places=2, no_zeros=True)

return f"{space_used}/{total_space} ({percentage_used}%)"
42 changes: 31 additions & 11 deletions modules/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ def status_code_is_success(status_code: int) -> bool:
return 200 <= status_code < 300


def format_fraction(number: float, denominator: int = 1, decimal_places: int = 1) -> str:
def format_decimal(number: float, denominator: int = 1, decimal_places: int = 1, no_zeros: bool = False) -> str:
if decimal_places <= 0:
return f'{int(number / denominator):d}'
value = f'{int(number / denominator):d}'
else:
return f'{float(number / denominator):.{decimal_places}f}'
value = f'{float(number / denominator):.{decimal_places}f}'

if no_zeros:
if value.endswith("."):
value = value[:-1]
if value.endswith("." + "0" * decimal_places):
value = value[:-decimal_places - 1]

return value


def format_thousands(number: int, delimiter: str = "") -> str:
Expand All @@ -33,13 +41,13 @@ def format_thousands(number: int, delimiter: str = "") -> str:
return f"{format_thousands(number // 1000, delimiter)}{delimiter}{number % 1000:03d}"


def human_bitrate(_bytes, decimal_places: int = 1) -> str:
# Return the given bitrate as a human friendly bps, Kbps, Mbps, Gbps, or Tbps string
def _human_size(_bytes, interval: int = 1000, decimal_places: int = 1, no_zeros: bool = False) -> str:
# Return the given byte size as a human friendly string

KB = float(1024)
MB = float(KB ** 2) # 1,048,576
GB = float(KB ** 3) # 1,073,741,824
TB = float(KB ** 4) # 1,099,511,627,776
KB = float(interval)
MB = float(interval ** 2)
GB = float(interval ** 3)
TB = float(interval ** 4)

denominator = 1
letter = ""
Expand All @@ -58,8 +66,20 @@ def human_bitrate(_bytes, decimal_places: int = 1) -> str:
denominator = TB
letter = "T"

value = format_fraction(number=_bytes, denominator=denominator, decimal_places=decimal_places)
return f"{value} {letter}bps"
value = format_decimal(number=_bytes, denominator=denominator, decimal_places=decimal_places, no_zeros=no_zeros)
return f"{value} {letter}"


def human_size(_bytes, decimal_places: int = 1, no_zeros: bool = False) -> str:
# Return the given byte size as a human friendly string
value = _human_size(_bytes, interval=1000, decimal_places=decimal_places, no_zeros=no_zeros)
return f"{value}B"


def human_bitrate(_bytes, decimal_places: int = 1, no_zeros: bool = False) -> str:
# Return the given bitrate as a human friendly bps, Kbps, Mbps, Gbps, or Tbps string
value = _human_size(_bytes, interval=1024, decimal_places=decimal_places, no_zeros=no_zeros)
return f"{value}bps"


def milliseconds_to_minutes_seconds(milliseconds: int) -> str:
Expand Down
12 changes: 10 additions & 2 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
)
from modules.analytics import GoogleAnalytics
from modules.config_parser import Config
from modules.statics import splash_logo
from modules.statics import (
splash_logo,
MONITORED_DISK_SPACE_FOLDER,
KEY_PERFORMANCE_MONITOR_DISK_SPACE_PATH,
)
from modules.errors import determine_exit_code

# Parse arguments
Expand All @@ -31,6 +35,7 @@
"""
parser.add_argument("-c", "--config", help="Path to config file", default=DEFAULT_CONFIG_PATH)
parser.add_argument("-l", "--log", help="Log file directory", default=DEFAULT_LOG_DIR)
parser.add_argument("-u", "--usage", help="Path to directory to monitor for disk usage", default=MONITORED_DISK_SPACE_FOLDER)

args = parser.parse_args()

Expand All @@ -39,7 +44,10 @@
file_log_level=FILE_LOG_LEVEL)

# Set up configuration
config = Config(app_name=APP_NAME, config_path=f"{args.config}")
kwargs = {
KEY_PERFORMANCE_MONITOR_DISK_SPACE_PATH: args.usage,
}
config = Config(app_name=APP_NAME, config_path=f"{args.config}", **kwargs)

# Set up analytics
analytics = GoogleAnalytics(analytics_id=GOOGLE_ANALYTICS_ID,
Expand Down
2 changes: 2 additions & 0 deletions templates/tauticord.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,11 @@
<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="advanced" Required="false" Mask="false">Performance</Config>
<Config Name="Monitor Plex user count" Target="TC_MONITOR_TAUTULLI_USER_COUNT" Default="False" Description="Whether to monitor how many users have access to the Plex server" Type="Variable" Display="always" Required="false" Mask="false">False</Config>
<Config Name="Monitor disk space" Target="TC_MONITOR_DISK_SPACE" Default="False" Description="Whether to monitor usage of the Monitor Path directory" Type="Variable" Display="always" Required="false" Mask="false">False</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>
<Config Name="Monitor Path" Target="/monitor" Default="/mnt/user/appdata/tauticord/monitor" Mode="ro" Description="Directory to monitor usage percentage of (e.g. your Plex library)" Type="Path" Display="advanced" Required="true" Mask="false">/mnt/user/appdata/tauticord/monitor</Config>
</Container>

0 comments on commit 99484c3

Please sign in to comment.