#!/usr/bin/env -S bash # A Bash script to monitor system stats and output them in JSON format. # --- Configuration --- # Default sleep duration in seconds. Can be overridden by the first argument. SLEEP_DURATION=3 # --- Argument Parsing --- # Check if a command-line argument is provided for the sleep duration. if [[ -n "$1" ]]; then # Basic validation to ensure the argument is a number (integer or float). if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then SLEEP_DURATION=$1 else # Output to stderr if the format is invalid. echo "Warning: Invalid duration format '$1'. Using default of ${SLEEP_DURATION}s." >&2 fi fi # --- Global Cache Variables --- # These variables will store the discovered CPU temperature sensor path and type # to avoid searching for it on every loop iteration. TEMP_SENSOR_PATH="" TEMP_SENSOR_TYPE="" # Network speed monitoring variables PREV_RX_BYTES=0 PREV_TX_BYTES=0 PREV_TIME=0 # --- Data Collection Functions --- # # Gets memory usage in GB, MB, and as a percentage. # get_memory_info() { awk ' /MemTotal/ {total=$2} /MemAvailable/ {available=$2} END { if (total > 0) { usage_kb = total - available usage_gb = usage_kb / 1000000 usage_percent = (usage_kb / total) * 100 printf "%.1f %.0f\n", usage_gb, usage_percent } else { # Fallback if /proc/meminfo is unreadable or empty. print "0.0 0 0" } } ' /proc/meminfo } # # Gets the usage percentage of the root filesystem ("/"). # get_disk_usage() { # df gets disk usage. --output=pcent shows only the percentage for the root path. # tail -1 gets the data line, and tr removes the '%' sign and whitespace. df --output=pcent / | tail -1 | tr -d ' %' } # # Calculates current CPU usage over a short interval. # get_cpu_usage() { # Read all 10 CPU time fields to prevent errors on newer kernels. read -r cpu prev_user prev_nice prev_system prev_idle prev_iowait prev_irq prev_softirq prev_steal prev_guest prev_guest_nice < /proc/stat # Calculate previous total and idle times. local prev_total_idle=$((prev_idle + prev_iowait)) local prev_total=$((prev_user + prev_nice + prev_system + prev_idle + prev_iowait + prev_irq + prev_softirq + prev_steal + prev_guest + prev_guest_nice)) # Wait for a short period. sleep 0.05 # Read all 10 CPU time fields again for the second measurement. read -r cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat # Calculate new total and idle times. local total_idle=$((idle + iowait)) local total=$((user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice)) # Add a check to prevent division by zero if total hasn't changed. if (( total <= prev_total )); then echo "0.0" return fi # Calculate the difference over the interval. local diff_total=$((total - prev_total)) local diff_idle=$((total_idle - prev_total_idle)) # Use awk for floating-point calculation and print the percentage. awk -v total="$diff_total" -v idle="$diff_idle" ' BEGIN { if (total > 0) { # Formula: 100 * (Total - Idle) / Total usage = 100 * (total - idle) / total printf "%.1f\n", usage } else { print "0.0" } }' } # # Finds and returns the CPU temperature in degrees Celsius. # Caches the sensor path for efficiency. # get_cpu_temp() { # If the sensor path hasn't been found yet, search for it. if [[ -z "$TEMP_SENSOR_PATH" ]]; then for dir in /sys/class/hwmon/hwmon*; do # Check if the 'name' file exists and read it. if [[ -f "$dir/name" ]]; then local name name=$(<"$dir/name") # Check for supported sensor types. if [[ "$name" == "coretemp" || "$name" == "k10temp" || "$name" == "zenpower" ]]; then TEMP_SENSOR_PATH=$dir TEMP_SENSOR_TYPE=$name break # Found it, no need to keep searching. fi fi done fi # If after searching no sensor was found, return 0. if [[ -z "$TEMP_SENSOR_PATH" ]]; then echo 0 return fi # --- Get temp based on sensor type --- if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then # For Intel 'coretemp', average all available temperature sensors. local total_temp=0 local sensor_count=0 # Use a for loop with a glob to iterate over all temp input files. # This is more efficient than 'find' for this simple case. for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do # The glob returns the pattern itself if no files match, # so we must check if the file actually exists. if [[ -f "$temp_file" ]]; then total_temp=$((total_temp + $(<"$temp_file"))) sensor_count=$((sensor_count + 1)) fi done if (( sensor_count > 0 )); then # Use awk for the final division to handle potential floating point numbers # and convert from millidegrees to integer degrees Celsius. awk -v total="$total_temp" -v count="$sensor_count" 'BEGIN { print int(total / count / 1000) }' else # If no sensor files were found, return 0. echo 0 fi elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then # For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature. local tctl_input="" for label_file in "$TEMP_SENSOR_PATH"/temp*_label; do if [[ -f "$label_file" ]] && [[ $(<"$label_file") == "Tctl" ]]; then # The input file has the same name but with '_input' instead of '_label'. tctl_input="${label_file%_label}_input" break fi done if [[ -f "$tctl_input" ]]; then # Read the temperature and convert from millidegrees to degrees. echo "$(( $(<"$tctl_input") / 1000 ))" else echo 0 # Fallback fi elif [[ "$TEMP_SENSOR_TYPE" == "zenpower" ]]; then # For zenpower, read the first available temp sensor for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do if [[ -f "$temp_file" ]]; then local temp_value temp_value=$(cat "$temp_file" | tr -d '\n\r') # Remove any newlines echo "$((temp_value / 1000))" return fi done echo 0 if [[ -f "$tctl_input" ]]; then # Read the temperature and convert from millidegrees to degrees. echo "$(($(<"$tctl_input") / 1000))" else echo 0 # Fallback fi else echo 0 # Should not happen if cache logic is correct. fi } # --- Main Loop --- # This loop runs indefinitely, gathering and printing stats. while true; do # Call the functions to gather all the data. # get_memory_info read -r mem_gb mem_per <<< "$(get_memory_info)" # Command substitution captures the single output from the other functions. disk_per=$(get_disk_usage) cpu_usage=$(get_cpu_usage) cpu_temp=$(get_cpu_temp) # Get network speeds current_time=$(date +%s.%N) total_rx=0 total_tx=0 # Read total bytes from /proc/net/dev for all interfaces while IFS=: read -r interface stats; do # Skip only loopback interface, allow other interfaces if [[ "$interface" =~ ^lo[[:space:]]*$ ]]; then continue fi # Extract rx and tx bytes (fields 1 and 9 in the stats part) rx_bytes=$(echo "$stats" | awk '{print $1}') tx_bytes=$(echo "$stats" | awk '{print $9}') # Add to totals if they are valid numbers if [[ "$rx_bytes" =~ ^[0-9]+$ ]] && [[ "$tx_bytes" =~ ^[0-9]+$ ]]; then total_rx=$((total_rx + rx_bytes)) total_tx=$((total_tx + tx_bytes)) fi done < <(tail -n +3 /proc/net/dev) # Calculate speeds if we have previous data rx_speed=0 tx_speed=0 if [[ "$PREV_TIME" != "0" ]]; then time_diff=$(awk -v current="$current_time" -v prev="$PREV_TIME" 'BEGIN { printf "%.3f", current - prev }') rx_diff=$((total_rx - PREV_RX_BYTES)) tx_diff=$((total_tx - PREV_TX_BYTES)) # Calculate speeds in bytes per second using awk rx_speed=$(awk -v rx="$rx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", rx / time }') tx_speed=$(awk -v tx="$tx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", tx / time }') fi # Update previous values for next iteration PREV_RX_BYTES=$total_rx PREV_TX_BYTES=$total_tx PREV_TIME=$current_time # Use printf to format the final JSON output string, adding the mem_mb key. printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s", "rx_speed": "%s", "tx_speed": "%s"}\n' \ "$cpu_usage" \ "$cpu_temp" \ "$mem_gb" \ "$mem_per" \ "$disk_per" \ "$rx_speed" \ "$tx_speed" # Wait for the specified duration before the next update. sleep "$SLEEP_DURATION" done