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:
- Only collecting metrics every 360 seconds
- Implementing efficient log rotation
- 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.
/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
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
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