Files
linux-scripts/gitlab-metrics-exporter.sh
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

788 lines
32 KiB
Bash
Executable File

#!/bin/bash
################################################
#### GitLab Metrics Collector ####
#### for Prometheus node_exporter textfile ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### Version: 1.01-210426 ####
################################################
set -o pipefail
SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_NAME
# Default configuration
readonly DEFAULT_NODE_DIR="/var/lib/node_exporter"
readonly DEFAULT_COLLECTION_INTERVAL=60
readonly DEFAULT_MAX_PROJECTS=100
readonly DEFAULT_CURL_TIMEOUT=30
readonly DEFAULT_METRICS_URL="http://localhost/-/metrics"
readonly DEFAULT_SIDEKIQ_URL="http://localhost:8082/metrics"
# Configuration variables (can be overridden by environment)
GITLAB_URL=${GITLAB_URL:-}
GITLAB_TOKEN=${GITLAB_TOKEN:-}
GITLAB_METRICS_URL=${GITLAB_METRICS_URL:-$DEFAULT_METRICS_URL}
GITLAB_SIDEKIQ_URL=${GITLAB_SIDEKIQ_URL:-$DEFAULT_SIDEKIQ_URL}
NODE_DIR=${NODE_DIR:-$DEFAULT_NODE_DIR}
COLLECTION_INTERVAL=${COLLECTION_INTERVAL:-$DEFAULT_COLLECTION_INTERVAL}
MAX_PROJECTS=${MAX_PROJECTS:-$DEFAULT_MAX_PROJECTS}
DEBUG=${DEBUG:-}
# Runtime flags
RUN_MODE="once"
LOCAL_MODE=false
# Error tracking
ERRORS_TOTAL=0
handle_error() {
local exit_code=$1
local line_number=$2
echo "Error: $SCRIPT_NAME failed at line $line_number with exit code $exit_code" >&2
exit "$exit_code"
}
trap 'handle_error $? $LINENO' ERR
debug_echo() {
if [[ -n "$DEBUG" ]]; then
echo "[DEBUG] $*" >&2
fi
}
show_help() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]
GitLab metrics collector for Prometheus node_exporter textfile directory.
OPTIONS:
--once Run collection once and exit (default)
--daemon Run continuously at COLLECTION_INTERVAL
--local Scrape /-/metrics endpoint for server-side metrics (Puma, Sidekiq, Redis, DB)
--help, -h Show this help message
ENVIRONMENT VARIABLES:
GITLAB_URL GitLab base URL (required, e.g. https://gitlab.example.com)
GITLAB_TOKEN GitLab private access token (required for API metrics)
GITLAB_METRICS_URL Local metrics endpoint URL (default: $DEFAULT_METRICS_URL)
GITLAB_SIDEKIQ_URL Sidekiq exporter endpoint URL (default: $DEFAULT_SIDEKIQ_URL)
NODE_DIR Node exporter textfile directory (default: $DEFAULT_NODE_DIR)
COLLECTION_INTERVAL Seconds between collections in daemon mode (default: $DEFAULT_COLLECTION_INTERVAL)
MAX_PROJECTS Maximum number of projects to collect per-project metrics for (default: $DEFAULT_MAX_PROJECTS)
DEBUG Enable debug output
EXAMPLES:
GITLAB_URL=https://gitlab.example.com GITLAB_TOKEN=glpat-xxx $SCRIPT_NAME --once
GITLAB_URL=https://gitlab.example.com GITLAB_TOKEN=glpat-xxx $SCRIPT_NAME --daemon
$SCRIPT_NAME --local --once
GITLAB_URL=https://gitlab.example.com GITLAB_TOKEN=glpat-xxx $SCRIPT_NAME --local --daemon
DEBUG=1 GITLAB_URL=https://gitlab.example.com GITLAB_TOKEN=glpat-xxx $SCRIPT_NAME
EOF
}
sanitize_label() {
local value="$1"
echo "${value//[^a-zA-Z0-9_]/_}"
}
gitlab_api() {
local endpoint="$1"
local include_headers="${2:-false}"
local url="${GITLAB_URL}${endpoint}"
debug_echo "API call: $url"
if [[ "$include_headers" == "true" ]]; then
curl -sf --max-time "$DEFAULT_CURL_TIMEOUT" \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-D - \
"$url" 2>/dev/null
else
curl -sf --max-time "$DEFAULT_CURL_TIMEOUT" \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"$url" 2>/dev/null
fi
}
gitlab_api_paginated() {
local endpoint="$1"
local per_page="${2:-100}"
local max_pages="${3:-50}"
local page=1
local all_results="[]"
while [[ $page -le $max_pages ]]; do
local separator="?"
if [[ "$endpoint" == *"?"* ]]; then
separator="&"
fi
local response
response=$(gitlab_api "${endpoint}${separator}per_page=${per_page}&page=${page}" "true" 2>/dev/null) || break
local headers body
headers=$(echo "$response" | sed '/^\r\{0,1\}$/q')
body=$(echo "$response" | sed '1,/^\r\{0,1\}$/d')
if [[ -z "$body" ]] || ! echo "$body" | jq -e '.' >/dev/null 2>&1; then
break
fi
local count
count=$(echo "$body" | jq 'length' 2>/dev/null) || break
if [[ "$count" -eq 0 ]]; then
break
fi
all_results=$(echo "$all_results" "$body" | jq -s '.[0] + .[1]' 2>/dev/null) || break
local next_page
next_page=$(echo "$headers" | grep -i '^x-next-page:' | tr -d '[:space:]' | cut -d: -f2)
if [[ -z "$next_page" ]]; then
break
fi
page=$((page + 1))
done
echo "$all_results"
}
collect_instance_health() {
local metrics=""
debug_echo "Collecting instance health metrics"
# Check if instance is reachable
local up=0
if gitlab_api "/-/health" >/dev/null 2>&1; then
up=1
fi
metrics+="# HELP gitlab_instance_up Whether the GitLab instance is reachable\n"
metrics+="# TYPE gitlab_instance_up gauge\n"
metrics+="gitlab_instance_up $up\n"
# Version info
local version_json
if version_json=$(gitlab_api "/api/v4/version" 2>/dev/null); then
local version revision
version=$(echo "$version_json" | jq -r '.version // "unknown"' 2>/dev/null)
revision=$(echo "$version_json" | jq -r '.revision // "unknown"' 2>/dev/null)
metrics+="# HELP gitlab_instance_version_info GitLab version information\n"
metrics+="# TYPE gitlab_instance_version_info gauge\n"
metrics+="gitlab_instance_version_info{version=\"$version\",revision=\"$revision\"} 1\n"
debug_echo "GitLab version: $version ($revision)"
else
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to collect version info"
fi
echo -e "$metrics"
}
collect_project_statistics() {
local metrics=""
debug_echo "Collecting project statistics"
local projects
if ! projects=$(gitlab_api_paginated "/api/v4/projects?statistics=true" 100 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to collect project statistics"
return
fi
local project_count
project_count=$(echo "$projects" | jq 'length' 2>/dev/null) || project_count=0
debug_echo "Found $project_count projects"
metrics+="# HELP gitlab_project_stars_count Number of stars for a project\n"
metrics+="# TYPE gitlab_project_stars_count gauge\n"
metrics+="# HELP gitlab_project_forks_count Number of forks for a project\n"
metrics+="# TYPE gitlab_project_forks_count gauge\n"
metrics+="# HELP gitlab_project_open_issues_count Number of open issues for a project\n"
metrics+="# TYPE gitlab_project_open_issues_count gauge\n"
metrics+="# HELP gitlab_project_commit_count Number of commits in default branch\n"
metrics+="# TYPE gitlab_project_commit_count gauge\n"
metrics+="# HELP gitlab_project_storage_size_bytes Total storage size in bytes\n"
metrics+="# TYPE gitlab_project_storage_size_bytes gauge\n"
metrics+="# HELP gitlab_project_repository_size_bytes Repository size in bytes\n"
metrics+="# TYPE gitlab_project_repository_size_bytes gauge\n"
metrics+="# HELP gitlab_project_lfs_objects_size_bytes LFS objects size in bytes\n"
metrics+="# TYPE gitlab_project_lfs_objects_size_bytes gauge\n"
metrics+="# HELP gitlab_project_job_artifacts_size_bytes Job artifacts size in bytes\n"
metrics+="# TYPE gitlab_project_job_artifacts_size_bytes gauge\n"
metrics+="# HELP gitlab_project_packages_size_bytes Packages size in bytes\n"
metrics+="# TYPE gitlab_project_packages_size_bytes gauge\n"
metrics+="# HELP gitlab_project_wiki_size_bytes Wiki size in bytes\n"
metrics+="# TYPE gitlab_project_wiki_size_bytes gauge\n"
metrics+="# HELP gitlab_project_snippets_size_bytes Snippets size in bytes\n"
metrics+="# TYPE gitlab_project_snippets_size_bytes gauge\n"
metrics+="# HELP gitlab_project_uploads_size_bytes Uploads size in bytes\n"
metrics+="# TYPE gitlab_project_uploads_size_bytes gauge\n"
echo "$projects" | jq -c '.[]' 2>/dev/null | while IFS= read -r project; do
local name namespace
name=$(echo "$project" | jq -r '.name // "unknown"' 2>/dev/null)
namespace=$(echo "$project" | jq -r '.namespace.name // "unknown"' 2>/dev/null)
name=$(sanitize_label "$name")
namespace=$(sanitize_label "$namespace")
local labels="project=\"$name\",namespace=\"$namespace\""
local stars forks issues
stars=$(echo "$project" | jq -r '.star_count // 0' 2>/dev/null)
forks=$(echo "$project" | jq -r '.forks_count // 0' 2>/dev/null)
issues=$(echo "$project" | jq -r '.open_issues_count // 0' 2>/dev/null)
metrics+="gitlab_project_stars_count{$labels} $stars\n"
metrics+="gitlab_project_forks_count{$labels} $forks\n"
metrics+="gitlab_project_open_issues_count{$labels} $issues\n"
local commit_count storage_size repo_size lfs_size artifacts_size packages_size wiki_size snippets_size uploads_size
commit_count=$(echo "$project" | jq -r '.statistics.commit_count // 0' 2>/dev/null)
storage_size=$(echo "$project" | jq -r '.statistics.storage_size // 0' 2>/dev/null)
repo_size=$(echo "$project" | jq -r '.statistics.repository_size // 0' 2>/dev/null)
lfs_size=$(echo "$project" | jq -r '.statistics.lfs_objects_size // 0' 2>/dev/null)
artifacts_size=$(echo "$project" | jq -r '.statistics.job_artifacts_size // 0' 2>/dev/null)
packages_size=$(echo "$project" | jq -r '.statistics.packages_size // 0' 2>/dev/null)
wiki_size=$(echo "$project" | jq -r '.statistics.wiki_size // 0' 2>/dev/null)
snippets_size=$(echo "$project" | jq -r '.statistics.snippets_size // 0' 2>/dev/null)
uploads_size=$(echo "$project" | jq -r '.statistics.uploads_size // 0' 2>/dev/null)
metrics+="gitlab_project_commit_count{$labels} $commit_count\n"
metrics+="gitlab_project_storage_size_bytes{$labels} $storage_size\n"
metrics+="gitlab_project_repository_size_bytes{$labels} $repo_size\n"
metrics+="gitlab_project_lfs_objects_size_bytes{$labels} $lfs_size\n"
metrics+="gitlab_project_job_artifacts_size_bytes{$labels} $artifacts_size\n"
metrics+="gitlab_project_packages_size_bytes{$labels} $packages_size\n"
metrics+="gitlab_project_wiki_size_bytes{$labels} $wiki_size\n"
metrics+="gitlab_project_snippets_size_bytes{$labels} $snippets_size\n"
metrics+="gitlab_project_uploads_size_bytes{$labels} $uploads_size\n"
done
echo -e "$metrics"
}
collect_pipeline_metrics() {
local metrics=""
debug_echo "Collecting pipeline metrics"
local projects
if ! projects=$(gitlab_api "/api/v4/projects?per_page=${MAX_PROJECTS}&simple=true" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to fetch projects for pipeline metrics"
return
fi
metrics+="# HELP gitlab_pipeline_status Count of pipelines by status\n"
metrics+="# TYPE gitlab_pipeline_status gauge\n"
metrics+="# HELP gitlab_pipeline_duration_seconds Duration of the latest pipeline\n"
metrics+="# TYPE gitlab_pipeline_duration_seconds gauge\n"
echo "$projects" | jq -c '.[]' 2>/dev/null | while IFS= read -r project; do
local project_id name namespace
project_id=$(echo "$project" | jq -r '.id' 2>/dev/null)
name=$(sanitize_label "$(echo "$project" | jq -r '.name // "unknown"' 2>/dev/null)")
namespace=$(sanitize_label "$(echo "$project" | jq -r '.namespace.name // "unknown"' 2>/dev/null)")
local pipelines
if ! pipelines=$(gitlab_api "/api/v4/projects/${project_id}/pipelines?per_page=20" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to fetch pipelines for project $project_id"
continue
fi
# Count pipelines per status
local statuses
statuses=$(echo "$pipelines" | jq -r '.[].status // empty' 2>/dev/null | sort | uniq -c | awk '{print $2 " " $1}')
while IFS= read -r line; do
if [[ -n "$line" ]]; then
local status count
status=$(echo "$line" | awk '{print $1}')
count=$(echo "$line" | awk '{print $2}')
metrics+="gitlab_pipeline_status{project=\"$name\",namespace=\"$namespace\",status=\"$status\"} $count\n"
fi
done <<< "$statuses"
# Latest pipeline duration
local duration
duration=$(echo "$pipelines" | jq -r '.[0].duration // empty' 2>/dev/null)
if [[ -n "$duration" && "$duration" != "null" ]]; then
metrics+="gitlab_pipeline_duration_seconds{project=\"$name\",namespace=\"$namespace\"} $duration\n"
fi
done
echo -e "$metrics"
}
collect_runner_metrics() {
local metrics=""
debug_echo "Collecting runner metrics"
local runners
if ! runners=$(gitlab_api_paginated "/api/v4/runners" 100 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to collect runner metrics"
return
fi
local total online_total
total=$(echo "$runners" | jq 'length' 2>/dev/null) || total=0
online_total=$(echo "$runners" | jq '[.[] | select(.status == "online")] | length' 2>/dev/null) || online_total=0
metrics+="# HELP gitlab_runner_active Whether a runner is active\n"
metrics+="# TYPE gitlab_runner_active gauge\n"
metrics+="# HELP gitlab_runner_online Whether a runner is online\n"
metrics+="# TYPE gitlab_runner_online gauge\n"
metrics+="# HELP gitlab_runners_total Total number of runners\n"
metrics+="# TYPE gitlab_runners_total gauge\n"
metrics+="# HELP gitlab_runners_online_total Total number of online runners\n"
metrics+="# TYPE gitlab_runners_online_total gauge\n"
echo "$runners" | jq -c '.[]' 2>/dev/null | while IFS= read -r runner; do
local runner_name runner_type active status
runner_name=$(sanitize_label "$(echo "$runner" | jq -r '.description // "unknown"' 2>/dev/null)")
runner_type=$(echo "$runner" | jq -r '.runner_type // "unknown"' 2>/dev/null)
active=$(echo "$runner" | jq -r '.active // false' 2>/dev/null)
status=$(echo "$runner" | jq -r '.status // "unknown"' 2>/dev/null)
local active_val=0
if [[ "$active" == "true" ]]; then
active_val=1
fi
local online_val=0
if [[ "$status" == "online" ]]; then
online_val=1
fi
metrics+="gitlab_runner_active{runner_name=\"$runner_name\",runner_type=\"$runner_type\"} $active_val\n"
metrics+="gitlab_runner_online{runner_name=\"$runner_name\",runner_type=\"$runner_type\"} $online_val\n"
done
metrics+="gitlab_runners_total $total\n"
metrics+="gitlab_runners_online_total $online_total\n"
echo -e "$metrics"
}
collect_user_metrics() {
local metrics=""
debug_echo "Collecting user metrics"
local response
if ! response=$(gitlab_api "/api/v4/users?per_page=1" "true" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to collect user metrics"
return
fi
local total
total=$(echo "$response" | grep -i '^x-total:' | tr -d '[:space:]' | cut -d: -f2)
if [[ -n "$total" ]]; then
metrics+="# HELP gitlab_users_total Total number of GitLab users\n"
metrics+="# TYPE gitlab_users_total gauge\n"
metrics+="gitlab_users_total $total\n"
debug_echo "Total users: $total"
else
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to parse user count from X-Total header"
fi
echo -e "$metrics"
}
collect_group_metrics() {
local metrics=""
debug_echo "Collecting group metrics"
local groups
if ! groups=$(gitlab_api_paginated "/api/v4/groups" 100 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to collect group metrics"
return
fi
local total
total=$(echo "$groups" | jq 'length' 2>/dev/null) || total=0
metrics+="# HELP gitlab_groups_total Total number of GitLab groups\n"
metrics+="# TYPE gitlab_groups_total gauge\n"
metrics+="gitlab_groups_total $total\n"
debug_echo "Total groups: $total"
echo -e "$metrics"
}
collect_job_metrics() {
local metrics=""
debug_echo "Collecting job metrics"
local projects
if ! projects=$(gitlab_api "/api/v4/projects?per_page=${MAX_PROJECTS}&simple=true" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to fetch projects for job metrics"
return
fi
metrics+="# HELP gitlab_jobs_by_status Count of jobs by status\n"
metrics+="# TYPE gitlab_jobs_by_status gauge\n"
echo "$projects" | jq -c '.[]' 2>/dev/null | while IFS= read -r project; do
local project_id name namespace
project_id=$(echo "$project" | jq -r '.id' 2>/dev/null)
name=$(sanitize_label "$(echo "$project" | jq -r '.name // "unknown"' 2>/dev/null)")
namespace=$(sanitize_label "$(echo "$project" | jq -r '.namespace.name // "unknown"' 2>/dev/null)")
local jobs
if ! jobs=$(gitlab_api "/api/v4/projects/${project_id}/jobs?per_page=20" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to fetch jobs for project $project_id"
continue
fi
local statuses
statuses=$(echo "$jobs" | jq -r '.[].status // empty' 2>/dev/null | sort | uniq -c | awk '{print $2 " " $1}')
while IFS= read -r line; do
if [[ -n "$line" ]]; then
local status count
status=$(echo "$line" | awk '{print $1}')
count=$(echo "$line" | awk '{print $2}')
metrics+="gitlab_jobs_by_status{project=\"$name\",namespace=\"$namespace\",status=\"$status\"} $count\n"
fi
done <<< "$statuses"
done
echo -e "$metrics"
}
collect_merge_request_metrics() {
local metrics=""
debug_echo "Collecting merge request metrics"
local projects
if ! projects=$(gitlab_api "/api/v4/projects?per_page=${MAX_PROJECTS}&simple=true" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to fetch projects for merge request metrics"
return
fi
metrics+="# HELP gitlab_open_merge_requests Number of open merge requests\n"
metrics+="# TYPE gitlab_open_merge_requests gauge\n"
echo "$projects" | jq -c '.[]' 2>/dev/null | while IFS= read -r project; do
local project_id name namespace
project_id=$(echo "$project" | jq -r '.id' 2>/dev/null)
name=$(sanitize_label "$(echo "$project" | jq -r '.name // "unknown"' 2>/dev/null)")
namespace=$(sanitize_label "$(echo "$project" | jq -r '.namespace.name // "unknown"' 2>/dev/null)")
local mrs
if ! mrs=$(gitlab_api "/api/v4/projects/${project_id}/merge_requests?state=opened&per_page=100" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to fetch merge requests for project $project_id"
continue
fi
local count
count=$(echo "$mrs" | jq 'length' 2>/dev/null) || count=0
metrics+="gitlab_open_merge_requests{project=\"$name\",namespace=\"$namespace\"} $count\n"
done
echo -e "$metrics"
}
collect_environment_metrics() {
local metrics=""
debug_echo "Collecting environment metrics"
local projects
if ! projects=$(gitlab_api "/api/v4/projects?per_page=${MAX_PROJECTS}&simple=true" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to fetch projects for environment metrics"
return
fi
metrics+="# HELP gitlab_environments_total Number of environments per project\n"
metrics+="# TYPE gitlab_environments_total gauge\n"
echo "$projects" | jq -c '.[]' 2>/dev/null | while IFS= read -r project; do
local project_id name namespace
project_id=$(echo "$project" | jq -r '.id' 2>/dev/null)
name=$(sanitize_label "$(echo "$project" | jq -r '.name // "unknown"' 2>/dev/null)")
namespace=$(sanitize_label "$(echo "$project" | jq -r '.namespace.name // "unknown"' 2>/dev/null)")
local envs
if ! envs=$(gitlab_api "/api/v4/projects/${project_id}/environments?per_page=100" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
debug_echo "Failed to fetch environments for project $project_id"
continue
fi
local count
count=$(echo "$envs" | jq 'length' 2>/dev/null) || count=0
metrics+="gitlab_environments_total{project=\"$name\",namespace=\"$namespace\"} $count\n"
done
echo -e "$metrics"
}
collect_local_metrics() {
local metrics=""
debug_echo "Scraping local metrics from $GITLAB_METRICS_URL"
local raw_metrics
if ! raw_metrics=$(curl -sf --max-time "$DEFAULT_CURL_TIMEOUT" "$GITLAB_METRICS_URL" 2>/dev/null); then
ERRORS_TOTAL=$((ERRORS_TOTAL + 1))
echo "Error: Failed to scrape $GITLAB_METRICS_URL" >&2
echo "Ensure this host's IP is in gitlab_rails['monitoring_whitelist']" >&2
return
fi
# GitLab version info
local version_patterns="^gitlab_version_info[{ ]"
local version_help="^# (HELP|TYPE) gitlab_version_info"
metrics+=$(echo "$raw_metrics" | grep -E "$version_help|$version_patterns" 2>/dev/null || true)
metrics+=$'\n'
# Puma metrics
local puma_patterns="^puma_workers[{ ]|^puma_running_workers[{ ]|^puma_running[{ ]|^puma_queued_connections[{ ]|^puma_active_connections[{ ]|^puma_pool_capacity[{ ]|^puma_max_threads[{ ]|^puma_idle_threads[{ ]"
local puma_help="^# (HELP|TYPE) puma_"
metrics+=$(echo "$raw_metrics" | grep -E "$puma_help|$puma_patterns" 2>/dev/null || true)
metrics+=$'\n'
# Sidekiq metrics (served by separate Sidekiq exporter, default localhost:8082)
local sidekiq_raw
if sidekiq_raw=$(curl -sf --max-time "$DEFAULT_CURL_TIMEOUT" "$GITLAB_SIDEKIQ_URL" 2>/dev/null); then
debug_echo "Scraped Sidekiq metrics from $GITLAB_SIDEKIQ_URL"
# Core Sidekiq job metrics
local sidekiq_patterns="^sidekiq_running_jobs[{ ]|^sidekiq_concurrency[{ ]|^sidekiq_mem_total_bytes[{ ]|^sidekiq_jobs_failed_total[{ ]|^sidekiq_jobs_dead_total[{ ]|^sidekiq_enqueued_jobs_total[{ ]|^sidekiq_jobs_completion_seconds[_{ ]|^sidekiq_jobs_queue_duration_seconds[_{ ]|^sidekiq_jobs_cpu_seconds[_{ ]|^sidekiq_jobs_db_seconds[_{ ]|^sidekiq_jobs_gitaly_seconds[_{ ]|^sidekiq_redis_requests_total[{ ]|^sidekiq_redis_requests_duration_seconds[_{ ]"
local sidekiq_help="^# (HELP|TYPE) sidekiq_(running_jobs|concurrency|mem_total_bytes|jobs_failed_total|jobs_dead_total|enqueued_jobs_total|jobs_completion_seconds|jobs_queue_duration_seconds|jobs_cpu_seconds|jobs_db_seconds|jobs_gitaly_seconds|redis_requests_total|redis_requests_duration_seconds)"
metrics+=$(echo "$sidekiq_raw" | grep -E "$sidekiq_help|$sidekiq_patterns" 2>/dev/null || true)
metrics+=$'\n'
# CI/CD pipeline internals
local ci_patterns="^pipelines_created_total[{ ]|^deployments[{ ]|^gitlab_ci_pipeline_creation_duration_seconds[_{ ]|^gitlab_ci_pipeline_failure_reasons[{ ]|^gitlab_ci_active_jobs[_{ ]"
local ci_help="^# (HELP|TYPE) (pipelines_created_total|deployments|gitlab_ci_pipeline_creation_duration_seconds|gitlab_ci_pipeline_failure_reasons|gitlab_ci_active_jobs)"
metrics+=$(echo "$sidekiq_raw" | grep -E "$ci_help|$ci_patterns" 2>/dev/null || true)
metrics+=$'\n'
# Email delivery metrics
local email_patterns="^gitlab_emails_delivered_total[{ ]|^gitlab_emails_delivery_attempts_total[{ ]"
local email_help="^# (HELP|TYPE) gitlab_emails_(delivered_total|delivery_attempts_total)"
metrics+=$(echo "$sidekiq_raw" | grep -E "$email_help|$email_patterns" 2>/dev/null || true)
metrics+=$'\n'
# External HTTP (webhooks, integrations)
local ext_http_patterns="^gitlab_external_http_total[{ ]|^gitlab_external_http_duration_seconds[_{ ]"
local ext_http_help="^# (HELP|TYPE) gitlab_external_http_(total|duration_seconds)"
metrics+=$(echo "$sidekiq_raw" | grep -E "$ext_http_help|$ext_http_patterns" 2>/dev/null || true)
metrics+=$'\n'
# Sidekiq SLI apdex/errors
local sli_patterns="^gitlab_sli_sidekiq_execution_apdex_success_total[{ ]|^gitlab_sli_sidekiq_execution_apdex_total[{ ]|^gitlab_sli_sidekiq_execution_error_total[{ ]|^gitlab_sli_sidekiq_execution_total[{ ]"
local sli_help="^# (HELP|TYPE) gitlab_sli_sidekiq_execution"
metrics+=$(echo "$sidekiq_raw" | grep -E "$sli_help|$sli_patterns" 2>/dev/null || true)
metrics+=$'\n'
# DB transaction duration, primary SQL, threads, cache, workers
local extra_patterns="^gitlab_database_transaction_seconds[_{ ]|^gitlab_sql_primary_duration_seconds[_{ ]|^gitlab_ruby_threads_running_threads[{ ]|^gitlab_ruby_threads_max_expected_threads[{ ]|^limited_capacity_worker_running_jobs[{ ]|^limited_capacity_worker_max_running_jobs[{ ]|^limited_capacity_worker_remaining_work_count[{ ]|^redis_hit_miss_operations_total[{ ]"
local extra_help="^# (HELP|TYPE) (gitlab_database_transaction_seconds|gitlab_sql_primary_duration_seconds|gitlab_ruby_threads_running_threads|gitlab_ruby_threads_max_expected_threads|limited_capacity_worker_running_jobs|limited_capacity_worker_max_running_jobs|limited_capacity_worker_remaining_work_count|redis_hit_miss_operations_total)"
metrics+=$(echo "$sidekiq_raw" | grep -E "$extra_help|$extra_patterns" 2>/dev/null || true)
metrics+=$'\n'
else
debug_echo "Warning: Could not scrape Sidekiq exporter at $GITLAB_SIDEKIQ_URL (is sidekiq_exporter enabled?)"
fi
# Redis metrics
local redis_patterns="^gitlab_redis_client_requests_total[{ ]|^gitlab_redis_client_exceptions_total[{ ]|^gitlab_redis_client_requests_duration_seconds[_{ ]|^gitlab_redis_client_requests_duration_seconds_sum[{ ]|^gitlab_redis_client_requests_duration_seconds_count[{ ]"
local redis_help="^# (HELP|TYPE) gitlab_redis_client_(requests_total|exceptions_total|requests_duration_seconds)"
metrics+=$(echo "$raw_metrics" | grep -E "$redis_help|$redis_patterns" 2>/dev/null || true)
metrics+=$'\n'
# Database connection pool metrics
local db_patterns="^gitlab_database_connection_pool_"
local db_help="^# (HELP|TYPE) gitlab_database_connection_pool_"
metrics+=$(echo "$raw_metrics" | grep -E "$db_help|$db_patterns" 2>/dev/null || true)
metrics+=$'\n'
# Process metrics (CPU, memory, file descriptors)
local process_patterns="^ruby_process_resident_memory_bytes[{ ]|^ruby_process_cpu_seconds_total[{ ]|^process_open_fds[{ ]|^process_max_fds[{ ]|^ruby_gc_stat_heap_live_slots[{ ]|^ruby_gc_stat_heap_free_slots[{ ]"
local process_help="^# (HELP|TYPE) (ruby_process_resident_memory_bytes|ruby_process_cpu_seconds_total|process_open_fds|process_max_fds|ruby_gc_stat_heap_live_slots|ruby_gc_stat_heap_free_slots)"
metrics+=$(echo "$raw_metrics" | grep -E "$process_help|$process_patterns" 2>/dev/null || true)
metrics+=$'\n'
# GitLab transaction/request metrics
local txn_patterns="^gitlab_transaction_duration_seconds[{ _]|^gitlab_sql_duration_seconds[{ _]|^gitlab_cache_operation_duration_seconds[{ _]"
local txn_help="^# (HELP|TYPE) (gitlab_transaction_duration_seconds|gitlab_sql_duration_seconds|gitlab_cache_operation_duration_seconds)"
metrics+=$(echo "$raw_metrics" | grep -E "$txn_help|$txn_patterns" 2>/dev/null || true)
metrics+=$'\n'
# User session and ActionCable metrics
local session_patterns="^user_session_logins_total[{ ]|^action_cable_active_connections[{ ]|^action_cable_pool_current_size[{ ]"
local session_help="^# (HELP|TYPE) (user_session_logins_total|action_cable_active_connections|action_cable_pool_current_size)"
metrics+=$(echo "$raw_metrics" | grep -E "$session_help|$session_patterns" 2>/dev/null || true)
metrics+=$'\n'
local metric_count
metric_count=$(echo "$metrics" | grep -cv '^#\|^$' 2>/dev/null) || metric_count=0
debug_echo "Extracted $metric_count local metrics"
printf '%s\n' "$metrics"
}
run_collection() {
local start_time
start_time=$(date +%s)
ERRORS_TOTAL=0
debug_echo "Starting metrics collection"
local all_metrics=""
# API-based metrics (require GITLAB_URL and GITLAB_TOKEN)
if [[ -n "$GITLAB_URL" && -n "$GITLAB_TOKEN" ]]; then
all_metrics+="$(collect_instance_health)"$'\n'
all_metrics+="$(collect_project_statistics)"$'\n'
all_metrics+="$(collect_pipeline_metrics)"$'\n'
all_metrics+="$(collect_runner_metrics)"$'\n'
all_metrics+="$(collect_user_metrics)"$'\n'
all_metrics+="$(collect_group_metrics)"$'\n'
all_metrics+="$(collect_job_metrics)"$'\n'
all_metrics+="$(collect_merge_request_metrics)"$'\n'
all_metrics+="$(collect_environment_metrics)"$'\n'
fi
# Local server-side metrics (scraped from /-/metrics)
if [[ "$LOCAL_MODE" == true ]]; then
all_metrics+="$(collect_local_metrics)"$'\n'
fi
local end_time duration
end_time=$(date +%s)
duration=$((end_time - start_time))
all_metrics+="# HELP gitlab_collector_duration_seconds Time taken to collect all metrics\n"
all_metrics+="# TYPE gitlab_collector_duration_seconds gauge\n"
all_metrics+="gitlab_collector_duration_seconds $duration\n"
all_metrics+="# HELP gitlab_collector_last_run_timestamp Unix timestamp of last collection run\n"
all_metrics+="# TYPE gitlab_collector_last_run_timestamp gauge\n"
all_metrics+="gitlab_collector_last_run_timestamp $end_time\n"
all_metrics+="# HELP gitlab_collector_errors_total Number of errors during collection\n"
all_metrics+="# TYPE gitlab_collector_errors_total gauge\n"
all_metrics+="gitlab_collector_errors_total $ERRORS_TOTAL\n"
# Write atomically
mkdir -p "$NODE_DIR"
local tmp_file
tmp_file=$(mktemp "${NODE_DIR}/gitlab_metrics.prom.XXXXXX")
echo -e "$all_metrics" > "$tmp_file"
mv "$tmp_file" "${NODE_DIR}/gitlab_metrics.prom"
debug_echo "Collection complete in ${duration}s with $ERRORS_TOTAL errors"
}
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
--once)
RUN_MODE="once"
shift
;;
--daemon)
RUN_MODE="daemon"
shift
;;
--local)
LOCAL_MODE=true
shift
;;
--help|-h)
show_help
exit 0
;;
*)
echo "Error: Unknown option: $1" >&2
show_help >&2
exit 1
;;
esac
done
}
validate_requirements() {
# API credentials only required when not running local-only
if [[ -z "$GITLAB_URL" && "$LOCAL_MODE" != true ]]; then
echo "Error: GITLAB_URL is required (or use --local for server-side only)" >&2
exit 1
fi
if [[ -z "$GITLAB_TOKEN" && "$LOCAL_MODE" != true ]]; then
echo "Error: GITLAB_TOKEN is required (or use --local for server-side only)" >&2
exit 1
fi
# Strip trailing slash from URLs
GITLAB_URL="${GITLAB_URL%/}"
GITLAB_METRICS_URL="${GITLAB_METRICS_URL%/}"
GITLAB_SIDEKIQ_URL="${GITLAB_SIDEKIQ_URL%/}"
if ! command -v curl >/dev/null 2>&1; then
echo "Error: curl is required but not installed" >&2
exit 1
fi
if [[ -n "$GITLAB_TOKEN" ]]; then
if ! command -v jq >/dev/null 2>&1; then
echo "Error: jq is required but not installed" >&2
exit 1
fi
fi
}
main() {
parse_arguments "$@"
validate_requirements
debug_echo "GitLab URL: $GITLAB_URL"
debug_echo "Metrics URL: $GITLAB_METRICS_URL"
debug_echo "Sidekiq URL: $GITLAB_SIDEKIQ_URL"
debug_echo "Node exporter dir: $NODE_DIR"
debug_echo "Run mode: $RUN_MODE"
debug_echo "Local mode: $LOCAL_MODE"
debug_echo "Max projects: $MAX_PROJECTS"
if [[ "$RUN_MODE" == "daemon" ]]; then
debug_echo "Running in daemon mode with ${COLLECTION_INTERVAL}s interval"
while true; do
run_collection
sleep "$COLLECTION_INTERVAL"
done
else
run_collection
fi
debug_echo "Script completed successfully"
}
# Execute main function if script is run directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi