#!/bin/bash
################################################################################
# Script Name: rsyslog-metrics-exporter.sh
# Version: 1.0
# Description: Prometheus exporter for rsyslog internal metrics via impstats
#              JSON output. Exports queue depths, action success/failure,
#              input message counts, process resource usage, and overall
#              rsyslog health metrics.
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
#
# Prerequisites:
#   - rsyslog with impstats module enabled (JSON output to file)
#   - jq (JSON parser)
#   - netcat (nc) for HTTP mode
#   - Standard Unix tools (awk, grep, tail)
#
# Performance:
#   The stats file is read once per collection cycle — the last occurrence
#   of each named object is extracted in a single pass using jq.
#   Typical run time: under one second.
#
# Usage:
#   # One-shot output to stdout
#   ./rsyslog-metrics-exporter.sh --once
#
#   # Textfile collector mode (daemon, writes every COLLECTION_INTERVAL)
#   ./rsyslog-metrics-exporter.sh --daemon --textfile
#
#   # HTTP server mode
#   ./rsyslog-metrics-exporter.sh --http -p 9199
#
# Metrics Exported:
#   Core Status:
#     - rsyslog_up - rsyslog process status (1=running, 0=down)
#     - rsyslog_info{version} - rsyslog version info
#
#   Queue Metrics:
#     - rsyslog_messages_total - Total messages processed
#     - rsyslog_queue_size{queue} - Current queue depth
#     - rsyslog_queue_enqueued_total{queue} - Messages enqueued
#     - rsyslog_queue_dequeued_total{queue} - Messages dequeued
#     - rsyslog_queue_full_total{queue} - Times queue was full
#     - rsyslog_queue_max_size{queue} - Configured max queue size
#     - rsyslog_queue_disk_usage_bytes{queue} - Disk-assisted queue usage
#
#   Action Metrics:
#     - rsyslog_action_processed_total{action} - Messages processed per action
#     - rsyslog_action_failed_total{action} - Failed action attempts
#     - rsyslog_action_suspended{action} - 1 if action is suspended
#     - rsyslog_action_resumed_total{action} - Times action resumed
#
#   Input Metrics:
#     - rsyslog_input_received_total{input} - Messages received per input
#
#   Process Metrics:
#     - rsyslog_process_memory_bytes - RSS memory of rsyslog
#     - rsyslog_process_open_fds - Open file descriptors
#
#   Exporter Health:
#     - rsyslog_exporter_duration_seconds - Script execution time
#     - rsyslog_exporter_last_run_timestamp - Last run timestamp
#     - rsyslog_exporter_success - 1 if collection succeeded
#
# Configuration (environment variables):
#   NODE_DIR             - Textfile collector directory (default: /var/lib/node_exporter)
#   STATS_FILE           - impstats JSON output file (default: /var/log/rsyslog-stats.log)
#   COLLECTION_INTERVAL  - Seconds between collections in daemon mode (default: 60)
#   HTTP_PORT            - HTTP server port (default: 9199)
#   DEBUG                - Set to any value to enable debug output
#
# impstats Configuration:
#   Add to /etc/rsyslog.d/impstats.conf:
#     module(load="impstats"
#       interval="60"
#       severity="7"
#       log.syslog="off"
#       log.file="/var/log/rsyslog-stats.log"
#       format="json"
#     )
#
################################################################################

set -euo pipefail

# ============================================================================
# CONFIGURATION VARIABLES
# ============================================================================

NODE_DIR="${NODE_DIR:-/var/lib/node_exporter}"
STATS_FILE="${STATS_FILE:-/var/log/rsyslog-stats.log}"
COLLECTION_INTERVAL="${COLLECTION_INTERVAL:-60}"
HTTP_PORT="${HTTP_PORT:-9199}"
DEBUG="${DEBUG:-}"

OUTPUT_FILE=""
HTTP_MODE=false
DAEMON_MODE=false
ONCE_MODE=false
TEMP_FILE=""

# ============================================================================
# HELPER FUNCTIONS
# ============================================================================

