#!/bin/bash ############################################################# #### Speedtest Metrics Exporter #### #### Internet & LAN speed metrics for Prometheus #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version: 2.1 #### #### #### #### Usage: ./speedtest-metrics.sh [OPTIONS] #### ############################################################# set -euo pipefail ######################### ### Output Mode ### ######################### LISTEN_PORT="${SPEEDTEST_EXPORTER_PORT:-9196}" TEXTFILE_DIR="/var/lib/node_exporter" OUTPUT_FILE="" HTTP_MODE=false ######################### ### Parse Arguments ### ######################### show_help() { cat <&2 show_help >&2 exit 1 ;; esac done } parse_args "$@" ######################### ### Metrics Collection ### ######################### collect_metrics() { # Configuration TEMP_FILE="/tmp/speedtest_$$" IPERF_SERVER="${IPERF_SERVER:-192.168.1.100}" # Set to your local iperf3 server IP IPERF_PORT="${IPERF_PORT:-9182}" # iperf3 port # Multiple speedtest servers - add/remove server IDs as needed # Common server IDs for major cities: # Dallas/DFW: 5029 (AT&T), 12190 (Spectrum), 26847 (Verizon) # New York: 3737 (Verizon), 11570 (Optimum), 17395 (Spectrum) SPEEDTEST_SERVERS="${SPEEDTEST_SERVERS:-auto}" # Comma-separated server IDs or "auto" cleanup() { rm -f "$TEMP_FILE" } trap cleanup EXIT # Record script start time SCRIPT_START_TIME=$(date +%s.%N) # Internet Speed Test - Multiple Servers echo "# Running internet speedtest on multiple servers..." >&2 # Initialize arrays to store results for all servers declare -a SERVER_IDS=() declare -a PING_LATENCIES=() declare -a PING_JITTERS=() declare -a PING_LOWS=() declare -a PING_HIGHS=() declare -a DOWNLOAD_MBPS=() declare -a UPLOAD_MBPS=() declare -a PACKET_LOSSES=() declare -a EXTERNAL_IPS=() declare -a TEST_TIMESTAMPS=() declare -a SERVER_NAMES=() declare -a SERVER_LOCATIONS=() declare -a SERVER_COUNTRIES=() declare -a ISPS=() declare -a RESULT_URLS=() declare -a DOWNLOAD_SIZES=() declare -a UPLOAD_SIZES=() declare -a SUCCESSES=() # Convert comma-separated servers to array IFS=',' read -ra SERVERS <<< "$SPEEDTEST_SERVERS" # Test each server for server_id in "${SERVERS[@]}"; do server_id=$(echo "$server_id" | xargs) # Trim whitespace echo "# Testing server $server_id..." >&2 TEMP_SERVER_FILE="/tmp/speedtest_${server_id}_$$" # Handle auto server selection vs specific server ID if [[ "$server_id" == "auto" ]]; then speedtest_cmd="speedtest --format=json" else speedtest_cmd="speedtest -s $server_id --format=json" fi if $speedtest_cmd --accept-license --accept-gdpr > "$TEMP_SERVER_FILE" 2>/dev/null; then echo "# Server $server_id: SUCCESS" >&2 # Parse results for this server ping_latency=$(jq -r '.ping.latency // "0"' "$TEMP_SERVER_FILE") ping_jitter=$(jq -r '.ping.jitter // "0"' "$TEMP_SERVER_FILE") ping_low=$(jq -r '.ping.low // "0"' "$TEMP_SERVER_FILE") ping_high=$(jq -r '.ping.high // "0"' "$TEMP_SERVER_FILE") download_bandwidth=$(jq -r '.download.bandwidth // "0"' "$TEMP_SERVER_FILE") upload_bandwidth=$(jq -r '.upload.bandwidth // "0"' "$TEMP_SERVER_FILE") packet_loss=$(jq -r '.packetLoss // "0"' "$TEMP_SERVER_FILE") external_ip=$(jq -r '.interface.externalIp // "unknown"' "$TEMP_SERVER_FILE") # Handle timestamp conversion test_timestamp_raw=$(jq -r '.timestamp // "0"' "$TEMP_SERVER_FILE") if [[ "$test_timestamp_raw" != "0" ]] && [[ "$test_timestamp_raw" != "unknown" ]]; then test_timestamp=$(date -d "$test_timestamp_raw" +%s 2>/dev/null || echo "0") else test_timestamp=0 fi server_name=$(jq -r '.server.name // "unknown"' "$TEMP_SERVER_FILE") server_location=$(jq -r '.server.location // "unknown"' "$TEMP_SERVER_FILE") server_country=$(jq -r '.server.country // "unknown"' "$TEMP_SERVER_FILE") isp=$(jq -r '.isp // "unknown"' "$TEMP_SERVER_FILE") result_url=$(jq -r '.result.url // "unknown"' "$TEMP_SERVER_FILE") download_size=$(jq -r '.download.bytes // "0"' "$TEMP_SERVER_FILE") upload_size=$(jq -r '.upload.bytes // "0"' "$TEMP_SERVER_FILE") # Convert from bits to Mbps (fallback to awk if bc unavailable) download_mbps=$(echo "scale=2; $download_bandwidth / 125000" | bc -l 2>/dev/null || echo "$download_bandwidth" | awk '{printf "%.2f", $1/125000}') upload_mbps=$(echo "scale=2; $upload_bandwidth / 125000" | bc -l 2>/dev/null || echo "$upload_bandwidth" | awk '{printf "%.2f", $1/125000}') success=1 else echo "# Server $server_id: FAILED" >&2 # Set default values for failed test ping_latency=0; ping_jitter=0; ping_low=0; ping_high=0 download_mbps=0; upload_mbps=0; packet_loss=0 external_ip="unknown"; test_timestamp=0; server_name="unknown" server_location="unknown"; server_country="unknown"; isp="unknown" result_url="unknown"; download_size=0; upload_size=0 success=0 fi # Store results in arrays SERVER_IDS+=("$server_id") PING_LATENCIES+=("$ping_latency") PING_JITTERS+=("$ping_jitter") PING_LOWS+=("$ping_low") PING_HIGHS+=("$ping_high") DOWNLOAD_MBPS+=("$download_mbps") UPLOAD_MBPS+=("$upload_mbps") PACKET_LOSSES+=("$packet_loss") EXTERNAL_IPS+=("$external_ip") TEST_TIMESTAMPS+=("$test_timestamp") SERVER_NAMES+=("$server_name") SERVER_LOCATIONS+=("$server_location") SERVER_COUNTRIES+=("$server_country") ISPS+=("$isp") RESULT_URLS+=("$result_url") DOWNLOAD_SIZES+=("$download_size") UPLOAD_SIZES+=("$upload_size") SUCCESSES+=("$success") # Cleanup temp file rm -f "$TEMP_SERVER_FILE" done # Local Network Speed Test (iperf3) - Enhanced with additional metrics echo "# Testing local network speed..." >&2 if command -v iperf3 >/dev/null 2>&1; then # Test download from local server (we are client) if local_down=$(timeout 10 iperf3 -c "$IPERF_SERVER" -p "$IPERF_PORT" -t 5 -J 2>/dev/null); then local_download_mbps=$(echo "$local_down" | jq -r '.end.sum_received.bits_per_second // "0"' | awk '{printf "%.2f", $1/1000000}') local_download_bytes=$(echo "$local_down" | jq -r '.end.sum_received.bytes // "0"') local_download_retransmits=$(echo "$local_down" | jq -r '.end.sum_sent.retransmits // "0"') local_download_rtt=$(echo "$local_down" | jq -r '.end.streams[0].sender.mean_rtt // "0"' | awk '{printf "%.3f", $1/1000}') # Convert to ms local_download_rtt_var=$(echo "$local_down" | jq -r '.end.streams[0].sender.rtt_variance // "0"' | awk '{printf "%.3f", $1/1000}') local_download_cpu_local=$(echo "$local_down" | jq -r '.end.cpu_utilization_percent.host_total // "0"') local_download_cpu_remote=$(echo "$local_down" | jq -r '.end.cpu_utilization_percent.remote_total // "0"') local_download_congestion_window=$(echo "$local_down" | jq -r '.end.streams[0].sender.max_snd_cwnd // "0"') local_download_success=1 else local_download_mbps=0; local_download_bytes=0; local_download_retransmits=0 local_download_rtt=0; local_download_rtt_var=0; local_download_cpu_local=0 local_download_cpu_remote=0; local_download_congestion_window=0; local_download_success=0 fi # Test upload to local server (we are client, reverse mode) if local_up=$(timeout 10 iperf3 -c "$IPERF_SERVER" -p "$IPERF_PORT" -t 5 -R -J 2>/dev/null); then local_upload_mbps=$(echo "$local_up" | jq -r '.end.sum_sent.bits_per_second // "0"' | awk '{printf "%.2f", $1/1000000}') local_upload_bytes=$(echo "$local_up" | jq -r '.end.sum_sent.bytes // "0"') local_upload_retransmits=$(echo "$local_up" | jq -r '.end.sum_received.retransmits // "0"') local_upload_rtt=$(echo "$local_up" | jq -r '.end.streams[0].receiver.mean_rtt // "0"' | awk '{printf "%.3f", $1/1000}') local_upload_rtt_var=$(echo "$local_up" | jq -r '.end.streams[0].receiver.rtt_variance // "0"' | awk '{printf "%.3f", $1/1000}') local_upload_cpu_local=$(echo "$local_up" | jq -r '.end.cpu_utilization_percent.host_total // "0"') local_upload_cpu_remote=$(echo "$local_up" | jq -r '.end.cpu_utilization_percent.remote_total // "0"') local_upload_congestion_window=$(echo "$local_up" | jq -r '.end.streams[0].receiver.max_snd_cwnd // "0"') local_upload_success=1 else local_upload_mbps=0; local_upload_bytes=0; local_upload_retransmits=0 local_upload_rtt=0; local_upload_rtt_var=0; local_upload_cpu_local=0 local_upload_cpu_remote=0; local_upload_congestion_window=0; local_upload_success=0 fi else echo "# iperf3 not installed, skipping local network test" >&2 local_download_mbps=0; local_upload_mbps=0; local_download_bytes=0; local_upload_bytes=0 local_download_retransmits=0; local_upload_retransmits=0; local_download_rtt=0; local_upload_rtt=0 local_download_rtt_var=0; local_upload_rtt_var=0; local_download_cpu_local=0; local_upload_cpu_local=0 local_download_cpu_remote=0; local_upload_cpu_remote=0; local_download_congestion_window=0; local_upload_congestion_window=0 local_download_success=0; local_upload_success=0 fi # Calculate script runtime SCRIPT_END_TIME=$(date +%s.%N) SCRIPT_RUNTIME=$(echo "$SCRIPT_END_TIME - $SCRIPT_START_TIME" | bc -l 2>/dev/null || echo "$SCRIPT_END_TIME $SCRIPT_START_TIME" | awk '{printf "%.3f", $1-$2}') # Output Prometheus metrics cat < "$tmp_file" mv "$tmp_file" "$OUTPUT_FILE" echo "Metrics written to $OUTPUT_FILE" >&2 else echo "$metrics" fi } start_server() { if ! command -v socat >/dev/null 2>&1; then echo "socat is required for HTTP mode. Install it first." >&2 exit 1 fi echo "Starting Speedtest Metrics Exporter on port $LISTEN_PORT" >&2 echo "Metrics available at http://localhost:$LISTEN_PORT/metrics" >&2 while true; do socat TCP-LISTEN:"$LISTEN_PORT",reuseaddr,fork EXEC:"$0 --handle-request" 2>/dev/null || { echo "Server error, restarting in 5 seconds..." >&2 sleep 5 } done } # Main execution if [[ "$HTTP_MODE" == true ]]; then start_server elif [[ -n "$OUTPUT_FILE" ]]; then write_output else collect_metrics fi