MacOS Caching Server : System Performance Report (with Daemon and HTML report)

This article looks at performance of your MacOS content caching servers and runs in the background and logs metrics every minute with a daemon, then when required you can produce a report based on the outputted CSV file.

This daemon uses minimal resources by:
  1. Only collecting metrics every 360 seconds
  2. Implementing efficient log rotation
  3. Using lightweight system commands
When the daemon is running and the report script is executed you end up with a report as below:


Daemon Creation

This is the utility that will run in the background and update the report in this location:

/var/log/content-cache-monitor/cache_metrics.log

This will give you all the vitals required to produce the later report on the Mac device which will focus around Caching data, so first we need the code for the Daemon.

Script : content-cache-monitor.sh

#!/bin/bash

# Configuration
LOG_DIR="/var/log/content-cache-monitor"
LOG_FILE="${LOG_DIR}/cache_metrics.log"
DEBUG_LOG="${LOG_DIR}/debug.log"
PID_FILE="/var/run/content-cache-monitor.pid"

# Create log directory if it doesn't exist
mkdir -p "$LOG_DIR"
chmod 755 "$LOG_DIR"
touch "$LOG_FILE" "$DEBUG_LOG"
chmod 644 "$LOG_FILE" "$DEBUG_LOG"

# Function to get CPU usage
get_cpu_usage() {
    top -l 1 | grep "CPU usage" | awk '{print $3}' | cut -d'%' -f1
}

# Function to get network utilization
get_network_usage() {
    NETWORK_STATS=$(netstat -ibn | grep -v "Name" | head -n 1)
    BYTES_IN=$(echo "$NETWORK_STATS" | awk '{print $7}')
    BYTES_OUT=$(echo "$NETWORK_STATS" | awk '{print $10}')
    TOTAL_BYTES=$((BYTES_IN + BYTES_OUT))
    MAX_BYTES=$((125000000))
    USAGE=$((TOTAL_BYTES * 100 / MAX_BYTES))
    echo "$USAGE"
}

# Function to get content cache metrics
get_cache_metrics() {
    CACHE_STATUS=$(sudo AssetCacheManagerUtil status 2>/dev/null)
    
    # Get cache pressure from MaxCachePressureLast1Hour
    PRESSURE=$(echo "$CACHE_STATUS" | grep "MaxCachePressureLast1Hour:" | awk '{print $2}' | tr -d '%')
    
    # Convert pressure percentage to Low/Medium/High
    if [ -z "$PRESSURE" ] || [ "$PRESSURE" = "0" ]; then
        PRESSURE="Low"
        PRESSURE_NUM="33"
    elif [ "$PRESSURE" -lt "50" ]; then
        PRESSURE="Medium"
        PRESSURE_NUM="66"
    else
        PRESSURE="High"
        PRESSURE_NUM="100"
    fi
    
    # Get actual cache statistics
    CACHE_USED=$(echo "$CACHE_STATUS" | grep "^    CacheUsed:" | awk '{print $2 " " $3}')
    CACHE_FREE=$(echo "$CACHE_STATUS" | grep "^    CacheFree:" | awk '{print $2 " " $3}')
    CACHE_LIMIT=$(echo "$CACHE_STATUS" | grep "^    CacheLimit:" | awk '{print $2 " " $3}')
    
    echo "${PRESSURE}|${PRESSURE_NUM}|${CACHE_USED}|${CACHE_FREE}|${CACHE_LIMIT}"
}

# Function to collect and log metrics
collect_metrics() {
    while true; do
        TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
        CPU=$(get_cpu_usage)
        NETWORK=$(get_network_usage)
        CACHE_METRICS=$(get_cache_metrics)
        
        # Format everything on one line with proper separators
        echo "${TIMESTAMP}|${CPU}|${NETWORK}|${CACHE_METRICS}" >> "$LOG_FILE"
        sleep 360
    done
}