show_usage() {
    cat <<EOF
Usage: $0 [OPTIONS]

Export rsyslog impstats metrics as Prometheus metrics (v1.0).

MODES:
    --once          Run once and output to stdout (default)
    --textfile      Write to node_exporter textfile collector
    --daemon        Run continuously (use with --textfile)
    --http          Run HTTP server on port $HTTP_PORT

OPTIONS:
    -p, --port      HTTP port (default: $HTTP_PORT)
    -o, --output    Output file path
    --check         Verify impstats is configured and show setup instructions
    -h, --help      Show this help message

ENVIRONMENT VARIABLES:
    NODE_DIR             Textfile collector dir (default: /var/lib/node_exporter)
    STATS_FILE           impstats output file (default: /var/log/rsyslog-stats.log)
    COLLECTION_INTERVAL  Daemon interval in seconds (default: 60)
    HTTP_PORT            HTTP server port (default: 9199)
    DEBUG                Enable debug output (unset by default)

EXAMPLES:
    $0 --once                        # One-shot output to stdout
    $0 --textfile                    # Write to textfile collector (once)
    $0 --daemon --textfile           # Continuous textfile collector mode
    $0 --http --port 9199            # Run HTTP server
    $0 --check                       # Verify impstats configuration
    $0 -o /tmp/rsyslog.prom          # Write to custom file

METRICS:
    - Queue depths, enqueue/dequeue counters, full events
    - Per-action processed/failed/suspended counts
    - Per-input received message counts
    - rsyslog process memory and file descriptor usage
    - Total messages processed

EOF
    exit 0
}

parse_args() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help) show_usage ;;
            --once) ONCE_MODE=true; shift ;;
            --textfile) OUTPUT_FILE="$NODE_DIR/rsyslog_metrics.prom"; shift ;;
            --daemon) DAEMON_MODE=true; shift ;;
            --http) HTTP_MODE=true; shift ;;
            --check) check_impstats_config; exit 0 ;;
            -p|--port) HTTP_PORT="$2"; shift 2 ;;
            -o|--output) OUTPUT_FILE="$2"; shift 2 ;;
            *) echo "Unknown option: $1" >&2; exit 1 ;;
        esac
    done
}

debug_log() {
    [ -n "$DEBUG" ] && echo "DEBUG: $*" >&2
}

# Verify that impstats is configured and the stats file exists
# Shows suggested configuration if not found
check_impstats_config() {
    echo "Checking rsyslog impstats configuration..." >&2
    echo "" >&2

    # Check rsyslog is installed
    if ! command -v rsyslogd >/dev/null 2>&1; then
        echo "ERROR: rsyslogd not found in PATH" >&2
        return 1
    fi

    local version
    version=$(rsyslogd -v 2>/dev/null | head -1 || echo "unknown")
    echo "rsyslog: $version" >&2

    # Check if rsyslog is running
    if pidof rsyslogd >/dev/null 2>&1; then
        echo "Status: running (PID $(pidof rsyslogd))" >&2
    else
        echo "Status: NOT running" >&2
    fi

    # Check stats file
    if [ -f "$STATS_FILE" ]; then
        echo "Stats file: $STATS_FILE (exists, $(wc -l < "$STATS_FILE") lines)" >&2
        echo "" >&2

        # Verify it contains JSON
        if tail -1 "$STATS_FILE" 2>/dev/null | jq . >/dev/null 2>&1; then
            echo "OK: Stats file contains valid JSON" >&2
        else
            echo "WARNING: Stats file does not contain valid JSON" >&2
            echo "  Ensure impstats is configured with format=\"json\"" >&2
        fi
    else
        echo "Stats file: $STATS_FILE (NOT FOUND)" >&2
        echo "" >&2
        echo "impstats does not appear to be configured." >&2
        echo "Add the following to /etc/rsyslog.d/impstats.conf:" >&2
        echo "" >&2
        cat <<CONF
module(load="impstats"
  interval="60"
  severity="7"
  log.syslog="off"
  log.file="$STATS_FILE"
  format="json"
)
CONF
        echo "" >&2
        echo "Then restart rsyslog: systemctl restart rsyslog" >&2
    fi

    # Check jq
    if command -v jq >/dev/null 2>&1; then
        echo "jq: installed ($(jq --version 2>/dev/null))" >&2
    else
        echo "jq: NOT FOUND (required)" >&2
        echo "  Install with: apt install jq  OR  yum install jq" >&2
    fi

    # Check node_exporter textfile dir
    if [ -d "$NODE_DIR" ]; then
        echo "Textfile dir: $NODE_DIR (exists)" >&2
    else
        echo "Textfile dir: $NODE_DIR (not found, create for --textfile mode)" >&2
    fi
}

