Come monitorare i dati sulle prestazioni in macOS con uno script Shell

In questo articolo imparerai a monitorare i dati sulle prestazioni in macOS con uno script Shell. Nel mondo delle operazioni IT e della gestione degli endpoint, la comprensione delle prestazioni di un sistema non è facoltativa: è fondamentale. Dal garantire che il parco dispositivi Mac in ambiente enterprise rimanga integro fino alla risoluzione dei reclami degli utenti basata su metriche reali, la necessità di dati sulle prestazioni trasparenti e affidabili è più pressante che mai. Per i professionisti IT e i provider di servizi gestiti (MSP), un approccio proattivo al monitoraggio delle prestazioni può migliorare la qualità del servizio, ridurre i tempi di inattività e consentire un processo decisionale basato sui dati. È qui che entrano in gioco gli script shell automatizzati.

Contesto

Sebbene strumenti come Activity Monitor o dashboard delle prestazioni di terze parti forniscano informazioni utili, spesso mancano di automazione, coerenza e integrazione con piattaforme di gestione centralizzate come NinjaOne. Lo script che andremo ad esaminare colma questa lacuna raccogliendo un’ampia gamma di dati sulle prestazioni dei sistemi macOS come utilizzo di CPU, memoria, disco e rete, per poi formattarli in HTML e memorizzarli in un campo WYSIWYG personalizzato di NinjaOne.

Questo approccio elimina la necessità di diagnostica manuale, standardizza i report sulle prestazioni e rende le metriche visibili ai tecnici direttamente nella sezione dell’endpoint. È stato progettato per supportare i flussi di lavoro di monitoraggio zero-touch, la risoluzione dei problemi da remoto e i report di più alto livello.

Lo script per monitorare i dati sulle prestazioni in macOS:

#!/usr/bin/env bash
#
# Description: Collects system performance data (CPU, memory, disk, and network). The results can optionally be saved to a WYSIWYG custom field.
# By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use.
# Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. 
# Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. 
# Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. 
# Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. 
# Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. 
# Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. 
# EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
#
# Example: --daysSinceLastReboot "1" --durationToPerformTests "5" --numberOfEvents "5" --wysiwygCustomFieldName "WYSIWYG"
#
# [Alert] This computer was last started on 02/24/25 08:24:27 AM, which was 7.30 days ago.
# Collecting the last 5 error events from the system log.
#
# Collecting performance metrics for 5 minutes.
# Finished collecting performance metrics.
#
# Formatting total CPU results.
# Formatting CPU process data.
# Formatting the total RAM results.
# Formatting RAM process data.
# Formatting the network usage data.
# Formatting disk results.
# Formatting event log entries.
#
# Collecting system information.
#
# ### Apple M1 ###
# Average CPU % Used  Minimum CPU % Used  Maximum CPU % Used
# 48.07%              40.30%              67.37%
#
# ### Memory Usage ###
# Total Memory Installed: 16.00 GiB
# Average RAM % Used  Minimum RAM % Used  Maximum RAM % Used
# 99.52%              99.44%              99.59%
#
# ### Top 5 CPU Processes ###
# PID    Name              Average CPU % Used  Minimum CPU % Used  Maximum CPU % Used
# 656    WindowServer      21.57%              5.90%               36.90%
# 3503   firefox           18.05%              6.00%               30.50%
# 74524  Storage           8.95%               8.80%               12.70%
# 0      kernel_task       5.78%               5.40%               8.00%
# 74563  ApplicationsStor  5.02%               3.50%               10.40%
#
# ### Top 5 RAM Processes ###
# PID    Name              Average RAM % Used  Minimum RAM % Used  Maximum RAM % Used
# 3503   firefox           8.43%               8.33%               8.58%
# 72329  datagrip          7.96%               7.95%               7.96%
# 656    WindowServer      4.27%               4.26%               4.28%
# 40463  plugin-container  3.87%               3.87%               3.89%
# 828    ninjarmm-macagen  3.28%               3.27%               3.28%
#
# ### Network Usage ###
# Network Adapter  MAC Address        Type   Average Sent & Received  Minimum Sent & Received  Maximum Sent & Received
# en0              00:00:00:00:00:00  Wi-Fi  7.98 Mbps                7.91 Mbps                8.03 Mbps
#
# ### Disk Usage ###
# Volume: disk1s1
# Name: iSCPreboot
# Mount Point: /System/Volumes/iSCPreboot
# Consumed Space: 5.55 MiB (1.11%)
# Container: disk1
# Free Space: 479.16 MiB
# Total Space: 500 MiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk1s2
# Name: xART
# Mount Point: /System/Volumes/xarts
# Consumed Space: 6.02 MiB (1.20%)
# Container: disk1
# Free Space: 479.16 MiB
# Total Space: 500 MiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk1s3
# Name: Hardware
# Mount Point: /System/Volumes/Hardware
# Consumed Space: 4.52 MiB (0.90%)
# Container: disk1
# Free Space: 479.16 MiB
# Total Space: 500 MiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk1s4
# Name: Recovery
# Mount Point: N/A
# Consumed Space: 20 KiB (0%)
# Container: disk1
# Free Space: 479.16 MiB
# Total Space: 500 MiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk2s1
# Name: Recovery
# Mount Point: N/A
# Consumed Space: 1.84 GiB (36.87%)
# Container: disk2
# Free Space: 3.14 GiB
# Total Space: 5 GiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk2s2
# Name: Update
# Mount Point: N/A
# Consumed Space: 2.84 MiB (0.06%)
# Container: disk2
# Free Space: 3.14 GiB
# Total Space: 5 GiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk3s1
# Name: Macintosh HD - Data
# Mount Point: /System/Volumes/Data
# Consumed Space: 156.20 GiB (68.43%)
# Container: disk3
# Free Space: 52.70 GiB
# Total Space: 228.27 GiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk3s2
# Name: Update
# Mount Point: /System/Volumes/Update
# Consumed Space: 62.33 MiB (0.03%)
# Container: disk3
# Free Space: 52.70 GiB
# Total Space: 228.27 GiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk3s3
# Name: Macintosh HD
# Mount Point: N/A
# Consumed Space: 10.48 GiB (4.59%)
# Container: disk3
# Free Space: 52.70 GiB
# Total Space: 228.27 GiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk3s4
# Name: Preboot
# Mount Point: /System/Volumes/Preboot
# Consumed Space: 6.73 GiB (2.95%)
# Container: disk3
# Free Space: 52.70 GiB
# Total Space: 228.27 GiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk3s5
# Name: Recovery
# Mount Point: N/A
# Consumed Space: 998.96 MiB (0.43%)
# Container: disk3
# Free Space: 52.70 GiB
# Total Space: 228.27 GiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# Volume: disk3s6
# Name: VM
# Mount Point: /System/Volumes/VM
# Consumed Space: 1 GiB (0.44%)
# Container: disk3
# Free Space: 52.70 GiB
# Total Space: 228.27 GiB
# Physical Disk: APPLE SSD AP0256Q
# Media Type: SSD
#
# The custom field 'WYSIWYG' has been provided.
# Converting CPU process data into HTML.
# Converting RAM process data into HTML.
# Converting network usage data into HTML.
# Converting disk usage data into HTML.
# Assembling the html data into the html card
# Highlighting last startup date.
# Highlighting overall CPU usage metrics.
# Converting system log data into an HTML format.
#
# Attempting to set the custom field 'WYSIWYG'.
# Successfully set the custom field 'WYSIWYG'.
#
# ### Last 5 warnings and errors in the system log. ###
# Service: trustd
# PID: 1656
# Time Created: 03/03/25 03:37:58 PM
# Message: (Security) [com.apple.security:seckey] SecKeyVerifySignature failed: Error Domain=NSOSStatusErrorDomain Code=-67808 "RSA signature verification failed, no match" UserInfo={numberOfErrorsDeep=0, NSDescription=RSA signature verification failed, no match}
#
# Service: trustd
# PID: 1656
# Time Created: 03/03/25 03:37:58 PM
# Message: (Security) [com.apple.security:seckey] SecKeyVerifySignature failed: Error Domain=NSOSStatusErrorDomain Code=-67808 "RSA signature verification failed, no match" UserInfo={numberOfErrorsDeep=0, NSDescription=RSA signature verification failed, no match}
#
# Service: trustd
# PID: 1656
# Time Created: 03/03/25 03:37:58 PM
# Message: (Security) [com.apple.security:seckey] SecKeyVerifySignature failed: Error Domain=NSOSStatusErrorDomain Code=-67808 "RSA signature verification failed, no match" UserInfo={numberOfErrorsDeep=0, NSDescription=RSA signature verification failed, no match}
#
# Service: trustd
# PID: 1656
# Time Created: 03/03/25 03:37:58 PM
# Message: (Security) [com.apple.security:seckey] SecKeyVerifySignature failed: Error Domain=NSOSStatusErrorDomain Code=-67808 "RSA signature verification failed, no match" UserInfo={numberOfErrorsDeep=0, NSDescription=RSA signature verification failed, no match}
#
# Service: trustd
# PID: 1656
# Time Created: 03/03/25 03:37:58 PM
# Message: (Security) [com.apple.security:seckey] SecKeyVerifySignature failed: Error Domain=NSOSStatusErrorDomain Code=-67808 "RSA signature verification failed, no match" UserInfo={numberOfErrorsDeep=0, NSDescription=RSA signature verification failed, no match}
#
# Preset Parameter: --daysSinceLastReboot "7"
#		Specify the number of days by which the system should have been rebooted.
#
# Preset Parameter: --durationToPerformTests "5"
#		The duration (in minutes) for which the performance tests should be executed.
#
# Preset Parameter: --numberOfEvents "5"
#		The number of error events to retrieve from the system log.
#
# Preset Parameter: --wysiwygCustomField "ReplaceMeWithAnyWYSIWYGCustomField"
#		Optionally specify the name of a WYSIWYG custom field to store the formatted performance data.
#
# Preset Parameter: --help
#		Displays some help text.
#
# Minimum Supported OS: macOS Ventura
# Release Notes: Initial Release

