Files
linux-scripts/migrate-promtail-to-alloy.sh
T

566 lines
18 KiB
Bash

#!/bin/bash
##############################################################################
#### Promtail to Grafana Alloy Migration Script ####
#### ####
#### Detects OS, reads existing Promtail config for Loki URL/hostname, ####
#### generates equivalent Alloy River config, installs Alloy, and ####
#### handles the cutover from Promtail to Alloy. ####
#### ####
#### Supports: Ubuntu, Debian, RHEL, CentOS, Rocky, Alma, Amazon Linux ####
#### ####
#### Author: Phil Connor ####
#### License: MIT ####
#### Contact: contact@mylinux.work ####
#### Version: 1.0.0-030326 ####
##############################################################################
set -euo pipefail
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_VERSION="1.0.0-030326"
# Defaults
ALLOY_CONFIG_DIR="/etc/alloy"
ALLOY_CONFIG_FILE="/etc/alloy/config.alloy"
PROMTAIL_CONFIG="/etc/promtail/config.yml"
LOKI_URL=""
CUSTOM_HOSTNAME=""
DRY_RUN=false
GENERATE_ONLY=false
SKIP_INSTALL=false
SKIP_CUTOVER=false
KEEP_PROMTAIL=true
VERBOSE=false
INCLUDE_JOURNAL=true
INCLUDE_NGINX=false
INCLUDE_APACHE=false
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
debug() { [[ "$VERBOSE" == true ]] && echo -e "${BLUE}[DEBUG]${NC} $1"; }
show_help() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]
Migrate from Promtail to Grafana Alloy. Generates an Alloy config that
maintains Promtail-compatible labels so existing dashboards keep working.
OPTIONS:
--loki-url URL Loki push URL (default: extracted from Promtail config)
--hostname NAME Override hostname (default: auto-detect or from Promtail)
--promtail-config F Path to existing Promtail config (default: /etc/promtail/config.yml)
--output FILE Alloy config output path (default: /etc/alloy/config.alloy)
--generate-only Only generate the Alloy config, don't install or cutover
--skip-install Skip Alloy installation (already installed)
--skip-cutover Generate config and install, but don't stop Promtail
--no-journal Skip systemd journal collection
--include-nginx Include nginx log collection
--include-apache Include Apache log collection
--remove-promtail Remove Promtail package after cutover (default: keep)
--dry-run Show what would be done without making changes
--verbose Enable verbose output
--version Show version
--help, -h Show this help message
EXAMPLES:
# Auto-detect everything from existing Promtail config
sudo $SCRIPT_NAME
# Specify Loki URL and hostname
sudo $SCRIPT_NAME --loki-url http://loki.example.com:3100 --hostname web-01
# Generate config only (don't install or cutover)
$SCRIPT_NAME --generate-only --loki-url http://loki:3100 --output /tmp/config.alloy
# Full migration with nginx logs
sudo $SCRIPT_NAME --include-nginx --remove-promtail
# Dry run to see what would happen
sudo $SCRIPT_NAME --dry-run
EOF
}
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
--loki-url) LOKI_URL="$2"; shift 2 ;;
--hostname) CUSTOM_HOSTNAME="$2"; shift 2 ;;
--promtail-config) PROMTAIL_CONFIG="$2"; shift 2 ;;
--output) ALLOY_CONFIG_FILE="$2"; shift 2 ;;
--generate-only) GENERATE_ONLY=true; shift ;;
--skip-install) SKIP_INSTALL=true; shift ;;
--skip-cutover) SKIP_CUTOVER=true; shift ;;
--no-journal) INCLUDE_JOURNAL=false; shift ;;
--include-nginx) INCLUDE_NGINX=true; shift ;;
--include-apache) INCLUDE_APACHE=true; shift ;;
--remove-promtail) KEEP_PROMTAIL=false; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--verbose) VERBOSE=true; shift ;;
--version) echo "$SCRIPT_NAME version $SCRIPT_VERSION"; exit 0 ;;
--help|-h) show_help; exit 0 ;;
*) error "Unknown option: $1"; show_help; exit 1 ;;
esac
done
}
detect_os() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release
OS=$ID
OS_PRETTY="$PRETTY_NAME"
else
error "Cannot detect OS"
exit 1
fi
debug "Detected OS: $OS_PRETTY"
}
detect_hostname() {
if [[ -n "$CUSTOM_HOSTNAME" ]]; then
DETECTED_HOSTNAME="$CUSTOM_HOSTNAME"
debug "Using custom hostname: $DETECTED_HOSTNAME"
return
fi
# Try to extract from Promtail config
if [[ -f "$PROMTAIL_CONFIG" ]]; then
local pt_host
pt_host=$(grep -m1 'host:' "$PROMTAIL_CONFIG" 2>/dev/null | awk '{print $2}' | tr -d '"' || true)
if [[ -n "$pt_host" ]]; then
DETECTED_HOSTNAME="$pt_host"
debug "Extracted hostname from Promtail config: $DETECTED_HOSTNAME"
return
fi
fi
DETECTED_HOSTNAME=$(hostname -f 2>/dev/null || hostname)
debug "Using system hostname: $DETECTED_HOSTNAME"
}
detect_loki_url() {
if [[ -n "$LOKI_URL" ]]; then
debug "Using provided Loki URL: $LOKI_URL"
return
fi
# Extract from Promtail config
if [[ -f "$PROMTAIL_CONFIG" ]]; then
LOKI_URL=$(grep -m1 'url:' "$PROMTAIL_CONFIG" 2>/dev/null | awk '{print $2}' | tr -d '"' | sed 's|/loki/api/v1/push||' || true)
if [[ -n "$LOKI_URL" ]]; then
debug "Extracted Loki URL from Promtail config: $LOKI_URL"
return
fi
fi
error "Could not determine Loki URL"
error "Provide with --loki-url or ensure Promtail config exists at $PROMTAIL_CONFIG"
exit 1
}
check_promtail_status() {
if systemctl is-active --quiet promtail 2>/dev/null; then
PROMTAIL_RUNNING=true
log "Promtail is currently running"
else
PROMTAIL_RUNNING=false
debug "Promtail is not running"
fi
}
# Generate an Alloy loki.source.file block if the log file exists
generate_file_source() {
local label="$1"
local path="$2"
local job="$3"
local extra_labels="$4"
if [[ "$DRY_RUN" == true ]] || [[ -f "$path" ]] || [[ "$path" == *"*"* ]]; then
cat << EOF
loki.source.file "$label" {
targets = [
{
"__path__" = "$path",
"job" = "$job",
"host" = "$DETECTED_HOSTNAME",${extra_labels}
},
]
forward_to = [loki.write.default.receiver]
}
EOF
else
debug "Skipping $path (file does not exist)"
fi
}
generate_alloy_config() {
log "Generating Alloy config for $OS ($DETECTED_HOSTNAME)..."
local os_label
case "$OS" in
ubuntu|debian) os_label="ubuntu" ;;
rhel|centos|rocky|almalinux|amzn) os_label="rhel-family" ;;
*) os_label="$OS" ;;
esac
local config=""
# Header
config+="// Grafana Alloy Configuration for $DETECTED_HOSTNAME
// Migrated from Promtail on $(date +%Y-%m-%d)
// OS: $OS_PRETTY
// Labels maintained for Promtail dashboard compatibility
logging {
level = \"info\"
}
"
# Journal source
if [[ "$INCLUDE_JOURNAL" == true ]]; then
config+="
// System logs via systemd journal
loki.source.journal \"systemd_journal\" {
max_age = \"12h\"
labels = {
job = \"systemd-journal\",
host = \"$DETECTED_HOSTNAME\",
os = \"$os_label\",
}
forward_to = [loki.relabel.journal_relabel.receiver]
}
loki.relabel \"journal_relabel\" {
forward_to = [loki.write.default.receiver]
rule {
source_labels = [\"__journal__systemd_unit\"]
target_label = \"unit\"
}
rule {
source_labels = [\"__journal_priority\"]
target_label = \"priority\"
}
rule {
source_labels = [\"__journal__hostname\"]
target_label = \"hostname\"
}
}
"
fi
# OS-specific file sources
case "$OS" in
ubuntu|debian)
config+="
// Ubuntu/Debian system logs"
config+=$(generate_file_source "syslog" "/var/log/syslog" "messages" "
\"os\" = \"ubuntu\",")
config+=$(generate_file_source "auth" "/var/log/auth.log" "auth" "
\"log_type\" = \"authentication\",")
config+=$(generate_file_source "kern" "/var/log/kern.log" "kernel" "")
config+=$(generate_file_source "cron" "/var/log/cron.log" "cron" "")
config+=$(generate_file_source "mail" "/var/log/mail.log" "mail" "")
config+=$(generate_file_source "apt" "/var/log/apt/history.log" "packages" "
\"package_manager\" = \"apt\",")
config+=$(generate_file_source "boot" "/var/log/boot.log" "boot" "")
;;
rhel|centos|rocky|almalinux|amzn)
config+="
// RHEL/CentOS/Rocky/Alma/Amazon Linux system logs"
config+=$(generate_file_source "messages" "/var/log/messages" "messages" "
\"os\" = \"rhel-family\",")
config+=$(generate_file_source "secure" "/var/log/secure" "auth" "
\"log_type\" = \"authentication\",")
config+=$(generate_file_source "cron" "/var/log/cron" "cron" "")
config+=$(generate_file_source "maillog" "/var/log/maillog" "mail" "")
config+=$(generate_file_source "yum" "/var/log/yum.log" "packages" "
\"package_manager\" = \"yum\",")
config+=$(generate_file_source "boot" "/var/log/boot.log" "boot" "")
;;
*)
config+="
// Generic system logs"
config+=$(generate_file_source "syslog" "/var/log/syslog" "messages" "")
config+=$(generate_file_source "auth" "/var/log/auth.log" "auth" "
\"log_type\" = \"authentication\",")
;;
esac
# Application wildcard
config+=$(generate_file_source "application_logs" "/var/log/*.log" "application" "")
# Nginx
if [[ "$INCLUDE_NGINX" == true ]]; then
config+="
// Nginx logs"
config+=$(generate_file_source "nginx_access" "/var/log/nginx/access.log" "nginx" "
\"log_type\" = \"access\",")
config+=$(generate_file_source "nginx_error" "/var/log/nginx/error.log" "nginx" "
\"log_type\" = \"error\",")
fi
# Apache
if [[ "$INCLUDE_APACHE" == true ]]; then
config+="
// Apache logs"
config+=$(generate_file_source "apache_access" "/var/log/apache2/access.log" "apache" "
\"log_type\" = \"access\",")
config+=$(generate_file_source "apache_error" "/var/log/apache2/error.log" "apache" "
\"log_type\" = \"error\",")
config+=$(generate_file_source "httpd_access" "/var/log/httpd/access_log" "apache" "
\"log_type\" = \"access\",")
config+=$(generate_file_source "httpd_error" "/var/log/httpd/error_log" "apache" "
\"log_type\" = \"error\",")
fi
# Loki write endpoint
config+="
// Write to Loki
loki.write \"default\" {
endpoint {
url = \"${LOKI_URL}/loki/api/v1/push\"
}
}
"
GENERATED_CONFIG="$config"
}
write_config() {
local output_file="$1"
if [[ "$DRY_RUN" == true ]]; then
log "DRY RUN: Would write config to $output_file"
echo "--- Generated config.alloy ---"
echo "$GENERATED_CONFIG"
echo "--- End config ---"
return
fi
local output_dir
output_dir=$(dirname "$output_file")
mkdir -p "$output_dir"
# Backup existing config
if [[ -f "$output_file" ]]; then
local backup="${output_file}.bak.$(date +%Y%m%d%H%M%S)"
cp "$output_file" "$backup"
log "Backed up existing config to $backup"
fi
echo "$GENERATED_CONFIG" > "$output_file"
log "Alloy config written to $output_file"
}
install_alloy() {
if [[ "$DRY_RUN" == true ]]; then
log "DRY RUN: Would install Grafana Alloy"
return
fi
# Check if already installed
if command -v alloy >/dev/null 2>&1; then
log "Alloy is already installed: $(alloy --version 2>&1 | head -1)"
return
fi
log "Installing Grafana Alloy..."
case "$OS" in
ubuntu|debian)
apt-get install -y apt-transport-https software-properties-common
mkdir -p /etc/apt/keyrings/
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/grafana.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | tee /etc/apt/sources.list.d/grafana.list
apt-get update -qq
apt-get install -y alloy
;;
rhel|centos|rocky|almalinux|amzn)
cat > /etc/yum.repos.d/grafana.repo << 'REPO'
[grafana]
name=grafana
baseurl=https://rpm.grafana.com
repo_gpgcheck=1
enabled=1
gpgcheck=1
gpgkey=https://rpm.grafana.com/gpg.key
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
REPO
if command -v dnf >/dev/null 2>&1; then
dnf install -y alloy
else
yum install -y alloy
fi
;;
*)
error "Unsupported OS for automatic installation: $OS"
error "Install Alloy manually: https://grafana.com/docs/alloy/latest/set-up/install/"
exit 1
;;
esac
log "Alloy installed: $(alloy --version 2>&1 | head -1)"
}
validate_config() {
if [[ "$DRY_RUN" == true ]]; then
log "DRY RUN: Would validate config with 'alloy fmt'"
return
fi
if ! command -v alloy >/dev/null 2>&1; then
warn "Alloy not installed, skipping validation"
return
fi
log "Validating Alloy config..."
if alloy fmt "$ALLOY_CONFIG_FILE" >/dev/null 2>&1; then
log "Config validation passed"
else
error "Config validation failed. Check $ALLOY_CONFIG_FILE for syntax errors"
error "Run: alloy fmt $ALLOY_CONFIG_FILE"
exit 1
fi
}
perform_cutover() {
if [[ "$DRY_RUN" == true ]]; then
log "DRY RUN: Would stop Promtail and start Alloy"
return
fi
# Stop Promtail
if systemctl is-active --quiet promtail 2>/dev/null; then
log "Stopping Promtail..."
systemctl stop promtail
systemctl disable promtail
log "Promtail stopped and disabled"
fi
# Add alloy user to required groups
if getent group adm >/dev/null 2>&1; then
usermod -a -G adm alloy 2>/dev/null || true
fi
if getent group systemd-journal >/dev/null 2>&1; then
usermod -a -G systemd-journal alloy 2>/dev/null || true
fi
# Start Alloy
log "Starting Alloy..."
systemctl enable --now alloy
sleep 2
if systemctl is-active --quiet alloy; then
log "Alloy is running"
else
error "Alloy failed to start. Check: journalctl -u alloy --no-pager -n 30"
error "Rolling back — restarting Promtail"
systemctl enable --now promtail 2>/dev/null || true
exit 1
fi
# Remove Promtail if requested
if [[ "$KEEP_PROMTAIL" == false ]]; then
log "Removing Promtail package..."
case "$OS" in
ubuntu|debian) apt-get remove -y promtail 2>/dev/null || true ;;
*) yum remove -y promtail 2>/dev/null || dnf remove -y promtail 2>/dev/null || true ;;
esac
log "Promtail removed"
else
log "Promtail package kept (use 'systemctl start promtail' to rollback)"
fi
}
print_summary() {
echo ""
echo "=========================================="
echo " Migration Summary"
echo "=========================================="
echo " OS: $OS_PRETTY"
echo " Hostname: $DETECTED_HOSTNAME"
echo " Loki URL: $LOKI_URL"
echo " Alloy config: $ALLOY_CONFIG_FILE"
if [[ "$DRY_RUN" != true ]] && [[ "$GENERATE_ONLY" != true ]]; then
echo ""
echo " Alloy status: $(systemctl is-active alloy 2>/dev/null || echo 'not checked')"
echo ""
echo " Verify:"
echo " systemctl status alloy"
echo " journalctl -u alloy -f"
echo " curl http://localhost:12345 (Alloy UI)"
echo ""
echo " Rollback:"
echo " sudo systemctl stop alloy"
echo " sudo systemctl start promtail"
fi
if [[ "$GENERATE_ONLY" == true ]]; then
echo ""
echo " Config generated. Review and deploy manually."
fi
echo "=========================================="
echo ""
}
main() {
parse_arguments "$@"
log "Promtail → Alloy Migration Script v${SCRIPT_VERSION}"
echo ""
# Check root (unless generate-only)
if [[ "$GENERATE_ONLY" != true ]] && [[ "$DRY_RUN" != true ]] && [[ "$EUID" -ne 0 ]]; then
error "This script must be run as root (or use --generate-only)"
exit 1
fi
detect_os
detect_hostname
detect_loki_url
check_promtail_status
# Generate config
generate_alloy_config
write_config "$ALLOY_CONFIG_FILE"
if [[ "$GENERATE_ONLY" == true ]]; then
print_summary
exit 0
fi
# Install Alloy
if [[ "$SKIP_INSTALL" != true ]]; then
install_alloy
fi
# Validate
validate_config
# Cutover
if [[ "$SKIP_CUTOVER" != true ]]; then
perform_cutover
fi
print_summary
}
main "$@"