# Clean up temp files on exit
cleanup() {
    [ -n "$TEMP_FILE" ] && rm -f "$TEMP_FILE"
    TEMP_FILE=""
}

# ============================================================================
# DATA COLLECTION FUNCTIONS
# ============================================================================

# Check if rsyslog is running
# Returns: 0 if running, 1 if not
is_rsyslog_running() {
    pidof rsyslogd >/dev/null 2>&1
}

# Get rsyslog version string
# Returns: version string (e.g., "8.2312.0")
get_rsyslog_version() {
    local version_line
    version_line=$(rsyslogd -v 2>/dev/null | head -1)
    # Extract version number from lines like "rsyslogd  8.2312.0 (aka 2023.12)"
    echo "$version_line" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
}

# Extract the latest stats for each unique object name from the impstats file
# The stats file grows over time; we only want the most recent entry per object.
# Args: $1 - origin filter (e.g., "core.queue", "core.action")
# Returns: JSON lines, one per unique object name (latest occurrence wins)
get_latest_stats() {
    local origin_filter="$1"

    if [ ! -f "$STATS_FILE" ] || [ ! -r "$STATS_FILE" ]; then
        debug_log "Stats file not readable: $STATS_FILE"
        return
    fi

    # Read the last 500 lines (covers several collection cycles) and
    # extract the last occurrence of each named object with the given origin
    tail -500 "$STATS_FILE" 2>/dev/null | \
        jq -c --arg origin "$origin_filter" \
        'select(.origin == $origin)' 2>/dev/null | \
        jq -s -c 'group_by(.name) | map(last) | .[]' 2>/dev/null
}

# Get the total messages processed from the main queue stats
# Returns: total enqueued count from "main Q"
get_total_messages() {
    if [ ! -f "$STATS_FILE" ] || [ ! -r "$STATS_FILE" ]; then
        echo "0"
        return
    fi

    local val
    val=$(tail -500 "$STATS_FILE" 2>/dev/null | \
        jq -c 'select(.origin == "core.queue" and .name == "main Q")' 2>/dev/null | \
        tail -1 | jq -r '.enqueued // 0' 2>/dev/null)
    echo "${val:-0}"
}

# Get rsyslog process RSS memory in bytes from /proc
# Returns: RSS in bytes, or 0 if unavailable
get_process_memory() {
    local pid
    pid=$(pidof rsyslogd 2>/dev/null) || { echo "0"; return; }

    # Use the first PID if multiple
    pid=${pid%% *}

    if [ -f "/proc/$pid/status" ]; then
        local vmrss_kb
        vmrss_kb=$(awk '/^VmRSS:/ {print $2}' "/proc/$pid/status" 2>/dev/null)
        if [ -n "$vmrss_kb" ]; then
            echo $((vmrss_kb * 1024))
            return
        fi
    fi

    echo "0"
}

# Get rsyslog open file descriptor count from /proc
# Returns: number of open fds, or 0 if unavailable
get_open_fds() {
    local pid
    pid=$(pidof rsyslogd 2>/dev/null) || { echo "0"; return; }

    # Use the first PID if multiple
    pid=${pid%% *}

    if [ -d "/proc/$pid/fd" ]; then
        ls "/proc/$pid/fd" 2>/dev/null | wc -l
        return
    fi

    echo "0"
}

# ============================================================================
# METRIC GENERATION
# ============================================================================

