Add flarum-exporter.sh v1.0 — Flarum forum metrics for Prometheus
This commit is contained in:
Executable
+386
@@ -0,0 +1,386 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user