# Script arguments
_arg_daysSinceLastReboot=
_arg_durationToPerformTests=5
_arg_numberOfEvents=5
_arg_wysiwygCustomField=
exitCode=0

# Converts a string input into an HTML table format.
convertToHTMLTable() {
  local _arg_delimiter=" "
  local _arg_inputObject

  # Process command-line arguments for the function.
  while test $# -gt 0; do
    _key="$1"
    case "$_key" in
      --delimiter | -d)
        test $# -lt 2 && echo "[Error] Missing value for the required argument" >&2 && return 1
        _arg_delimiter=$2
        shift
        ;;
      --*)
        echo "[Error] Got an unexpected argument" >&2
        return 1
        ;;
      *)
        _arg_inputObject=$1
        ;;
    esac
    shift
  done

  # Handles missing input by checking stdin or returning an error.
  if [[ -z $_arg_inputObject ]]; then
    if [ -p /dev/stdin ]; then
      _arg_inputObject=$(cat)
    else
      echo "[Error] Missing input object to convert to table" >&2
      return 1
    fi
  fi

  _arg_inputObject=${_arg_inputObject//"%"/"%%"}

  local htmlTable="<table>\n"
  htmlTable+=$(printf '%b' "$_arg_inputObject" 2> /dev/null | head -n1 | awk -F "$_arg_delimiter" '{
    printf "<tr>"
    for (i=1; i<=NF; i+=1)
      { printf "<th>"$i"</th>" }
    printf "</tr>"
    }')
  htmlTable+="\n"
  htmlTable+=$(printf '%b' "$_arg_inputObject" | tail -n +2 | awk -F "$_arg_delimiter" '{
    printf "<tr>"
    for (i=1; i<=NF; i+=1)
      { printf "<td>"$i"</td>" }
    print "</tr>"
    }')
  htmlTable+="\n</table>"

  printf '%b' "$htmlTable" '\n'
}

ConvertTo-FriendlySize() {
  local size=$1
  local units=(B KiB MiB GiB TiB PiB)
  local i=0

  while (($(echo "$size >= 1024" | bc -l) && i < ${#units[@]} - 1)); do
    size=$(echo "scale=6; $size/1024" | bc)
    ((i++))
  done

  # Round to two decimal places
  size=$(printf "%.2f" "$size")

  # Remove trailing .00 if present
  size=${size//.00/}

  echo "${size} ${units[i]}"
}

# Help menu
print_help() {
  printf '\n\n%s\n\n' 'Usage: [--daysSinceLastReboot|-r <arg>] [--durationToPerformTests|-d <arg>] [--numberOfEvents|-n <arg>]
  [--wysiwygCustomField|-w <arg>] [--help|-h]'
  printf '%s\n' 'Preset Parameter: --daysSinceLastReboot "7"'
  printf '\t%s\n' "Specify the number of days by which the system should have been rebooted."
  printf '%s\n' 'Preset Parameter: --durationToPerformTests "5"'
  printf '\t%s\n' "The duration (in minutes) for which the performance tests should be executed."
  printf '%s\n' 'Preset Parameter: --numberOfEvents "5"'
  printf '\t%s\n' "The number of error events to retrieve from the system log."
  printf '%s\n' 'Preset Parameter: --wysiwygCustomField "ReplaceMeWithAnyWYSIWYGCustomField"'
  printf '\t%s\n' "Optionally specify the name of a WYSIWYG custom field to store the formatted performance data."
  printf '%s\n' 'Preset Parameter: --help'
  printf '\t%s\n' "Displays this help menu."
}

die() {
  local _ret="${2:-1}"
  echo "$1" >&2
  test "${_PRINT_HELP:-no}" = yes && print_help >&2
  exit "${_ret}"
}

# Parses the given command line parameters
parse_commandline() {
  while test $# -gt 0; do
    _key="$1"
    case "$_key" in
      --daysSinceLastReboot | --dayssincelastreboot | --days | -r)
        test $# -lt 2 && die "[Error] Missing value for the optional argument '$_key'." 1
        _arg_daysSinceLastReboot=$2
        shift
        ;;
      --daysSinceLastReboot=*)
        _arg_daysSinceLastReboot="${_key##--daysSinceLastReboot=}"
        ;;
      --durationToPerformTests | --durationtoperformtests | --duration | -d)
        test $# -lt 2 && die "[Error] Missing value for the optional argument '$_key'." 1
        _arg_durationToPerformTests=$2
        shift
        ;;
      --durationToPerformTests=*)
        _arg_durationToPerformTests="${_key##--durationToPerformTests=}"
        ;;
      --numberOfEvents | --numberofevents | --events | -n)
        test $# -lt 2 && die "[Error] Missing value for the optional argument '$_key'." 1
        _arg_numberOfEvents=$2
        shift
        ;;
      --numberOfEvents=*)
        _arg_numberOfEvents="${_key##--numberOfEvents=}"
        ;;
      --wysiwygCustomField | --wysiwygcustomfield | --wysiwyg | -w)
        test $# -lt 2 && die "[Error] Missing value for the optional argument '$_key'." 1
        _arg_wysiwygCustomField=$2
        shift
        ;;
      --wysiwygCustomField=*)
        _arg_wysiwygCustomField="${_key##--wysiwygCustomField=}"
        ;;
      --help | -h)
        _PRINT_HELP=yes die
        ;;
      *)
        _PRINT_HELP=yes die "[Error] Got an unexpected argument '$1'" 1
        ;;
    esac
    shift
  done
}

echo " "
parse_commandline "$@"

# If script form variables are used, replace the command line parameters with their value.
if [[ -n $daysSinceLastReboot ]]; then
  _arg_daysSinceLastReboot="$daysSinceLastReboot"
fi
if [[ -n $durationToPerformTests ]]; then
  _arg_durationToPerformTests="$durationToPerformTests"
fi
if [[ -n $numberOfEvents ]]; then
  _arg_numberOfEvents="$numberOfEvents"
fi
if [[ -n $wysiwygCustomFieldName ]]; then
  _arg_wysiwygCustomField="$wysiwygCustomFieldName"
fi

# Ensure the script is being run with root permissions
if [[ $(id -u) -ne 0 ]]; then
  _PRINT_HELP=no die "[Error] This script must be run with root permissions. Try running it with sudo or as the system/root user." 1
fi

# Validate and sanitize the 'Days Since Last Reboot' argument
if [[ -n "$_arg_daysSinceLastReboot" ]]; then
  _arg_daysSinceLastReboot=$(echo "$_arg_daysSinceLastReboot" | xargs)

  # Check if the argument is empty after trimming
  if [[ -z "$_arg_daysSinceLastReboot" ]]; then
    _PRINT_HELP=yes die "[Error] The 'Days Since Last Reboot' value of '$_arg_daysSinceLastReboot' is invalid. Please provide a positive whole number or 0."
  fi
fi

# Ensure 'Days Since Last Reboot' is a valid numeric value
if [[ -n "$_arg_daysSinceLastReboot" && "$_arg_daysSinceLastReboot" =~ [^0-9] ]]; then
  _PRINT_HELP=yes die "[Error] The 'Days Since Last Reboot' value of '$_arg_daysSinceLastReboot' is invalid. Please provide a positive whole number or 0." 1
fi

# Validate and sanitize the 'Duration To Perform Tests' argument
if [[ -n "$_arg_durationToPerformTests" ]]; then
  _arg_durationToPerformTests=$(echo "$_arg_durationToPerformTests" | xargs)
fi

# Ensure the duration is provided
if [[ -z "$_arg_durationToPerformTests" ]]; then
  _PRINT_HELP=yes die "[Error] Please provide a valid duration for performing the tests." 1
fi

# Check if the duration is a valid positive number between 1 and 60
if [[ "$_arg_durationToPerformTests" =~ [^0-9] || "$_arg_durationToPerformTests" -eq 0 || "$_arg_durationToPerformTests" -ge 60 ]]; then
  _PRINT_HELP=yes die "[Error] The 'Duration To Perform Tests' value of '$_arg_durationToPerformTests' is invalid.
[Error] Please provide a positive whole number that's greater than 0 and less than 60." 1
fi

# Validate and sanitize the 'Number of Events' argument
if [[ -n "$_arg_numberOfEvents" ]]; then
  _arg_numberOfEvents=$(echo "$_arg_numberOfEvents" | xargs)
fi

# Ensure the number of events is provided
if [[ -z "$_arg_numberOfEvents" ]]; then
  _PRINT_HELP=yes die "[Error] Please provide a valid number of recent error events to include in the results." 1
fi

# Check if the number of events is a valid numeric value and within the allowed range
if [[ "$_arg_numberOfEvents" =~ [^0-9] || "$_arg_numberOfEvents" -gt 10000 ]]; then
  _PRINT_HELP=yes die "[Error] The 'Number of Events' value of '$_arg_numberOfEvents' is invalid. Please provide a positive whole number that is less than or equal to 10000 or greater than or equal to 0." 1
fi

# Validate and sanitize the 'WYSIWYG Custom Field' argument
if [[ -n "$_arg_wysiwygCustomField" ]]; then
  _arg_wysiwygCustomField=$(echo "$_arg_wysiwygCustomField" | xargs)

  # Check if the argument is empty
  if [[ -z "$_arg_wysiwygCustomField" ]]; then
    _PRINT_HELP=yes die "[Error] The 'WYSIWYG Custom Field' value of '$_arg_wysiwygCustomField' is invalid. Please provide a valid custom field name.
  https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Field-Setup" 1
  fi
fi

# Check if the custom field contains invalid characters
if [[ -n "$_arg_wysiwygCustomField" && "$_arg_wysiwygCustomField" =~ [^0-9a-zA-Z0-9] ]]; then
  _PRINT_HELP=yes die "[Error] The 'WYSIWYG Custom Field' value of '$_arg_wysiwygCustomField' is invalid. Please provide a valid custom field name.
  https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Field-Setup" 1