# Generate all Prometheus metrics
# Returns: Prometheus text format metrics on stdout
generate_metrics() {
    local script_start
    script_start=$(date +%s.%N 2>/dev/null || date +%s)
    local success=1

    # Verify jq is available
    if ! command -v jq >/dev/null 2>&1; then
        cat <<EOF
# HELP rsyslog_up rsyslog process status
# TYPE rsyslog_up gauge
rsyslog_up 0

# HELP rsyslog_exporter_success Whether the last collection succeeded
# TYPE rsyslog_exporter_success gauge
rsyslog_exporter_success 0
EOF
        echo "ERROR: jq is required but not installed" >&2
        return
    fi

    # ========================================================================
    # Core Status
    # ========================================================================
    local rsyslog_running=0
    if is_rsyslog_running; then
        rsyslog_running=1
    fi

    cat <<EOF
# HELP rsyslog_up rsyslog process status (1=running, 0=down)
# TYPE rsyslog_up gauge
rsyslog_up $rsyslog_running
EOF

    local version
    version=$(get_rsyslog_version)
    if [ -n "$version" ]; then
        cat <<EOF

# HELP rsyslog_info rsyslog version information
# TYPE rsyslog_info gauge
rsyslog_info{version="$version"} 1
EOF
    fi

    echo ""

    # ========================================================================
    # Total Messages
    # ========================================================================
    local total_messages
    total_messages=$(get_total_messages)

    cat <<EOF
# HELP rsyslog_messages_total Total messages processed (main queue enqueued)
# TYPE rsyslog_messages_total counter
rsyslog_messages_total $total_messages
EOF

    echo ""

    # ========================================================================
    # Queue Metrics
    # ========================================================================
    local queue_stats
    queue_stats=$(get_latest_stats "core.queue")

    if [ -n "$queue_stats" ]; then
        cat <<EOF
# HELP rsyslog_queue_size Current queue depth
# TYPE rsyslog_queue_size gauge
EOF
        echo "$queue_stats" | while IFS= read -r line; do
            local qname qsize
            qname=$(echo "$line" | jq -r '.name // "unknown"')
            qsize=$(echo "$line" | jq -r '.size // 0')
            echo "rsyslog_queue_size{queue=\"$qname\"} $qsize"
        done

        echo ""

        cat <<EOF
# HELP rsyslog_queue_enqueued_total Messages enqueued to queue
# TYPE rsyslog_queue_enqueued_total counter
EOF
        echo "$queue_stats" | while IFS= read -r line; do
            local qname val
            qname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.enqueued // 0')
            echo "rsyslog_queue_enqueued_total{queue=\"$qname\"} $val"
        done

        echo ""

        cat <<EOF
# HELP rsyslog_queue_dequeued_total Messages dequeued from queue
# TYPE rsyslog_queue_dequeued_total counter
EOF
        echo "$queue_stats" | while IFS= read -r line; do
            local qname val
            qname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.dequeued // 0')
            echo "rsyslog_queue_dequeued_total{queue=\"$qname\"} $val"
        done

        echo ""

        cat <<EOF
# HELP rsyslog_queue_full_total Times queue reached full capacity
# TYPE rsyslog_queue_full_total counter
EOF
        echo "$queue_stats" | while IFS= read -r line; do
            local qname val
            qname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.full // 0')
            echo "rsyslog_queue_full_total{queue=\"$qname\"} $val"
        done

        echo ""

        cat <<EOF
# HELP rsyslog_queue_max_size Configured maximum queue size
# TYPE rsyslog_queue_max_size gauge
EOF
        echo "$queue_stats" | while IFS= read -r line; do
            local qname val
            qname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.maxqsize // 0')
            echo "rsyslog_queue_max_size{queue=\"$qname\"} $val"
        done

        echo ""

        cat <<EOF
# HELP rsyslog_queue_disk_usage_bytes Disk-assisted queue disk usage in bytes
# TYPE rsyslog_queue_disk_usage_bytes gauge
EOF
        echo "$queue_stats" | while IFS= read -r line; do
            local qname val
            qname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.["diskspace.used"] // 0')
            echo "rsyslog_queue_disk_usage_bytes{queue=\"$qname\"} $val"
        done

        echo ""
    fi

    # ========================================================================
    # Action Metrics
    # ========================================================================
    local action_stats
    action_stats=$(get_latest_stats "core.action")

    if [ -n "$action_stats" ]; then
        cat <<EOF
# HELP rsyslog_action_processed_total Messages processed per action
# TYPE rsyslog_action_processed_total counter
EOF
        echo "$action_stats" | while IFS= read -r line; do
            local aname val
            aname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.processed // 0')
            echo "rsyslog_action_processed_total{action=\"$aname\"} $val"
        done

        echo ""

        cat <<EOF
# HELP rsyslog_action_failed_total Failed action attempts
# TYPE rsyslog_action_failed_total counter
EOF
        echo "$action_stats" | while IFS= read -r line; do
            local aname val
            aname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.failed // 0')
            echo "rsyslog_action_failed_total{action=\"$aname\"} $val"
        done

        echo ""

        cat <<EOF
# HELP rsyslog_action_suspended Action is currently suspended (1=yes, 0=no)
# TYPE rsyslog_action_suspended gauge
EOF
        echo "$action_stats" | while IFS= read -r line; do
            local aname val
            aname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.suspended // 0')
            echo "rsyslog_action_suspended{action=\"$aname\"} $val"
        done

        echo ""

        cat <<EOF
# HELP rsyslog_action_resumed_total Times action was resumed from suspended state
# TYPE rsyslog_action_resumed_total counter
EOF
        echo "$action_stats" | while IFS= read -r line; do
            local aname val
            aname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.resumed // 0')
            echo "rsyslog_action_resumed_total{action=\"$aname\"} $val"
        done

        echo ""
    fi

    # ========================================================================
    # Input Metrics
    # ========================================================================
    # Input modules use various origin values; collect them all
    local input_stats=""
    for input_origin in "imuxsock" "imjournal" "imtcp" "imudp" "imptcp" "imrelp"; do
        local stats
        stats=$(get_latest_stats "$input_origin")
        if [ -n "$stats" ]; then
            if [ -n "$input_stats" ]; then
                input_stats="${input_stats}"$'\n'"${stats}"
            else
                input_stats="$stats"
            fi
        fi
    done

    if [ -n "$input_stats" ]; then
        cat <<EOF
# HELP rsyslog_input_received_total Messages received per input
# TYPE rsyslog_input_received_total counter
EOF
        echo "$input_stats" | while IFS= read -r line; do
            local iname val
            iname=$(echo "$line" | jq -r '.name // "unknown"')
            val=$(echo "$line" | jq -r '.submitted // .received // 0')
            echo "rsyslog_input_received_total{input=\"$iname\"} $val"
        done

        echo ""
    fi

    # ========================================================================
    # Process Metrics
    # ========================================================================
    if is_rsyslog_running; then
        local mem_bytes open_fds
        mem_bytes=$(get_process_memory)
        open_fds=$(get_open_fds)

        cat <<EOF
# HELP rsyslog_process_memory_bytes RSS memory of rsyslog process in bytes
# TYPE rsyslog_process_memory_bytes gauge
rsyslog_process_memory_bytes $mem_bytes

# HELP rsyslog_process_open_fds Number of open file descriptors
# TYPE rsyslog_process_open_fds gauge
rsyslog_process_open_fds $open_fds
EOF

        echo ""
    fi

    # ========================================================================
    # Exporter Health
    # ========================================================================
    local script_end script_duration
    script_end=$(date +%s.%N 2>/dev/null || date +%s)
    script_duration=$(awk "BEGIN {printf \"%.3f\", $script_end - $script_start}" 2>/dev/null || echo "0")

    cat <<EOF
# HELP rsyslog_exporter_duration_seconds Time to generate all metrics
# TYPE rsyslog_exporter_duration_seconds gauge
rsyslog_exporter_duration_seconds $script_duration

# HELP rsyslog_exporter_last_run_timestamp Unix timestamp of last successful run
# TYPE rsyslog_exporter_last_run_timestamp gauge
rsyslog_exporter_last_run_timestamp $(date +%s)

# HELP rsyslog_exporter_success Whether the last collection succeeded
# TYPE rsyslog_exporter_success gauge
rsyslog_exporter_success $success
EOF

    echo ""
}