case "$1" in
    start)
        if [ -f "$PID_FILE" ]; then
            echo "Daemon already running with PID: $(cat $PID_FILE)"
            exit 1
        fi
        collect_metrics &
        echo $! > "$PID_FILE"
        echo "Daemon started with PID: $(cat $PID_FILE)"
        ;;
    stop)
        if [ -f "$PID_FILE" ]; then
            kill $(cat "$PID_FILE")
            rm "$PID_FILE"
            echo "Daemon stopped"
        else
            echo "Daemon not running"
        fi
        ;;
    restart)
        $0 stop
        sleep 1
        $0 start
        ;;
    status)
        if [ -f "$PID_FILE" ]; then
            PID=$(cat "$PID_FILE")
            if ps -p $PID > /dev/null; then
                echo "Daemon running with PID: $PID"
            else
                echo "Daemon not running (stale PID file)"
                rm "$PID_FILE"
            fi
        else
            echo "Daemon not running"
        fi
        ;;
    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac
exit 0

When this is created on the MacOS device we then need to make this executable with the command below:

chmod +x content-cache-monitor.sh

Then we need to manage the Daemon which can be done with the commands below:

Start the daemon : sudo ./content-cache-monitor.sh start
Stop the daemon: sudo ./content-cache-monitor.sh stop
Check daemon status: sudo ./content-cache-monitor.sh status
Restart the daemon : sudo ./content-cache-monitor.sh restart

Next after starting the daemon lets check the status with this command:

sudo ./content-cache-monitor.sh status

This should confirm it is running with the PID that it is running under:


cat /var/log/content-cache-monitor/cache_metrics.log


This means the data logging is working and all the variables are being recorded correctly, this will be default log every 360 seconds, if you wish to change this frequency update this section in the code, you need to update the sleep value:

      # Format everything on one line with proper separators
        echo "${TIMESTAMP}|${CPU}|${NETWORK}|${CACHE_METRICS}" >> "$LOG_FILE"
        sleep 360
    done
}

Dashboard Generation

Now we are getting the data into a CSV file we need to turn this into a html report that is easy to read, the output from this will be the image at the start of this blog post.

Script : generate_dashboard.sh

#!/bin/bash

# Configuration
LOG_FILE="/var/log/content-cache-monitor/cache_metrics.log"
OUTPUT_FILE="cache_dashboard.html"

# Function to get status class
get_status_class() {
    local value="$1"
    local type="$2"
    
    if [ "$type" = "pressure" ]; then
        if [ "$value" = "Low" ]; then
            echo "healthy"
        elif [ "$value" = "Medium" ]; then
            echo "degraded"
        else
            echo "unhealthy"
        fi
    else
        # Convert float to integer for comparison
        local int_value=$(printf "%.0f" "$value")
        if [ "$int_value" -lt 70 ]; then
            echo "healthy"
        elif [ "$int_value" -lt 85 ]; then
            echo "degraded"
        else
            echo "unhealthy"
        fi
    fi
}

# Get the last 60 entries for historical data
log_data=$(tail -n 60 "$LOG_FILE")

# Get current (last) entry
current_data=$(echo "$log_data" | tail -n 1)
IFS='|' read -r timestamp cpu network pressure pressure_num cache_used cache_free cache_limit <<< "$current_data"

# Convert pressure to uppercase for display
pressure_display=$(echo "$pressure" | tr '[:lower:]' '[:upper:]')

# Prepare historical data arrays
timestamps=""
cpu_values=""
network_values=""
first=true

while IFS='|' read -r ts cpu_val net_val press press_num cache_u cache_f cache_l; do
    if [ "$first" = true ]; then
        timestamps="'$ts'"
        cpu_values="$cpu_val"
        network_values="$net_val"
        first=false
    else
        timestamps="$timestamps,'$ts'"
        cpu_values="$cpu_values,$cpu_val"
        network_values="$network_values,$net_val"
    fi
done <<< "$log_data"

# Get status classes
pressure_status=$(get_status_class "$pressure" "pressure")
cpu_status=$(get_status_class "$cpu" "value")
network_status=$(get_status_class "$network" "value")

# Generate the dashboard HTML
cat > "$OUTPUT_FILE" << EOL
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cache Health Dashboard</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/chart.min.js"></script>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f5f5f7;
            color: #1d1d1f;
        }
        .dashboard {
            max-width: 1200px;
            margin: 0 auto;
        }
        .header {
            margin-bottom: 20px;
            padding: 10px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .header h1 {
            margin: 0;
            font-size: 24px;
            font-weight: 500;
        }
        .timestamp {
            color: #666;
            font-size: 14px;
        }
        .cards-container {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }
        .card {
            background: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .card h2 {
            margin: 0 0 10px 0;
            font-size: 16px;
            font-weight: 500;
            color: #666;
        }
        .metric {
            font-size: 32px;
            font-weight: 600;
            margin: 10px 0;
        }
        .status {
            display: inline-block;
            padding: 5px 10px;
            border-radius: 15px;
            font-size: 14px;
            font-weight: 500;
        }
        .status.healthy {
            background: #e3f7e9;
            color: #0a6b2d;
        }
        .status.degraded {
            background: #fff7e3;
            color: #946c00;
        }
        .status.unhealthy {
            background: #ffe3e3;
            color: #c11;
        }
        .charts-container {
            display: grid;
            grid-template-columns: 1fr;
            gap: 20px;
        }
        .chart-card {
            background: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            height: 400px;
        }
        .metric-detail {
            font-size: 14px;
            color: #666;
            margin: 5px 0;
        }
    </style>
</head>
<body>
    <div class="dashboard">
        <div class="header">
            <h1>Cache Health Dashboard</h1>
            <div class="timestamp">Last Updated: $timestamp</div>
        </div>
        <div class="cards-container">
            <div class="card">
                <h2>Cache Pressure</h2>
                <div class="metric">$pressure</div>
                <span class="status $pressure_status">
                    $pressure_display
                </span>
            </div>
            <div class="card">
                <h2>Cache Usage</h2>
                <div class="metric">$cache_used</div>
                <div class="metric-detail">Free: $cache_free</div>
                <div class="metric-detail">Total: $cache_limit</div>
            </div>
            <div class="card">
                <h2>System</h2>
                <div class="metric">$cpu%</div>
                <div class="metric-detail">CPU Usage</div>
                <div class="metric-detail">Network: $network%</div>
            </div>
        </div>
        <div class="charts-container">
            <div class="chart-card">
                <h2>Historical Metrics</h2>
                <canvas id="metricsChart"></canvas>
            </div>
        </div>
    </div>

    <script>
        // Create the historical metrics chart
        const ctx = document.getElementById('metricsChart').getContext('2d');
        new Chart(ctx, {
            type: 'line',
            data: {
                labels: [$timestamps],
                datasets: [
                    {
                        label: 'CPU Usage',
                        data: [$cpu_values],
                        borderColor: 'rgb(75, 192, 192)',
                        tension: 0.1
                    },
                    {
                        label: 'Network Usage',
                        data: [$network_values],
                        borderColor: 'rgb(153, 102, 255)',
                        tension: 0.1
                    }
                ]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                plugins: {
                    legend: {
                        position: 'top',
                    }
                },
                scales: {
                    y: {
                        beginAtZero: true,
                        max: 100
                    }
                }
            }
        });
    </script>
</body>
</html>
EOL

echo "Dashboard generated at: $OUTPUT_FILE"
echo "Open this file in your web browser to view the dashboard"

chmod +x generate_dashboard.sh

sed -i '' $'s/\r//' /Users/LCrouc2/generate_dashboard.sh

./generate_dashboard.sh

This will output a the html file that will format the CSV and produce a report, again that is something like this:


Problems?

If you have problems running the script with "file not found" then you may have copied this cover to the device with an incomparable format, if that is the case on the scripts run these commands to fix the issue:

sed -i '' $'s/\r//' content-cache-monitor.sh
sed -i '' $'s/\r//' generate_dashboard.sh
Previous Post Next Post

نموذج الاتصال