fi

# Locate the ninjarmm-cli tool, which is required for setting custom fields
if [[ -n "$_arg_wysiwygCustomField" ]]; then
  if [[ -n $NINJA_DATA_PATH && -f "$NINJA_DATA_PATH/ninjarmm-cli" ]]; then
    ninjarmmcliPath="$NINJA_DATA_PATH/ninjarmm-cli"
  elif [[ -f "/Applications/NinjaRMMAgent/programdata/ninjarmm-cli" ]]; then
    ninjarmmcliPath=/Applications/NinjaRMMAgent/programdata/ninjarmm-cli
  else
    _PRINT_HELP=no die "[Error] Unable to locate ninjarmm-cli. Ninjarmm-cli is required to set custom fields.
    https://ninjarmm.zendesk.com/hc/en-us/articles/4405408656013-Custom-Fields-and-Documentation-CLI-and-Scripting#h_01FB4NZ6GCG2V53NS4T1RQX8PY" 1
  fi
fi

# Ensure the script does not run if another instance is already running
lockFile=/Applications/NinjaRMMAgent/SystemPerformance.lock
if [[ -f "$lockFile" ]]; then
  echo "Process lock file found at '$lockFile'. Checking if the process is still running."

  if ! otherScript=$(cat "$lockFile"); then
    _PRINT_HELP=no die "[Error] Unable to access the lock file at '$lockFile'." 1
  fi

  if ! scriptProcess=$(ps -p "$otherScript" | sed '1d'); then
    _PRINT_HELP=no die "[Error] Unable to verify if another copy of the script is currently running with the process id (PID)." 1
  fi

  if [[ -n "$scriptProcess" ]]; then
    _PRINT_HELP=no die "[Error] This script is already running in another process with the process id (PID) '$otherScript'. Please wait for that process to complete." 1
  fi
fi

# Create a lock file to prevent multiple script instances
if ! echo $$ > $lockFile; then
  _PRINT_HELP=no die "[Error] Failed to create the process lock file at '$lockFile'. This is to prevent multiple instances of the performance script from running simultaneously." 1
fi

# Record the start date and time of the script
reportStartDate=$(date "+%x %r")

# Calculate the system's last boot time and check if it exceeds the allowed days since reboot
lastBoot=$(sysctl -n kern.boottime | awk '{ print $4 }' | sed "s/[^0-9]//g")
lastStartupDate=$(date -r "$lastBoot" "+%x %r")
if [[ -n "$_arg_daysSinceLastReboot" && $(date -v "-$_arg_daysSinceLastReboot"d "+%s") -gt "$lastBoot" ]]; then
  secondsSinceLastBoot=$(($(date "+%s") - lastBoot))
  daysSinceLastBoot=$(echo "$secondsSinceLastBoot" | awk '{printf "%.2f\n", $1 / 86400}')
  echo "[Alert] This computer was last started on $lastStartupDate, which was $daysSinceLastBoot days ago."

  exceededLastStartupLimit="true"
fi

# Collect error events from the system logs
if [[ "$_arg_numberOfEvents" -gt 0 ]]; then
  echo "Collecting the last $_arg_numberOfEvents error events from the system log."

  predicateFilter='(messageType == "error")'
  log show --last boot --predicate "$predicateFilter" --style "syslog" | tail -n "$_arg_numberOfEvents" > /tmp/syslog.log 2> /tmp/syslogErr.log &
fi

# Collect CPU, RAM, network, and disk performance metrics
echo ""
echo "Collecting performance metrics for $_arg_durationToPerformTests minutes."
diskutil apfs list -plist > /tmp/diskData.log 2> /tmp/diskDataErr.log
memory_pressure > /tmp/totalMemData.log 2> /tmp/totalMemDataErr.log
COLUMNS=10000 top -l "$((_arg_durationToPerformTests + 1))" -s 60 -o cpu -stats pid,cpu,command > /tmp/cpuData.log 2> /tmp/cpuDataErr.log &
COLUMNS=10000 top -l "$((_arg_durationToPerformTests + 1))" -s 60 -o mem -stats pid,mem,command > /tmp/memData.log 2> /tmp/memDataErr.log &
nettop -t external -x -L "$((_arg_durationToPerformTests + 1))" -s 60 > /tmp/netData.log 2> /tmp/netDataErr.log &

i=1
while ((i < "$_arg_durationToPerformTests")); do
  memory_pressure >> /tmp/totalMemData.log 2>> /tmp/totalMemDataErr.log

  sleep 60
  ((i++))
done

# Wait for background tasks to complete
wait

# Validate collected event logs and handle errors
if [[ "$_arg_numberOfEvents" -gt 0 ]]; then
  if [[ -s "/tmp/syslogErr.log" ]]; then
    cat "/tmp/syslogErr.log" >&2
    echo "[Error] Errors were detected while attempting to retrieve the last $_arg_numberOfEvents events." >&2
    exitCode=1
  fi

  if [[ -f "/tmp/syslog.log" ]]; then
    syslogLines=$(wc -l /tmp/syslog.log | grep -o "[0-9]*" | xargs)
  fi

  if [[ -z "$syslogLines" || "$syslogLines" =~ [^0-9] || "$syslogLines" -le 1 ]]; then
    echo "[Warning] No errors were found in the system log since the last boot. Is the system log service enabled?"
  fi
fi

# Validate collected performance data and handle errors
if [[ -s "/tmp/diskDataErr.log" ]]; then
  cat "/tmp/diskDataErr.log"
  echo "[Warning] Errors were detected while attempting to gather disk performance metrics."
fi
if [[ ! -s "/tmp/diskData.log" ]]; then
  _PRINT_HELP=no die "[Error] No disk performance metrics were collected." 1
fi
if [[ -s "/tmp/cpuDataErr.log" ]]; then
  cat "/tmp/cpuDataErr.log" >&2
  _PRINT_HELP=no die "[Error] Errors were detected while attempting to gather CPU performance metrics." 1
fi
if [[ ! -s "/tmp/cpuData.log" ]]; then
  _PRINT_HELP=no die "[Error] No cpu performance metrics were collected." 1
fi
if [[ -s "/tmp/memDataErr.log" ]]; then
  cat "/tmp/memDataErr.log" >&2
  _PRINT_HELP=no die "[Error] Errors were detected while attempting to gather RAM performance metrics." 1
fi
if [[ ! -s "/tmp/memData.log" ]]; then
  _PRINT_HELP=no die "[Error] No RAM performance metrics were collected." 1
fi
if [[ -s "/tmp/totalMemDataErr.log" ]]; then
  cat "/tmp/totalMemDataErr.log" >&2
  _PRINT_HELP=no die "[Error] Errors were detected while attempting to gather RAM performance metrics." 1
fi
if [[ ! -s "/tmp/totalMemData.log" ]]; then
  _PRINT_HELP=no die "[Error] No RAM performance metrics were collected." 1
fi
if [[ -s "/tmp/netDataErr.log" ]]; then
  cat "/tmp/netDataErr.log" >&2
  _PRINT_HELP=no die "[Error] Errors were detected while attempting to gather network performance metrics." 1
fi
if [[ ! -s "/tmp/netData.log" ]]; then
  _PRINT_HELP=no die "[Error] No network performance metrics were collected." 1
fi

# Indicate the completion of performance metric collection
echo "Finished collecting performance metrics."
echo ""

# Process and format the total CPU usage metrics
echo "Formatting total CPU results."
cpuTotalData=$(
  awk '
    BEGIN {
      # Define the table header for CPU results
      header = "Average CPU % Used;|;Minimum CPU % Used;|;Maximum CPU % Used"
    }
    /^[[:space:]]*CPU usage:/{

      # Extract CPU idle percentage and calculate the used percentage
      if ($7 ~ /[0-9]/) {
        split($7, cpuIdlePerc,"%")
        cpuPerc = 100 - $7
      }

      # Aggregate metrics
      countCpu += 1
      cpuSum += cpuPerc

      # Determine minimum and maximum CPU usage
      if(! cpuMin ){ cpuMin = cpuPerc }
      if(cpuPerc < cpuMin){ cpuMin = cpuPerc }
      if(! cpuMax){ cpuMax = cpuPerc }
      if(cpuPerc > cpuMax){ cpuMax = cpuPerc }
    }
    END {
      # Print the formatted CPU usage data
      print header
      avgCpu = cpuSum / countCpu
      printf "%.2f%%;|;%.2f%%;|;%.2f%%\n", avgCpu, cpuMin, cpuMax
    }
  ' '/tmp/cpuData.log'
)

# Extract specific CPU metrics (average, minimum, and maximum) from the formatted data
CPUAverage=$(echo "$cpuTotalData" | column -t -s ";|;" | grep "[0-9]" | awk '{ print $1 }')
CPUMinimum=$(echo "$cpuTotalData" | column -t -s ";|;" | grep "[0-9]" | awk '{ print $2 }')
CPUMaximum=$(echo "$cpuTotalData" | column -t -s ";|;" | grep "[0-9]" | awk '{ print $3 }')