# ============================================================================
# HTTP SERVER MODE
# ============================================================================

# Run simple HTTP server using netcat
# Serves metrics on /metrics endpoint
run_http_server() {
    echo "Starting rsyslog metrics exporter on port $HTTP_PORT..." >&2

    if ! command -v nc >/dev/null 2>&1; then
        echo "ERROR: netcat (nc) required for HTTP mode" >&2
        exit 1
    fi

    # Infinite loop accepting HTTP requests
    while true; do
        {
            read -r request
            # Check if request is for /metrics endpoint
            if [[ "$request" =~ ^GET\ /metrics ]]; then
                echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\n\r"
                generate_metrics
            else  # Serve HTML landing page for other requests
                echo -e "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r"
                cat <<EOF
<!DOCTYPE html>
<html>
<head><title>Rsyslog Metrics Exporter v1.0</title></head>
<body>
<h1>Rsyslog Metrics Exporter v1.0</h1>
<p><a href="/metrics">Metrics</a></p>
<h2>Metric Categories</h2>
<ul>
<li>Core Status: rsyslog up/down, version info</li>
<li>Queue Metrics: depth, enqueued/dequeued, full events, disk usage</li>
<li>Action Metrics: processed/failed/suspended per action</li>
<li>Input Metrics: messages received per input module</li>
<li>Process Metrics: memory usage, open file descriptors</li>
<li>Exporter Health: runtime, last run timestamp, success flag</li>
</ul>
</body>
</html>
EOF
            fi
        } | nc -l -p "$HTTP_PORT" -q 1 2>/dev/null
    done
}

