Files
linux-scripts/flarum-exporter.sh
T
chiefgeek 28a9bdf6c7
Lint Scripts / powershell-lint (push) Failing after 12m42s
Lint Scripts / shellcheck (push) Failing after 16m56s
Add flarum-exporter.sh v1.0 — Flarum forum metrics for Prometheus
2026-05-26 22:51:51 +02:00

387 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Flarum Prometheus Metrics Exporter
#
# Prometheus exporter for Flarum forum software.
# Uses MariaDB/MySQL to query the Flarum database directly for
# user counts, discussion/post stats, tag breakdowns, engagement
# metrics (likes, flags), and overall forum health.
#
# Usage:
# FLARUM_DB_PASS="xxx" ./flarum-exporter.sh
# FLARUM_DB_PASS="xxx" ./flarum-exporter.sh --textfile
# FLARUM_DB_PASS="xxx" ./flarum-exporter.sh --http
#
# Parameters:
# --textfile Write to textfile collector directory
# --http Start a simple HTTP server on port 9199
# --help Show usage
#
# Environment:
# FLARUM_DB_HOST Database host (default: localhost)
# FLARUM_DB_PORT Database port (default: 3306)
# FLARUM_DB_NAME Database name (default: flarum)
# FLARUM_DB_USER Database user (default: flarum)
# FLARUM_DB_PASS Database password (required)
# TEXTFILE_DIR Textfile collector directory (default: /var/lib/node_exporter/textfile_collector)
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
# Version: 1.0
#
# Metrics Exported:
# Core:
# - flarum_up
# - flarum_exporter_info{version}
#
# Users:
# - flarum_users_total
# - flarum_users_email_confirmed
# - flarum_users_suspended
# - flarum_users_active_1d
# - flarum_users_active_7d
# - flarum_users_active_30d
#
# Content:
# - flarum_discussions_total
# - flarum_discussions_open
# - flarum_discussions_locked
# - flarum_discussions_sticky
# - flarum_posts_total
# - flarum_comments_total
#
# Tags:
# - flarum_tags_total
# - flarum_discussions_per_tag{tag}
#
# Engagement:
# - flarum_likes_total
# - flarum_flags_total
#
# Exporter:
# - flarum_exporter_duration_seconds
# - flarum_exporter_last_run_timestamp
set -euo pipefail
# --- Configuration ---
readonly VERSION="1.0"
readonly SCRIPT_NAME="$(basename "$0")"
FLARUM_DB_HOST="${FLARUM_DB_HOST:-localhost}"
FLARUM_DB_PORT="${FLARUM_DB_PORT:-3306}"
FLARUM_DB_NAME="${FLARUM_DB_NAME:-flarum}"
FLARUM_DB_USER="${FLARUM_DB_USER:-flarum}"
FLARUM_DB_PASS="${FLARUM_DB_PASS:-}"
TEXTFILE_DIR="${TEXTFILE_DIR:-/var/lib/node_exporter/textfile_collector}"
TEXTFILE_MODE=false
HTTP_MODE=false
readonly HTTP_PORT=9199
OUTPUT=""
START_TIME=""
# --- Functions ---
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS]
Flarum Prometheus Metrics Exporter
Options:
--textfile Write metrics to textfile collector directory
--http Start HTTP server on port ${HTTP_PORT} serving /metrics
--help Show this help message
Environment Variables:
FLARUM_DB_HOST Database host (default: localhost)
FLARUM_DB_PORT Database port (default: 3306)
FLARUM_DB_NAME Database name (default: flarum)
FLARUM_DB_USER Database user (default: flarum)
FLARUM_DB_PASS Database password (required)
TEXTFILE_DIR Output directory (default: /var/lib/node_exporter/textfile_collector)
Examples:
FLARUM_DB_PASS="xxx" $SCRIPT_NAME
FLARUM_DB_PASS="xxx" $SCRIPT_NAME --textfile
FLARUM_DB_PASS="xxx" $SCRIPT_NAME --http
EOF
exit 0
}
check_dependencies() {
local missing=()
for cmd in mysql bc; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "ERROR: Missing required commands: ${missing[*]}" >&2
echo "Install with: apt install ${missing[*]} OR dnf install ${missing[*]}" >&2
exit 1
fi
}
validate_config() {
if [[ -z "$FLARUM_DB_PASS" ]]; then
echo "ERROR: FLARUM_DB_PASS environment variable is required" >&2
exit 1
fi
}
db_query() {
local query="$1"
mysql -h "$FLARUM_DB_HOST" -P "$FLARUM_DB_PORT" -u "$FLARUM_DB_USER" \
-p"$FLARUM_DB_PASS" "$FLARUM_DB_NAME" -N -B -e "$query" 2>/dev/null || echo ""
}
sanitize_label() {
local value="$1"
echo "$value" | sed 's/[^a-zA-Z0-9_\/.-]/_/g'
}
add_metric() {
local name="$1"
local type="$2"
local help="$3"
local value="$4"
local labels="${5:-}"
if [[ -n "$labels" ]]; then
OUTPUT+="# HELP ${name} ${help}
# TYPE ${name} ${type}
${name}{${labels}} ${value}
"
else
OUTPUT+="# HELP ${name} ${help}
# TYPE ${name} ${type}
${name} ${value}
"
fi
}
add_metric_value() {
local name="$1"
local value="$2"
local labels="${3:-}"
if [[ -n "$labels" ]]; then
OUTPUT+="${name}{${labels}} ${value}
"
else
OUTPUT+="${name} ${value}
"
fi
}
collect_health() {
local result
result=$(db_query "SELECT 1;")
if [[ -z "$result" || "$result" != "1" ]]; then
add_metric "flarum_up" "gauge" "Flarum database reachability (1=up, 0=down)" "0"
return 1
fi
add_metric "flarum_up" "gauge" "Flarum database reachability (1=up, 0=down)" "1"
return 0
}
collect_users() {
local total
total=$(db_query "SELECT COUNT(*) FROM users;")
add_metric "flarum_users_total" "gauge" "Total number of registered users" "${total:-0}"
local confirmed
confirmed=$(db_query "SELECT COUNT(*) FROM users WHERE is_email_confirmed = 1;")
add_metric "flarum_users_email_confirmed" "gauge" "Users with confirmed email" "${confirmed:-0}"
local suspended
suspended=$(db_query "SELECT COUNT(*) FROM users WHERE suspended_until IS NOT NULL AND suspended_until > NOW();")
add_metric "flarum_users_suspended" "gauge" "Currently suspended users" "${suspended:-0}"
local active_1d
active_1d=$(db_query "SELECT COUNT(*) FROM users WHERE last_seen_at >= NOW() - INTERVAL 1 DAY;")
add_metric "flarum_users_active_1d" "gauge" "Users active in the past 24 hours" "${active_1d:-0}"
local active_7d
active_7d=$(db_query "SELECT COUNT(*) FROM users WHERE last_seen_at >= NOW() - INTERVAL 7 DAY;")
add_metric "flarum_users_active_7d" "gauge" "Users active in the past 7 days" "${active_7d:-0}"
local active_30d
active_30d=$(db_query "SELECT COUNT(*) FROM users WHERE last_seen_at >= NOW() - INTERVAL 30 DAY;")
add_metric "flarum_users_active_30d" "gauge" "Users active in the past 30 days" "${active_30d:-0}"
}
collect_content() {
local discussions_total
discussions_total=$(db_query "SELECT COUNT(*) FROM discussions;")
add_metric "flarum_discussions_total" "gauge" "Total number of discussions" "${discussions_total:-0}"
local discussions_open
discussions_open=$(db_query "SELECT COUNT(*) FROM discussions WHERE hidden_at IS NULL;")
add_metric "flarum_discussions_open" "gauge" "Open discussions (not hidden/deleted)" "${discussions_open:-0}"
local discussions_locked
discussions_locked=$(db_query "SELECT COUNT(*) FROM discussions WHERE is_locked = 1;")
add_metric "flarum_discussions_locked" "gauge" "Locked discussions" "${discussions_locked:-0}"
local discussions_sticky
discussions_sticky=$(db_query "SELECT COUNT(*) FROM discussions WHERE is_sticky = 1;")
add_metric "flarum_discussions_sticky" "gauge" "Sticky discussions" "${discussions_sticky:-0}"
local posts_total
posts_total=$(db_query "SELECT COUNT(*) FROM posts;")
add_metric "flarum_posts_total" "gauge" "Total number of posts" "${posts_total:-0}"
local comments_total
comments_total=$(db_query "SELECT COALESCE(SUM(comment_count), 0) FROM discussions;")
add_metric "flarum_comments_total" "gauge" "Total comment count across all discussions" "${comments_total:-0}"
}
collect_tags() {
local tags_total
tags_total=$(db_query "SELECT COUNT(*) FROM tags;")
add_metric "flarum_tags_total" "gauge" "Total number of tags" "${tags_total:-0}"
# Per-tag discussion counts
local tag_data
tag_data=$(db_query "SELECT t.name, COUNT(dt.discussion_id) FROM tags t LEFT JOIN discussion_tag dt ON t.id = dt.tag_id GROUP BY t.id, t.name;")
if [[ -n "$tag_data" ]]; then
OUTPUT+="# HELP flarum_discussions_per_tag Number of discussions per tag
# TYPE flarum_discussions_per_tag gauge
"
while IFS=$'\t' read -r tag_name count; do
if [[ -n "$tag_name" ]]; then
local safe_tag
safe_tag=$(sanitize_label "$tag_name")
add_metric_value "flarum_discussions_per_tag" "${count:-0}" "tag=\"${safe_tag}\""
fi
done <<< "$tag_data"
fi
}
collect_engagement() {
local likes
likes=$(db_query "SELECT COUNT(*) FROM post_likes;" 2>/dev/null)
if [[ -n "$likes" ]]; then
add_metric "flarum_likes_total" "gauge" "Total number of likes" "${likes:-0}"
fi
local flags
flags=$(db_query "SELECT COUNT(*) FROM flags;" 2>/dev/null)
if [[ -n "$flags" ]]; then
add_metric "flarum_flags_total" "gauge" "Total number of flags" "${flags:-0}"
fi
}
write_output() {
if [[ "$TEXTFILE_MODE" == true ]]; then
local output_file="${TEXTFILE_DIR}/flarum.prom"
local temp_file
temp_file=$(mktemp "${TEXTFILE_DIR}/flarum.prom.XXXXXX")
mkdir -p "$TEXTFILE_DIR"
echo "$OUTPUT" > "$temp_file"
mv "$temp_file" "$output_file"
else
echo "$OUTPUT"
fi
}
serve_http() {
# Check for netcat
local nc_cmd=""
if command -v ncat &>/dev/null; then
nc_cmd="ncat"
elif command -v nc &>/dev/null; then
nc_cmd="nc"
else
echo "ERROR: Neither ncat nor nc found. Install nmap-ncat or netcat." >&2
exit 1
fi
echo "# Flarum exporter listening on port ${HTTP_PORT}..."
while true; do
{
# Read the HTTP request
local request=""
read -r request || true
# Collect fresh metrics
OUTPUT=""
START_TIME=$(date +%s%N)
add_metric "flarum_exporter_info" "gauge" "Exporter version information" "1" "version=\"${VERSION}\""
if collect_health; then
collect_users
collect_content
collect_tags
collect_engagement
fi
local end_time duration
end_time=$(date +%s%N)
duration=$(echo "scale=2; ($end_time - $START_TIME) / 1000000000" | bc 2>/dev/null || echo "0")
add_metric "flarum_exporter_duration_seconds" "gauge" "Time to generate all metrics" "$duration"
add_metric "flarum_exporter_last_run_timestamp" "gauge" "Unix timestamp of last successful run" "$(date +%s)"
# Serve response
local body="$OUTPUT"
local content_length=${#body}
printf "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s" "$content_length" "$body"
} | "$nc_cmd" -l -p "$HTTP_PORT" 2>/dev/null || "$nc_cmd" -l "$HTTP_PORT" 2>/dev/null || true
done
}
# --- Main ---
main() {
# Parse arguments
for arg in "$@"; do
case "$arg" in
--textfile) TEXTFILE_MODE=true ;;
--http) HTTP_MODE=true ;;
--help|-h) usage ;;
*) echo "Unknown option: $arg" >&2; usage ;;
esac
done
check_dependencies
validate_config
if [[ "$HTTP_MODE" == true ]]; then
serve_http
exit 0
fi
START_TIME=$(date +%s%N)
# Exporter info
add_metric "flarum_exporter_info" "gauge" "Exporter version information" "1" "version=\"${VERSION}\""
# Collect metrics
if collect_health; then
collect_users
collect_content
collect_tags
collect_engagement
fi
# Exporter performance
local end_time duration
end_time=$(date +%s%N)
duration=$(echo "scale=2; ($end_time - $START_TIME) / 1000000000" | bc 2>/dev/null || echo "0")
add_metric "flarum_exporter_duration_seconds" "gauge" "Time to generate all metrics" "$duration"
add_metric "flarum_exporter_last_run_timestamp" "gauge" "Unix timestamp of last successful run" "$(date +%s)"
write_output
}
main "$@"