# Process and format CPU usage by process
echo "Formatting CPU process data."
cpuProcessData=$(
  awk '
    BEGIN {
      # Define the table header for process-level CPU metrics
      header = "PID;|;Name;|;Average CPU % Used;|;Minimum CPU % Used;|;Maximum CPU % Used"
    }
    /^[[:space:]]*[0-9]/ && NF >= 3{
      # Extract process information (PID, name, and CPU usage)
      procId = $1
      gsub(/[^0-9]/, "", procId)

      cpuPerc = $2

      procName = ""
      for (i = 3; i <= NF; i++) {
        procName = procName $i " "
      }
      gsub(/^[ \t\r\n]+/, "", procName)
      gsub(/[ \t\r\n]+$/, "", procName)

      if (procName == "nettop" || procName == "top" || procName == "log") {
        next
      }

      # Aggregate CPU metrics for each process
      sumCpu[procId] += cpuPerc
      countCpu[procId] += 1
      pidName[procId] = procName

      # Determine minimum and maximum CPU usage for each process
      if(! cpuMin[procId] ){ cpuMin[procId] = cpuPerc }
      if(cpuPerc < cpuMin[procId]){ cpuMin[procId] = cpuPerc }
      if(! cpuMax[procId]){ cpuMax[procId] = cpuPerc }
      if(cpuPerc > cpuMax[procId]){ cpuMax[procId] = cpuPerc }
    }
    END {
      # Print the formatted process-level CPU data
      print header
      for (pid in sumCpu) {
        avgCpu = sumCpu[pid] / countCpu[pid]
        printf "%s;|;%s;|;%.2f%%;|;%.2f%%;|;%.2f%%\n", pid, pidName[pid], avgCpu, cpuMin[pid], cpuMax[pid]
      }
    }
  ' '/tmp/cpuData.log' | {
    # Sort and display the top 6 processes by average CPU usage
    read -r header
    echo "$header"
    sed 's/;|;/\//g' |
    sort -t '/' -k3,3nr |
    sed 's/\//;|;/g'
  } | head -n 6
)

# Process and format the total RAM usage metrics
echo "Formatting the total RAM results."
memTotalData=$(
  awk '
    BEGIN {
      # Define the table header for RAM results
      header = "Average RAM % Used;|;Minimum RAM % Used;|;Maximum RAM % Used"
    }
    /^[[:space:]]*System-wide/ {
      # Compute the percentage of RAM used
      gsub(/%/, "", $5)
      memPerc = 100 - $5

      # Aggregate metrics
      memSum += memPerc
      memCount += 1

      # Determine minimum and maximum RAM usage
      if(! memMin ){ memMin = memPerc }
      if(memPerc < memMin){ memMin = memPerc }
      if(! memMax){ memMax = memPerc }
      if(memPerc > memMax){ memMax = memPerc }
    }
    END {
      # Print the formatted RAM usage data
      print header
      avgMem = memSum / memCount
      printf "%.2f%%;|;%.2f%%;|;%.2f%%\n", avgMem, memMin, memMax
    }
  ' '/tmp/totalMemData.log'
)

# Extract specific RAM metrics (average, minimum, and maximum) from the formatted data
MEMAverage=$(echo "$memTotalData" | column -t -s ";|;" | grep "[0-9]" | awk '{ print $1 }')
MEMMinimum=$(echo "$memTotalData" | column -t -s ";|;" | grep "[0-9]" | awk '{ print $2 }')
MEMMaximum=$(echo "$memTotalData" | column -t -s ";|;" | grep "[0-9]" | awk '{ print $3 }')

# Process and format RAM usage by process
echo "Formatting RAM process data."
memProcessData=$(
  awk '
    BEGIN {
      # Define the table header for process-level RAM metrics
      header = "PID;|;Name;|;Average RAM % Used;|;Minimum RAM % Used;|;Maximum RAM % Used"
    }
    /^[[:space:]]*PhysMem:/ {
      # Extract total and free memory and calculate used percentage
      memFreeHumanFriendly = $8
      memUsedHumanFriendly = $2

      # Split the free memory string to isolate the numeric part (ignoring non-digits).
      split(memFreeHumanFriendly, memFreeNumber,/[^0-9]/)

      # Similarly, split the used memory string.
      split(memUsedHumanFriendly, memUsedNumber,/[^0-9]/)

      # Convert the free memory value to bytes based on the unit (T, G, M, or K).
      if(memFreeHumanFriendly ~ /T/){
        memFreeBytes = memFreeNumber[1] * 1000000000000
      }
      if(memFreeHumanFriendly ~ /G/){
        memFreeBytes = memFreeNumber[1] * 1000000000
      }
      if(memFreeHumanFriendly ~ /M/){
        memFreeBytes = memFreeNumber[1] * 1000000
      }
      if(memFreeHumanFriendly ~ /K/){
        memFreeBytes = memFreeNumber[1] * 1000
      }

      # Convert the used memory value to bytes based on the unit (T, G, M, or K).
      if(memUsedHumanFriendly ~ /T/){
        memUsedBytes = memUsedNumber[1] * 1000000000000
      }
      if(memUsedHumanFriendly ~ /G/){
        memUsedBytes = memUsedNumber[1] * 1000000000
      }
      if(memUsedHumanFriendly ~ /M/){
        memUsedBytes = memUsedNumber[1] * 1000000
      }
      if(memUsedHumanFriendly ~ /K/){
        memUsedBytes = memUsedNumber[1] * 1000
      }

      # Calculate the total system memory in bytes.
      memTotal = memUsedBytes + memFreeBytes
    }
    /^[[:space:]]*[0-9]/ && NF >= 3 {
      # Extract the process ID (PID) from the first field and remove any non-digit characters.
      procId = $1
      gsub(/[^0-9]/, "", procId)

      # Concatenate fields 3 to NF to build the process name.
      procName = ""
      for (i = 3; i <= NF; i++) {
        procName = procName $i " "
      }

      # Trim leading and trailing whitespace from the process name.
      gsub(/^[ \t\r\n]+/, "", procName)
      gsub(/[ \t\r\n]+$/, "", procName)

      # Skip processing for specific process names that are not relevant.
      if (procName == "nettop" || procName == "top" || procName == "log") {
        next
      }

      # Extract the process-specific used memory (in a human-friendly format) from the second field.
      memProcessUsedHumanFriendly = $2

      # Split the process used memory string to extract the numeric part.
      split(memProcessUsedHumanFriendly, memProcessUsedNumber,/[^0-9]/)

      # Convert the process used memory value to bytes based on its unit.
      if(memProcessUsedHumanFriendly ~ /T/){
        memProcessUsedBytes = memProcessUsedNumber[1] * 1000000000000
      }
      if(memProcessUsedHumanFriendly ~ /G/){
        memProcessUsedBytes = memProcessUsedNumber[1] * 1000000000
      }
      if(memProcessUsedHumanFriendly ~ /M/){
        memProcessUsedBytes = memProcessUsedNumber[1] * 1000000
      }
      if(memProcessUsedHumanFriendly ~ /K/){
        memProcessUsedBytes = memProcessUsedNumber[1] * 1000
      }

      # Calculate the percentage of system RAM used by this process.
      ramPerc = ( memProcessUsedBytes / memTotal ) * 100

      # Aggregate RAM metrics for each process
      sumRam[procId] += ramPerc
      countRam[procId] += 1
      pidName[procId] = procName

      # Determine minimum and maximum RAM usage for each process
      if(! ramMin[procId] ){ ramMin[procId] = ramPerc }
      if(ramPerc < ramMin[procId]){ ramMin[procId] = ramPerc }
      if(! ramMax[procId]){ ramMax[procId] = ramPerc }
      if(ramPerc > ramMax[procId]){ ramMax[procId] = ramPerc }
    }
    END {
      # Print the formatted process-level RAM data
      print header
      for (pid in sumRam) {
        avgRam = sumRam[pid] / countRam[pid]
        printf "%s;|;%s;|;%.2f%%;|;%.2f%%;|;%.2f%%\n", pid, pidName[pid], avgRam, ramMin[pid], ramMax[pid]
      }
    }
  ' '/tmp/memData.log' | {
    # Sort and display the top 6 processes by average RAM usage
    read -r header
    echo "$header"
    sed 's/;|;/\//g' |
    sort -t '/' -k3,3nr |
    sed 's/\//;|;/g'
  } | head -n 6
)

# Process and format network usage metrics
echo "Formatting the network usage data."
netData=$(
  awk -F "," '
    BEGIN {
      # Define the table header for network usage metrics
      header = "Network Adapter;|;MAC Address;|;Type;|;Average Sent & Received;|;Minimum Sent & Received;|;Maximum Sent & Received"

      # Set SUBSEP to "|" for multidimensional array keys.
      SUBSEP = "|"
    }
    # Process only lines that have a non-empty 3rd field and start with a digit (ignoring leading spaces)
    length($3) > 0 && /^[[:space:]]*[0-9]/{
      netAdapter = $3
      receivedBytes = $5
      transmittedBytes = $6

      # Split the first field (time) using ":" as a delimiter.
      split($1, time, ":")

      # Skip the loopback interface "lo0".
      if (netAdapter == "lo0"){
        next
      }

      # Calculate total bits transferred: add received and transmitted bytes, then multiply by 8.
      totalBits = (receivedBytes + transmittedBytes) * 8

      # Convert total bits to megabits.
      totalMegabits = totalBits / 1000000

      # Aggregate metrics
      total[netAdapter,time[2]] += totalMegabits
      adapterName[netAdapter,time[2]] = netAdapter
    }
    END {
      # Print the formatted network usage data
      print header

      # Loop over each unique composite key from the adapterName array.
      for (adapter in adapterName) {
        # Map the composite key to the adapter name for aggregation.
        name[adapterName[adapter]] = adapterName[adapter]

        # Calculate the per-second average usage for this time slice by dividing by 60 seconds.
        perSecondAvg = (total[adapter] / 60)

        # Sum up the per-second averages for each adapter.
        netSum[adapterName[adapter]] += perSecondAvg
        countAdapter[adapterName[adapter]] +=1

        # Get the minimum and maximum
        if(! netMin[adapterName[adapter]]){ netMin[adapterName[adapter]] = perSecondAvg }
        if(perSecondAvg < netMin[adapterName[adapter]]){ netMin[adapterName[adapter]] = perSecondAvg }
        if(! netMax[adapterName[adapter]]){ netMax[adapterName[adapter]] = perSecondAvg }
        if(perSecondAvg > netMax[adapterName[adapter]]){ netMax[adapterName[adapter]] = perSecondAvg }
      }

      # Loop over each aggregated adapter.
      for (combinedAdapter in netSum) {
        # Run ifconfig command to fetch the MAC address for the adapter.
        ifconfig = "ifconfig " name[combinedAdapter]

        while ((ifconfig | getline line) > 0) {
          # Look for the line containing the "ether" keyword.
          if (line ~ /ether/) {
            numberOfFields = split(line, field, " ")

            # Loop through the fields to find the MAC address following "ether".
            for (i = 1; i <= numberOfFields; i++) {
              if (field[i] ~ /ether/) {
                adapterMac = field[i+1]
              }
            }
          }
        }
        close(ifconfig)

        # Run networksetup command to determine the adapter type.
        # It searches for the hardware port corresponding to the adapter name.
        networkSetup = "networksetup -listallhardwareports | grep -B 1 \"" name[combinedAdapter] "\" | head -n 1 | sed \"s/Hardware Port: //\""

        while ((networkSetup | getline line) > 0) {
          # If the line contains "Wi-Fi", set the adapter type accordingly.
          if (line ~ /Wi-Fi/){
            adapterType = "Wi-Fi"
          } else if (line ~ /Ethernet Adapter/) {
            adapterType = "Wired"
          }else {
            adapterType = "Other"
          }
        }
        close(networkSetup)

        # Calculate the average network usage for this adapter.
        avgNet = netSum[combinedAdapter] / countAdapter[combinedAdapter]

        # Print the final results in CSV format:
        # Adapter name, MAC address, adapter type, average, minimum, and maximum usage (appended with "Mbps").
        printf "%s;|;%s;|;%s;|;%.2f Mbps;|;%.2f Mbps;|;%.2f Mbps\n", name[combinedAdapter], adapterMac, adapterType, avgNet, netMin[combinedAdapter], netMax[combinedAdapter]
      }
    }
  ' '/tmp/netData.log'
)

echo "Formatting disk results."
i=0
apfsContainers=$(/usr/libexec/PlistBuddy -c 'print' /tmp/diskData.log | grep "ContainerReference" | sed "s/ContainerReference =//" | xargs)
diskData="Volume;|;Name;|;Mount Point;|;Consumed Space;|;Container;|;Free Space;|;Total Space;|;Physical Disk;|;Media Type"
for container in $apfsContainers; do
  apfsVolumes=$(/usr/libexec/PlistBuddy -c "Print ':Containers:$i:Volumes'" /tmp/diskData.log | grep "DeviceIdentifier" | sed "s/DeviceIdentifier =//" | xargs)
  containerFreeSpace=$(/usr/libexec/PlistBuddy -c "Print :Containers:$i:CapacityFree" /tmp/diskData.log)
  containerTotalSize=$(/usr/libexec/PlistBuddy -c "Print :Containers:$i:CapacityCeiling" /tmp/diskData.log)

  diskInfo=$(diskutil info "$container")
  physicalDisk=$(echo "$diskInfo" | grep "Device / Media Name:" | sed "s/Device \/ Media Name://g" | xargs)
  if echo "$diskInfo" | grep "Removable Media:" | sed "s/Removable Media://g" | grep "Removable"; then
    continue
  fi

  if echo "$diskInfo" | grep "Solid State" | grep "Yes" > /dev/null 2> /dev/null; then
    deviceType="SSD"
  elif echo "$diskInfo" | grep "Solid State" | grep "No" > /dev/null 2> /dev/null; then
    deviceType="HDD"
  else
    deviceType="Unspecified"
  fi

  v=0
  for volume in $apfsVolumes; do
    volumeInfo=$(diskutil info "$volume")
    mountPoint=$(echo "$volumeInfo" | grep "Mount Point:" | sed "s/Mount Point://g" | xargs)
    volumeName=$(/usr/libexec/PlistBuddy -c "Print ':Containers:$i:Volumes:$v:Name'" /tmp/diskData.log)
    consumedSpace=$(/usr/libexec/PlistBuddy -c "Print ':Containers:$i:Volumes:$v:CapacityInUse'" /tmp/diskData.log)
    capacityQuota=$(/usr/libexec/PlistBuddy -c "Print ':Containers:$i:Volumes:$v:CapacityQuota'" /tmp/diskData.log)

    if [[ "$capacityQuota" == 0 ]]; then
      freeSpace="$containerFreeSpace"
      totalSpace=$containerTotalSize
    else
      freeSpace=$((capacityQuota - consumedSpace))
      totalSpace=$capacityQuota
    fi

    if [[ -n "$totalSpace" ]]; then
      consumedSpacePercentage=$(echo "scale=6; $consumedSpace / $totalSpace" | bc)
      consumedSpacePercentage=$(echo "scale=6; $consumedSpacePercentage * 100" | bc)
      consumedSpacePercentage=$(printf "%.2f" "$consumedSpacePercentage")
      consumedSpacePercentage=${consumedSpacePercentage//.00/}

      totalSpaceInGB=$(ConvertTo-FriendlySize "$totalSpace")
    fi

    consumedSpaceInGB=$(ConvertTo-FriendlySize "$consumedSpace")
    freeSpaceInGB=$(ConvertTo-FriendlySize "$freeSpace")

    if [[ -z "$mountPoint" ]]; then
      mountPoint="N/A"
    fi

    diskData+=$'\n'"$volume;|;$volumeName;|;$mountPoint;|;$consumedSpaceInGB ($consumedSpacePercentage%);|;$container;|;$freeSpaceInGB;|;$totalSpaceInGB;|;$physicalDisk;|;$deviceType"
    ((v++))
  done
  ((i++))
done

# Check if the journal log file exists and is not empty
if [[ -f "/tmp/syslog.log" && -s "/tmp/syslog.log" ]]; then
  # Count the number of lines in the journal log file
  journalLines=$(wc -l /tmp/syslog.log | grep -o "[0-9]*" | xargs)

  # If there are more than one line in the journal log, process the log entries
  if [[ "$journalLines" -ge 1 ]]; then
    echo "Formatting event log entries."

    # Use `awk` to process the journal log and extract relevant fields
    errorEvents=$(
      awk '
      BEGIN {
        # Define the header for the formatted output
        header = "Service;|;PID;|;Time Created;|;Message"
        print header
      }
      /^[0-9]/ {
        time = $2
        gsub(/\.[0-9]+/, "", time)

        if ( $1 !~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}/ ){
          next
        }

        # Convert the timestamp to epoch format
        dateCmd = "date -j -f \"%Y-%m-%d %H:%M:%S%z\" \"" $1 " " time "\" \"+%x %r\""
        dateCmd | getline date
        close(dateCmd)

        # Extract the service, PID, and message from the log entry
        if ($4 ~ /^.*\[[0-9]*\]:/) {
          split($4, servicePart, "[")
          service = servicePart[1]

          pid = substr(servicePart[2], 1, length(servicePart[2])-2)
          split($0, messagePart, "]: ")

          message = messagePart[2]
        }else{
          split($4, servicePart, ":")
          service = servicePart[1]
          pid = "N/A"

          split($0, messagePart, service": ")

          message = messagePart[2]
        }

        # Output the formatted data as a semicolon-separated string
        print service ";|;" pid ";|;" date ";|;" message
      }
    ' '/tmp/syslog.log'
    )
  fi
fi

echo ""
echo "Collecting system information."

# Retrieve the CPU model name
if ! CPU=$(sysctl -n machdep.cpu.brand_string); then
  echo "[Error] Failed to retrieve the cpu information." >&2
  exitCode=1
fi

# Retrieve the total memory installed on the system in GiB
if ! TotalMemory=$(sysctl -n hw.memsize | awk '{printf "%.2f GiB\n", $1/1024^3}'); then
  echo "[Error] Failed to retrieve the RAM information." >&2
  exitCode=1
fi

# Define the list of log files to clean up
logPaths=("/tmp/cpuData.log" "/tmp/cpuDataErr.log" "/tmp/diskData.log" "/tmp/diskDataErr.log" "/tmp/syslog.log" "syslogErr.log" "/tmp/memData.log" "/tmp/memDataErr.log" "/tmp/totalMemData.log" "/tmp/totalMemDataErr.log" "/tmp/netData.log" "/tmp/netDataErr.log")

# Iterate over the log files and remove each if it exists
for logFile in "${logPaths[@]}"; do
  if [[ -f "$logFile" ]]; then
    # Attempt to remove the log file and handle errors if it fails
    if ! rm "$logFile"; then
      echo "[Error] Failed to remove the log file at '$logFile'." >&2
      exitCode=1
    fi
  fi
done

# Display the CPU information and formatted total CPU usage data
echo ""
echo "### $CPU ###"
echo "$cpuTotalData" | column -t -s ";|;"

# Display the total memory installed and the formatted total memory usage data
echo ""
echo "### Memory Usage ###"
echo "Total Memory Installed: $TotalMemory"
echo "$memTotalData" | column -t -s ";|;"

# Display the top 5 CPU-consuming processes
echo ""
echo "### Top 5 CPU Processes ###"
echo "$cpuProcessData" | column -t -s ";|;"

# Display the top 5 RAM-consuming processes
echo ""
echo "### Top 5 RAM Processes ###"
echo "$memProcessData" | column -t -s ";|;"

# Display the formatted network usage data
echo ""
echo "### Network Usage ###"
echo "$netData" | column -t -s ";|;"

# Display the formatted disk usage data
echo ""
echo "### Disk Usage ###"
# Output disk data as a list instead of a table
echo "$diskData" | tail -n +2 | awk -F';[|];' '
{
  print "Volume: " $1
  print "Name: " $2
  print "Mount Point: " $3
  print "Consumed Space: " $4
  print "Container: " $5
  print "Free Space: " $6
  print "Total Space: " $7
  print "Physical Disk: " $8
  print "Media Type: " $9
  print ""
}'