# ============================================================================
# TEXTFILE WRITER (with atomic rename)
# ============================================================================

# Write metrics to textfile collector file atomically
# Uses temp file + rename to avoid partial reads
write_textfile() {
    local output_dir
    output_dir="$(dirname "$OUTPUT_FILE")"
    mkdir -p "$output_dir"

    # Create temp file in SAME directory for atomic rename (same filesystem)
    TEMP_FILE=$(mktemp "${output_dir}/.rsyslog_metrics.XXXXXX")

    # Generate metrics to temp file
    if ! generate_metrics > "$TEMP_FILE" 2>/dev/null; then
        rm -f "$TEMP_FILE"
        TEMP_FILE=""
        echo "ERROR: Failed to generate metrics" >&2
        return 1
    fi

    # Validate: file must have content
    local file_lines
    file_lines=$(wc -l < "$TEMP_FILE" 2>/dev/null || echo 0)

    if [ "$file_lines" -lt 5 ]; then
        rm -f "$TEMP_FILE"
        TEMP_FILE=""
        echo "ERROR: Metrics file too small ($file_lines lines), keeping previous" >&2
        return 1
    fi

    # Set permissions before move
    chmod 644 "$TEMP_FILE"

    # Atomic rename - no gap where file is missing
    mv -f "$TEMP_FILE" "$OUTPUT_FILE"
    TEMP_FILE=""

    debug_log "Metrics written to $OUTPUT_FILE ($file_lines lines)"
    return 0
}

# ============================================================================
# MAIN EXECUTION
# ============================================================================

# Main entry point - routes to appropriate output mode
main() {
    parse_args "$@"
    trap cleanup EXIT

    if [ "$HTTP_MODE" = true ]; then
        # Run HTTP server (blocks until killed)
        run_http_server
    elif [ -n "$OUTPUT_FILE" ] && [ "$DAEMON_MODE" = true ]; then
        # Daemon mode: write textfile on a loop
        echo "Starting rsyslog metrics exporter (daemon, interval=${COLLECTION_INTERVAL}s)..." >&2
        echo "Writing to: $OUTPUT_FILE" >&2

        while true; do
            write_textfile || true
            sleep "$COLLECTION_INTERVAL"
        done
    elif [ -n "$OUTPUT_FILE" ]; then
        # Textfile collector mode: write once
        write_textfile
        echo "Metrics written to $OUTPUT_FILE" >&2
    else
        # Default: output to stdout
        generate_metrics
    fi
}

# Execute main function with all script arguments
main "$@"