# Check if a custom field has been provided
if [[ -n "$_arg_wysiwygCustomField" ]]; then
  echo "The custom field '$_arg_wysiwygCustomField' has been provided."

  # Convert CPU process data to HTML format
  echo "Converting CPU process data into HTML."
  cpuProcessMetricTable=$(echo "$cpuProcessData" | convertToHTMLTable -d ";[|];")
  cpuProcessMetricTable=${cpuProcessMetricTable//"<th>"/"<th><b>"}
  cpuProcessMetricTable="${cpuProcessMetricTable//<\/th>/</b></th>}"
  cpuProcessMetricTable="${cpuProcessMetricTable//<table>/<table><caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'><b>Top 5 CPU Processes</b></caption>}"
  cpuProcessMetricTable=${cpuProcessMetricTable//"Average CPU % Used"/"<i class='fa-solid fa-arrow-down-up-across-line'></i>&nbsp;&nbsp;Average CPU % Used"}
  cpuProcessMetricTable=${cpuProcessMetricTable//"Minimum CPU % Used"/"<i class='fa-solid fa-arrows-down-to-line'></i>&nbsp;&nbsp;Minimum CPU % Used"}
  cpuProcessMetricTable=${cpuProcessMetricTable//"Maximum CPU % Used"/"<i class='fa-solid fa-arrows-up-to-line'></i>&nbsp;&nbsp;Maximum CPU % Used"}

  # Apply thresholds for CPU metrics to highlight warnings and danger levels
  cpuProcessMetricTable=$(
    echo "$cpuProcessMetricTable" | awk '
      BEGIN {
        dangerThreshold = 50.00
        warningThreshold = 20.00
      }
      {
        if ($0 ~ /<tr><td>/) {
          split($0, tableData, "<td>")

          avg = tableData[4]
          min = tableData[5]
          max = tableData[6]

          gsub(/[^0-9.]/, "", avg)
          gsub(/[^0-9.]/, "", min)
          gsub(/[^0-9.]/, "", max)

          avg += 0
          min += 0
          max += 0

          # Highlight rows based on thresholds
          if (avg > dangerThreshold || min > dangerThreshold || max > dangerThreshold) {
            sub(/<tr>/, "<tr class='\''danger'\''>");
          } else if (avg > warningThreshold || min > warningThreshold || max > warningThreshold) {
            sub(/<tr>/, "<tr class='\''warning'\''>");
          }
        }

        print $0
      }
    '
  )

  # Convert RAM process data to HTML format
  echo "Converting RAM process data into HTML."
  ramProcessMetricTable=$(echo "$memProcessData" | convertToHTMLTable -d ";[|];")
  ramProcessMetricTable=${ramProcessMetricTable//"<th>"/"<th><b>"}
  ramProcessMetricTable="${ramProcessMetricTable//<\/th>/</b></th>}"
  ramProcessMetricTable="${ramProcessMetricTable//<table>/<table><caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'><b>Top 5 RAM Processes</b></caption>}"
  ramProcessMetricTable=${ramProcessMetricTable//"Average RAM % Used"/"<i class='fa-solid fa-arrow-down-up-across-line'></i>&nbsp;&nbsp;Average RAM % Used"}
  ramProcessMetricTable=${ramProcessMetricTable//"Minimum RAM % Used"/"<i class='fa-solid fa-arrows-down-to-line'></i>&nbsp;&nbsp;Minimum RAM % Used"}
  ramProcessMetricTable=${ramProcessMetricTable//"Maximum RAM % Used"/"<i class='fa-solid fa-arrows-up-to-line'></i>&nbsp;&nbsp;Maximum RAM % Used"}

  # Apply thresholds for RAM metrics to highlight warnings and danger levels
  ramProcessMetricTable=$(
    echo "$ramProcessMetricTable" | awk '
      BEGIN {
        dangerThreshold = 30.00
        warningThreshold = 10.00
      }
      {
        if ($0 ~ /<tr><td>/) {
          split($0, tableData, "<td>")

          avg = tableData[4]
          min = tableData[5]
          max = tableData[6]

          gsub(/[^0-9.]/, "", avg)
          gsub(/[^0-9.]/, "", min)
          gsub(/[^0-9.]/, "", max)

          avg += 0
          min += 0
          max += 0

          # Highlight rows based on thresholds
          if (avg > dangerThreshold || min > dangerThreshold || max > dangerThreshold) {
            sub(/<tr>/, "<tr class='\''danger'\''>");
          } else if (avg > warningThreshold || min > warningThreshold || max > warningThreshold) {
            sub(/<tr>/, "<tr class='\''warning'\''>");
          }
        }

        print $0
      }
    '
  )

  echo "Converting network usage data into HTML."

  # Apply formatting to the network usage table
  networkInterfaceUsage=$(echo "$netData" | convertToHTMLTable -d ";[|];")
  networkInterfaceUsage=${networkInterfaceUsage//"<th>"/"<th><b>"}
  networkInterfaceUsage="${networkInterfaceUsage//<\/th>/</b></th>}"
  networkInterfaceUsage="${networkInterfaceUsage//<table>/<table><caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'><b>Network Usage</b></caption>}"
  networkInterfaceUsage=${networkInterfaceUsage//"Average Sent & Received"/"<i class='fa-solid fa-arrow-down-up-across-line'></i>&nbsp;&nbsp;Average Sent & Received"}
  networkInterfaceUsage=${networkInterfaceUsage//"Minimum Sent & Received"/"<i class='fa-solid fa-arrows-down-to-line'></i>&nbsp;&nbsp;Minimum Sent & Received"}
  networkInterfaceUsage=${networkInterfaceUsage//"Maximum Sent & Received"/"<i class='fa-solid fa-arrows-up-to-line'></i>&nbsp;&nbsp;Maximum Sent & Received"}
  networkInterfaceUsage="${networkInterfaceUsage//<th><b>Type<\/b><\/th>/<th><b><i class='fa-solid fa-network-wired'></i>&nbsp;&nbsp;Type</b></th>}"
  networkInterfaceUsage="${networkInterfaceUsage//<td>Wired<\/td>/<td><i class='fa-solid fa-ethernet'></i>&nbsp;&nbsp;Wired</td>}"
  networkInterfaceUsage="${networkInterfaceUsage//<td>Wi-Fi<\/td>/<td><i class='fa-solid fa-wifi'></i>&nbsp;&nbsp;Wi-Fi</td>}"
  networkInterfaceUsage="${networkInterfaceUsage//<td>Other<\/td>/<td><i class='fa-solid fa-circle-question'></i>&nbsp;&nbsp;Other</td>}"

  # Apply threshold-based row highlighting for network usage data
  networkInterfaceUsage=$(
    echo "$networkInterfaceUsage" | awk '
      BEGIN {
        dangerThreshold = 100.00
        warningThreshold = 10.00
      }
      {
        if ($0 ~ /<tr><td>/) {
          split($0, tableData, "<td>")

          avg = tableData[5]
          min = tableData[6]
          max = tableData[7]
          networkType = tableData[4]

          gsub(/&nbsp;/, "", networkType)
          gsub(/<i.*><\/i>/, "", networkType)
          gsub(/<\/td>/, "", networkType)

          gsub(/[^0-9.]/, "", avg)
          gsub(/[^0-9.]/, "", min)
          gsub(/[^0-9.]/, "", max)

          avg += 0
          min += 0
          max += 0

          # Highlight rows based on thresholds
          if (avg > dangerThreshold || min > dangerThreshold || max > dangerThreshold) {
            sub(/<tr>/, "<tr class='\''danger'\''>");
          } else if (avg > warningThreshold || min > warningThreshold || max > warningThreshold || networkType != "Wired" ) {
            sub(/<tr>/, "<tr class='\''warning'\''>");
          }
        }

        print $0
      }
    '
  )

  echo "Converting disk usage data into HTML."
  diskTable=$(echo "$diskData" | convertToHTMLTable -d ";[|];")

  # Apply formatting to the disk usage table
  diskTable=${diskTable//"<th>"/"<th><b>"}
  diskTable="${diskTable//<\/th>/</b></th>}"
  diskTable="${diskTable//<table>/<table><caption style='border-top: 1px; border-left: 1px; border-right: 1px; border-style: solid; border-color: #CAD0D6'><b>Disk Usage</b></caption>}"

  # Apply threshold-based row highlighting for disk usage data
  diskTable=$(
    echo "$diskTable" | awk '
      BEGIN {
        dangerThreshold = 90.00
        warningThreshold = 75.00
      }
      {
        if ($0 ~ /<tr><td>/) {
          split($0, tableData, "<td>")

          consumedSpace = tableData[5]
          mediaType = tableData[10]

          gsub(/^.*\(/, "", consumedSpace)
          gsub(/\).*$/, "", consumedSpace)
          gsub(/%/, "", consumedSpace)
          consumedSpace += 0.00

          gsub(/<\/td>.*/, "", mediaType)

          # Highlight rows based on thresholds and media type
          if (consumedSpace > dangerThreshold || (mediaType != "SSD" && mediaType != "Unspecified" )) {
            sub(/<tr>/, "<tr class='\''danger'\''>");
          } else if (consumedSpace > warningThreshold) {
            sub(/<tr>/, "<tr class='\''warning'\''>");
          }
        }

        print $0
      }
    '
  )

  # Record the completion date and time
  reportCompleteDate=$(date "+%x %r")

  # Assemble the HTML card for the report
  echo "Assembling the html data into the html card"
  htmlCard="<div class='card flex-grow-1'>
  <div class='card-title-box'>
    <div class='card-title'><i class='fa-solid fa-gauge-high'></i>&nbsp;&nbsp;System Performance Metrics</div>
  </div>
  <div class='card-body' style='white-space: nowrap'>
    <div class='container' style='padding-left: 0px'>
      <div class='row'>
        <div class='col-sm'>
          <p class='card-text'><b>Start Date and Time</b><br>$reportStartDate</p>
        </div>
        <div class='col-sm'>
          <p class='card-text'><b>Completed Date and Time</b><br>$reportCompleteDate</p>
        </div>
      </div>
    </div>
    <p id='lastStartup' class='card-text'><b>Last Startup Time</b><br>$lastStartupDate</p>
    <p><b>$CPU</b></p>
    <div class='container'>
      <div class='row' style='padding-left: 0px'>
        <div class='col-md'>
          <div class='stat-card' style='display: flex;'>
            <div class='stat-value' id='cpuOverallAvg' style='color: #008001;'>$CPUAverage</div>
            <div class='stat-desc'><i class='fa-solid fa-arrow-down-up-across-line'></i>&nbsp;&nbsp;Average CPU % Used</div>
          </div>
        </div>
        <div class='col-md'>
          <div class='stat-card' style='display: flex;'>
            <div class='stat-value' id='cpuOverallMin' style='color: #008001;'>$CPUMinimum</div>
            <div class='stat-desc'><i class='fa-solid fa-arrows-down-to-line'></i>&nbsp;&nbsp;Minimum CPU % Used</div>
          </div>
        </div>
        <div class='col-md'>
          <div class='stat-card' style='display: flex;'>
            <div class='stat-value' id='cpuOverallMax' style='color: #008001;'>$CPUMaximum</div>
            <div class='stat-desc'><i class='fa-solid fa-arrows-up-to-line'></i>&nbsp;&nbsp;Maximum CPU % Used</div>
          </div>
        </div>
      </div>
    </div>
    <p><b>Total Memory: $TotalMemory</b></p>
    <div class='container'>
      <div class='row' style='padding-left: 0px'>
        <div class='col-md'>
          <div class='stat-card' style='display: flex;'>
            <div class='stat-value' id='ramOverallAvg' style='color: #008001;'>$MEMAverage</div>
            <div class='stat-desc'><i class='fa-solid fa-arrow-down-up-across-line'></i>&nbsp;&nbsp;Average RAM % Used</div>
          </div>
        </div>
        <div class='col-md'>
          <div class='stat-card' style='display: flex;'>
            <div class='stat-value' id='ramOverallMin' style='color: #008001;'>$MEMMinimum</div>
            <div class='stat-desc'><i class='fa-solid fa-arrows-down-to-line'></i>&nbsp;&nbsp;Minimum RAM % Used</div>
          </div>
        </div>
        <div class='col-md'>
          <div class='stat-card' style='display: flex;'>
            <div class='stat-value' id='ramOverallMax' style='color: #008001;'>$MEMMaximum</div>
            <div class='stat-desc'><i class='fa-solid fa-arrows-up-to-line'></i>&nbsp;&nbsp;Maximum RAM % Used</div>
          </div>
        </div>
      </div>
    </div>
    $cpuProcessMetricTable
    <br>
    $ramProcessMetricTable
    <br>
    $networkInterfaceUsage
    <br>
    $diskTable
    <br>
  </div>
</div>"
  if [[ "$_arg_daysSinceLastReboot" -ge 0 ]]; then
    echo "Highlighting last startup date."
  fi

  # Check if the last startup exceeded the specified limit and update the HTML card accordingly
  escapedLastStartupDate="${lastStartupDate//\//\\/}"
  if [[ "$exceededLastStartupLimit" == "true" ]]; then
    searchString="id='lastStartup' class='card-text'><b>Last Startup Time<\/b><br>$escapedLastStartupDate"
    replacementString="id='lastStartup' class='card-text'><b>Last Startup Time</b><br>$lastStartupDate&nbsp;&nbsp;<i class='fa-solid fa-circle-exclamation' style='color: #D53948;'></i>"

    htmlCard="${htmlCard//$searchString/$replacementString}"
  elif [[ "$_arg_daysSinceLastReboot" -ge 0 ]]; then
    searchString="id='lastStartup' class='card-text'><b>Last Startup Time<\/b><br>$escapedLastStartupDate"
    replacementString="id='lastStartup' class='card-text'><b>Last Startup Time</b><br>$lastStartupDate&nbsp;&nbsp;<i class='fa-solid fa-circle-check' style='color: #008001;'></i>"

    htmlCard="${htmlCard//$searchString/$replacementString}"
  fi

  # Highlight overall CPU usage metrics based on thresholds
  echo "Highlighting overall CPU usage metrics."
  CPUNumericAverage=$(echo "$CPUAverage" | awk '{ gsub(/[^0-9.]/, ""); printf "%.0f", $1 }')
  CPUNumericMaximum=$(echo "$CPUMaximum" | awk '{ gsub(/[^0-9.]/, ""); printf "%.0f", $1 }')
  CPUNumericMinimum=$(echo "$CPUMinimum" | awk '{ gsub(/[^0-9.]/, ""); printf "%.0f", $1 }')

  # Update HTML card with warning or danger thresholds for CPU usage
  if [[ "$CPUNumericAverage" -ge 60 && "$CPUNumericAverage" -lt 90 ]]; then
    htmlCard=${htmlCard//"id='cpuOverallAvg' style='color: #008001;'"/"id='cpuOverallAvg' style='color: #FAC905;'"}
  fi
  if [[ "$CPUNumericMaximum" -ge 60 && "$CPUNumericMaximum" -lt 90 ]]; then
    htmlCard=${htmlCard//"id='cpuOverallMax' style='color: #008001;'"/"id='cpuOverallMax' style='color: #FAC905;'"}
  fi
  if [[ "$CPUNumericMinimum" -ge 60 && "$CPUNumericMinimum" -lt 90 ]]; then
    htmlCard=${htmlCard//"id='cpuOverallMin' style='color: #008001;'"/"id='cpuOverallMin' style='color: #FAC905;'"}
  fi
  if [[ "$CPUNumericAverage" -ge 90 ]]; then
    htmlCard=${htmlCard//"id='cpuOverallAvg' style='color: #008001;'"/"id='cpuOverallAvg' style='color: #D53948;'"}
  fi
  if [[ "$CPUNumericMaximum" -ge 90 ]]; then
    htmlCard=${htmlCard//"id='cpuOverallMax' style='color: #008001;'"/"id='cpuOverallMax' style='color: #D53948;'"}
  fi
  if [[ "$CPUNumericMinimum" -ge 90 ]]; then
    htmlCard=${htmlCard//"id='cpuOverallMin' style='color: #008001;'"/"id='cpuOverallMin' style='color: #D53948;'"}
  fi

  # Highlight overall RAM usage metrics based on thresholds
  echo "Highlighting overall RAM usage metrics."
  MEMNumericAverage=$(echo "$MEMAverage" | awk '{ gsub(/[^0-9.]/, ""); printf "%.0f", $1 }')
  MEMNumericMaximum=$(echo "$MEMMaximum" | awk '{ gsub(/[^0-9.]/, ""); printf "%.0f", $1 }')
  MEMNumericMinimum=$(echo "$MEMMinimum" | awk '{ gsub(/[^0-9.]/, ""); printf "%.0f", $1 }')

  # Update HTML card with warning or danger thresholds for RAM usage
  if [[ "$MEMNumericAverage" -ge 60 && "$MEMNumericAverage" -lt 90 ]]; then
    htmlCard=${htmlCard//"id='ramOverallAvg' style='color: #008001;'"/"id='ramOverallAvg' style='color: #FAC905;'"}
  fi
  if [[ "$MEMNumericMaximum" -ge 60 && "$MEMNumericMaximum" -lt 90 ]]; then
    htmlCard=${htmlCard//"id='ramOverallMax' style='color: #008001;'"/"id='ramOverallMax' style='color: #FAC905;'"}
  fi
  if [[ "$MEMNumericMinimum" -ge 60 && "$MEMNumericMinimum" -lt 90 ]]; then
    htmlCard=${htmlCard//"id='ramOverallMin' style='color: #008001;'"/"id='ramOverallMin' style='color: #FAC905;'"}
  fi
  if [[ "$MEMNumericAverage" -ge 90 ]]; then
    htmlCard=${htmlCard//"id='ramOverallAvg' style='color: #008001;'"/"id='ramOverallAvg' style='color: #D53948;'"}
  fi
  if [[ "$MEMNumericMaximum" -ge 90 ]]; then
    htmlCard=${htmlCard//"id='ramOverallMax' style='color: #008001;'"/"id='ramOverallMax' style='color: #D53948;'"}
  fi
  if [[ "$MEMNumericMinimum" -ge 90 ]]; then
    htmlCard=${htmlCard//"id='ramOverallMin' style='color: #008001;'"/"id='ramOverallMin' style='color: #D53948;'"}
  fi

  # Store the assembled HTML card as the WYSIWYG value
  wysiwygValue="$htmlCard"

  # Check if event data needs to be included in the WYSIWYG field
  if [[ "$_arg_numberOfEvents" -gt 0 ]]; then
    echo "Converting system log data into an HTML format."
  fi

  # Convert system log error events to HTML format or handle missing events
  if [[ "$_arg_numberOfEvents" -gt 0 && -n "$errorEvents" ]]; then
    eventLogTableMetrics=$(echo "$errorEvents" | convertToHTMLTable -d ";[|];")
    eventLogTableMetrics=$(sed -e 's/<th>/<th><b>/g' \
      -e 's/<\/th>/<\/b><\/th>/g' \
      -e 's/<th><b>Service/<th style="width: 250px"><b>Service/g' \
      -e 's/<th><b>PID/<th style="width: 75px"><b>PID/g' \
      -e 's/<th><b>Time Created/<th style="width: 200px"><b>Time Created/g' \
      <<< "$eventLogTableMetrics")
  elif [[ "$_arg_numberOfEvents" -gt 0 ]]; then
    eventLogTableMetrics="<p style='margin-top: 0px'>No error events were found in the system log since the last boot.</p>"
  fi

  # Append event log data to the WYSIWYG value
  if [[ "$_arg_numberOfEvents" -gt 0 ]]; then
    eventLogCard="<div class='card flex-grow-1'>
    <div class='card-title-box'>
        <div class='card-title'><i class='fa-solid fa-book'></i>&nbsp;&nbsp;Recent Error Events</div>
    </div>
    <div class='card-body' style='white-space: nowrap'>
        $eventLogTableMetrics
    </div>
</div>"
    wysiwygValue+="$eventLogCard"

    # Check and handle the 40,000-character limit for the WYSIWYG field
    characterCount=${#wysiwygValue}
    if [[ "$characterCount" -gt 40000 ]]; then
      echo "The current character count is '$characterCount'."
      echo "[Warning] The 40,000-character limit has been reached! Trimming output to fit within the allowed limit..."
    fi

    while [[ "$characterCount" -gt 40000 ]]; do
      totalLines=$(printf "%s" "$eventLogTableMetrics" | wc -l | xargs)

      exceededAmount=$((characterCount - 40000))
      if [[ "$exceededAmount" -lt 500 ]]; then
        batchSize=3
      else
        batchSize=$(((exceededAmount + 499) / 500))
        [[ $batchSize -lt 3 ]] && batchSize=3
      fi

      # Keep only the remaining lines within the limit
      linesToKeep=$((totalLines - batchSize))
      eventLogTableMetrics=$(echo "$eventLogTableMetrics" | head -n "$linesToKeep")
      eventLogTableMetrics+=$'\n'"</tbody>"
      eventLogTableMetrics+=$'\n'"</table>"

      wysiwygValue="$htmlCard"

      eventLogCard="<div class='card flex-grow-1'>
    <div class='card-title-box'>
        <div class='card-title'><i class='fa-solid fa-book'></i>&nbsp;&nbsp;Recent Error Events</div>
    </div>
    <div class='card-body' style='white-space: nowrap'>
        $eventLogTableMetrics
    </div>
</div>"
      wysiwygValue+="<h1>This info has been truncated to accommodate the 40,000 character limit.</h1>"
      wysiwygValue+="$eventLogCard"

      characterCount=${#wysiwygValue}
    done
  fi

  # Attempt to set the custom field using ninjarmm-cli
  echo ""
  echo "Attempting to set the custom field '$_arg_wysiwygCustomField'."
  # Try to set the multiline custom field using ninjarmm-cli and capture the output
  if ! output=$("$ninjarmmcliPath" set "$_arg_wysiwygCustomField" "$wysiwygValue" 2>&1); then
    echo "[Error] $output" >&2
    exitCode=1
  else
    echo "Successfully set the custom field '$_arg_wysiwygCustomField'."
  fi
fi

# Display the last number of events in the system log
if [[ -n "$_arg_numberOfEvents" ]]; then
  echo ""
  echo "### Last $_arg_numberOfEvents warnings and errors in the system log. ###"

  if [[ -n "$errorEvents" ]]; then
    echo "$errorEvents" | sed '1d' | awk -F ";[|];" '{
      print "Service: " $1
      print "PID: " $2
      print "Time Created: " $3
      print "Message: " $4
      print ""
    }'
  else
    echo "No errors or warnings were found in the system log. Ensure the system log service is enabled and configured with an adequate file size limit."
  fi
fi

# Attempt to remove the lock file
if [[ -f "$lockFile" ]]; then
  if ! rm "$lockFile"; then
    echo "[Error] Failed to remove the lock file at '$lockFile'. This file prevents multiple instances of the performance script from running simultaneously." >&2
    exitCode=1
  fi
fi

exit "$exitCode"

 

Analisi dettagliata dello script per monitorare i dati sulle prestazioni in macOS

Questo script shell per monitorare i dati sulle prestazioni in macOS è un’utility di diagnostica completa progettata per macOS Ventura e versioni successive. Esegue le seguenti operazioni di alto livello:

  1. Convalida dell’ingresso e parsing degli argomenti
    Accetta argomenti come –daysSinceLastReboot, –durationToPerformTests, –numberOfEvents e –wysiwygCustomFieldName. Questi controllano l’estensione e la profondità dell’esame diagnostico.
  2. Controlli di integrità per le startup
    Controlla che ci siano i permessi di root, impedisce l’esecuzione simultanea di script (usando un lockfile) e convalida il percorso dello strumento ninjarmm-cli di NinjaOne se è necessario l’aggiornamento del campo WYSIWYG.
  3. Fase di raccolta dati
    • Metriche della CPU: Utilizza top per controllare l’utilizzo della CPU a livello di processo e a livello di sistema.
    • Metriche di memoria: Utilizza memory_pressure e top per determinare l’uso della RAM per ogni processo.
    • Metriche del disco: Raccoglie le statistiche a livello di volume con diskutil apfs list -plist.
    • Metriche di rete: Utilizza nettop e ifconfig per analizzare il traffico a livello di scheda di rete.
    • Log eventi: Estrae i messaggi di errore recenti dal sistema di logging unificato di macOS utilizzando log show.
  4. Aggregazione e formattazione dei dati
    Dopo aver raccolto i log grezzi, lo script elabora i dati in tabelle strutturate. Per esempio:

    • I 5 principali processi che consumano CPU e RAM (media/min/max)
    • Utilizzo del disco tra i volumi
    • Throughput di rete per interfaccia
    • Un riepilogo delle informazioni sul sistema (modello di CPU, memoria installata ecc.)
  5. Aggiornamento opzionale dei campi WYSIWYG personalizzati
    Se viene specificato un campo WYSIWYG, lo script crea una scheda HTML già formattata correttamente utilizzando icone Font Awesome e indicatori di colore basati sulla soglia per evidenziare le metriche anomale.
  6. Pulizia e output finale
    Tutti i log temporanei vengono cancellati e i risultati delle prestazioni vengono stampati sul terminale. Se configurato, i risultati vengono inviati a NinjaOne.

🖼️ Aiuto visivo: Un diagramma di flusso che mostra le fasi dello script: validazione → raccolta dei dati → formattazione → caricamento opzionale.

Casi d’uso potenziali

Immagina un MSP che gestisce un gran numero di MacBook per dirigenti d’azienda. Un utente segnala che il suo computer è lento, ma i problemi intermittenti sono difficili da individuare in tempo reale. Con questo script per monitorare i dati sulle prestazioni in macOS:

  • L’MSP lo gestisce in remoto tramite NinjaOne.
  • Riceve un rapporto WYSIWYG che mostra un utilizzo del 99% della RAM a causa di un’applicazione che non si comporta come dovrebbe.
  • Un tecnico risolve rapidamente il problema e documenta la risoluzione, il tutto senza dover condividere lo schermo o trasferire file di log.

Questo migliora la conformità agli SLA, riduce l’MTTR (tempo medio di risoluzione) e migliora la soddisfazione degli utenti.

Confronti

Gli approcci tradizionali prevedono:

  • Avvio manuale di Monitoraggio attività o della Console.
  • Utilizzo di applicazioni di terze parti come iStat Menus o CleanMyMac.
  • Distribuzione di agenti di monitoraggio enterprise (con costi di licenza e prestazioni).

Rispetto a questi metodi, lo script per monitorare i dati sulle prestazioni in macOS qui analizzato:

  • Non richiede alcuna interazione con l’interfaccia grafica.
  • Funziona in modo nativo su macOS.
  • Si integra perfettamente con il motore di scripting e i campi personalizzati di NinjaOne.

È leggero, ripetibile e completamente verificabile.

Domande frequenti

D: Questo script per monitorare i dati sulle prestazioni in macOS supporta i Mac basati su Intel?

R: Sì, anche se le prestazioni possono variare. È stato testato su Apple Silicon e si aspetta gli output di sysctl e top di conseguenza.

D: Posso raccogliere più di 5 eventi di errore?

R: Sì, modificando l’argomento –numberOfEvents. Lo script per monitorare i dati sulle prestazioni in macOS supporta fino a 10.000 eventi, ma tronca l’output HTML se supera i 40.000 caratteri.

D: Questo script rallenterà la mia macchina?

R: Lo script per monitorare i dati sulle prestazioni in macOS utilizza metodi di verifiche a campione leggeri e viene eseguito per un periodo definito (per esempio 5 minuti), riducendo al minimo l’impatto sui sistemi di produzione.

D: Cosa succede se il campo WYSIWYG non è impostato?

R: Lo script per monitorare i dati sulle prestazioni in macOS stampa comunque i risultati sul terminale per la revisione manuale.

Implicazioni

I problemi di prestazioni del sistema, soprattutto su endpoint macOS, spesso non vengono diagnosticati finché gli utenti non si lamentano. Questo script per monitorare i dati sulle prestazioni in macOS consente ai team IT di:

  • Rilevare e agire preventivamente in caso di utilizzo elevato delle risorse.
  • Documentare l’integrità del sistema nel tempo per eventuali revisioni forensi.
  • Identificare il software che riduce le prestazioni (per esempio i servizi in background o le schede del browser che consumano troppa RAM).

Dal punto di vista della sicurezza, segnala anche le anomalie nel traffico di rete o i log di servizi inaspettati.

Raccomandazioni

  • Pianifica esecuzioni regolari: Distribuisci questo script per monitorare i dati sulle prestazioni in macOS con frequenza settimanale o dopo cicli di patch.
  • Utilizza l’automazione dei campi personalizzati: Utilizza lo script per monitorare i dati sulle prestazioni in macOS insieme ai flussi di lavoro di NinjaOne per interventi basati su soglie di utilizzo CPU o RAM.
  • Regola attentamente le soglie di utilizzo: Regola i livelli di avviso/pericolo in base a quella che è la norma dell’ambiente.
  • Tendenze a lungo termine: Esporta i risultati in una dashboard centrale per l’analisi delle tendenze.

Considerazioni finali

Per i team IT che gestiscono dispositivi macOS su vasta scala, la visibilità è fondamentale. Questo script shell per monitorare i dati sulle prestazioni in macOS colma il divario tra la diagnostica del sistema e le informazioni utili, fornendo istantanee delle prestazioni direttamente nei campi personalizzati di NinjaOne. Questo esemplifica come l’automazione e l’integrazione possano snellire i flussi di lavoro IT, migliorare l’esperienza degli utenti finali e mantenere l’integrità del sistema.

Se utilizzi NinjaOne e non hai ancora sperimentato la potenza degli script shell sugli endpoint macOS, questo script può essere un buon punto di partenza.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service delivery tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categorie:

Ti potrebbe interessare anche