311 lines
9.7 KiB
Python
311 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Installer & updater for Grafana Loki (Debian 13 / trixie).
|
|
Default run uses:
|
|
--deb-url latest from GitHub
|
|
--config /etc/loki/custom-config.yml
|
|
--data-dir /data/loki
|
|
|
|
--force: wykonuje wszystkie kroki od nowa i nadpisuje pliki/katalogi/konfigi.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import urllib.request
|
|
from urllib.error import URLError, HTTPError
|
|
|
|
GITHUB_LATEST_API = "https://api.github.com/repos/grafana/loki/releases/latest"
|
|
|
|
PKG_NAME = "loki"
|
|
BIN_PATH = "/usr/bin/loki"
|
|
|
|
SYSTEMD_UNIT = "/lib/systemd/system/loki.service"
|
|
SYSTEMD_OVERRIDE_DIR = "/etc/systemd/system/loki.service.d"
|
|
SYSTEMD_OVERRIDE_PATH = os.path.join(SYSTEMD_OVERRIDE_DIR, "override.conf")
|
|
|
|
DEFAULT_USER = "loki"
|
|
DEFAULT_GROUP = "loki"
|
|
DEFAULT_CONFIG_DIR = "/etc/loki"
|
|
DEFAULT_CONFIG_PATH = os.path.join(DEFAULT_CONFIG_DIR, "custom-config.yml")
|
|
DEFAULT_DATA_DIR = "/data/loki"
|
|
DEFAULT_LOG_DIR = "/var/log/loki"
|
|
|
|
CUSTOM_CONFIG = """
|
|
auth_enabled: false
|
|
|
|
server:
|
|
http_listen_port: 3100
|
|
grpc_listen_port: 9096
|
|
log_level: debug
|
|
grpc_server_max_concurrent_streams: 1000
|
|
|
|
common:
|
|
instance_addr: 0.0.0.0
|
|
path_prefix: /data/loki
|
|
storage:
|
|
filesystem:
|
|
chunks_directory: /data/loki/chunks
|
|
rules_directory: /data/loki/rules
|
|
replication_factor: 1
|
|
ring:
|
|
kvstore:
|
|
store: inmemory
|
|
|
|
query_range:
|
|
results_cache:
|
|
cache:
|
|
embedded_cache:
|
|
enabled: true
|
|
max_size_mb: 100
|
|
|
|
schema_config:
|
|
configs:
|
|
- from: 2020-10-24
|
|
store: tsdb
|
|
object_store: filesystem
|
|
schema: v13
|
|
index:
|
|
prefix: index_
|
|
period: 24h
|
|
|
|
storage_config:
|
|
filesystem:
|
|
directory: /data/loki/db
|
|
|
|
pattern_ingester:
|
|
enabled: true
|
|
metric_aggregation:
|
|
loki_address: 0.0.0.0:3100
|
|
|
|
ruler:
|
|
alertmanager_url: http://10.87.2.22:9093
|
|
|
|
frontend:
|
|
encoding: protobuf
|
|
|
|
query_scheduler:
|
|
max_outstanding_requests_per_tenant: 2048
|
|
|
|
limits_config:
|
|
ingestion_rate_mb: 1024
|
|
ingestion_burst_size_mb: 1024
|
|
max_query_lookback: 168h
|
|
max_query_series: 10000
|
|
retention_period: 336h
|
|
|
|
compactor:
|
|
working_directory: /data/loki/compactor
|
|
compaction_interval: 10m
|
|
retention_enabled: true
|
|
retention_delete_delay: 2h
|
|
retention_delete_worker_count: 150
|
|
delete_request_store: filesystem
|
|
"""
|
|
|
|
# ---------------- helpers ----------------
|
|
|
|
def log(msg: str, quiet: bool):
|
|
if not quiet:
|
|
print(msg)
|
|
|
|
def run(cmd, check=True, quiet=False):
|
|
p = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
if check and p.returncode != 0:
|
|
if quiet:
|
|
sys.stderr.write(p.stderr or p.stdout)
|
|
else:
|
|
print(p.stdout)
|
|
print(p.stderr, file=sys.stderr)
|
|
raise SystemExit(p.returncode)
|
|
return p.returncode, p.stdout.strip(), p.stderr.strip()
|
|
|
|
def ensure_user(user=DEFAULT_USER, group=DEFAULT_GROUP, quiet=False):
|
|
code, _, _ = run(f"getent group {group}", check=False, quiet=quiet)
|
|
if code != 0:
|
|
log(f"Creating group: {group}", quiet)
|
|
run(f"groupadd --system {group}")
|
|
code, _, _ = run(f"id -u {user}", check=False, quiet=quiet)
|
|
if code != 0:
|
|
log(f"Creating user: {user}", quiet)
|
|
run(
|
|
"useradd --system --no-create-home --shell /usr/sbin/nologin "
|
|
f"--gid {group} {user}"
|
|
)
|
|
|
|
def ensure_dirs(paths, owner=(DEFAULT_USER, DEFAULT_GROUP), mode=0o750, quiet=False, force=False):
|
|
for p in paths:
|
|
if force and os.path.isdir(p):
|
|
shutil.rmtree(p)
|
|
if not os.path.isdir(p):
|
|
log(f"Creating dir: {p}", quiet)
|
|
os.makedirs(p, exist_ok=True)
|
|
os.chmod(p, mode)
|
|
run(f"chown -R {owner[0]}:{owner[1]} {p}")
|
|
|
|
def write_config(path, content, mode=0o640, owner=(DEFAULT_USER, DEFAULT_GROUP), quiet=False, force=False):
|
|
if force or not os.path.exists(path):
|
|
base = os.path.dirname(path)
|
|
if base and not os.path.isdir(base):
|
|
os.makedirs(base, exist_ok=True)
|
|
log(f"Writing config: {path}", quiet)
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(content)
|
|
os.chmod(path, mode)
|
|
run(f"chown {owner[0]}:{owner[1]} {path}")
|
|
|
|
def download(url, dest, quiet=False):
|
|
log(f"Downloading: {url}", quiet)
|
|
req = urllib.request.Request(url, headers={"User-Agent": "loki-installer"})
|
|
with urllib.request.urlopen(req) as r, open(dest, "wb") as f:
|
|
shutil.copyfileobj(r, f)
|
|
|
|
def get_installed_version(quiet=False):
|
|
code, out, _ = run(f"dpkg-query -W -f='${{Version}}\n' {PKG_NAME}", check=False, quiet=quiet)
|
|
return out.strip() if code == 0 else None
|
|
|
|
def semver_tuple(v: str):
|
|
m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", v)
|
|
if not m:
|
|
return (0, 0, 0)
|
|
parts = [int(x) if x else 0 for x in m.groups()]
|
|
while len(parts) < 3:
|
|
parts.append(0)
|
|
return tuple(parts)
|
|
|
|
def find_latest_loki_deb_asset(quiet=False):
|
|
req = urllib.request.Request(GITHUB_LATEST_API, headers={"User-Agent": "loki-installer", "Accept": "application/vnd.github+json"})
|
|
with urllib.request.urlopen(req) as r:
|
|
data = json.loads(r.read().decode("utf-8"))
|
|
for a in data.get("assets", []):
|
|
name = a.get("name", "")
|
|
url = a.get("browser_download_url")
|
|
if name.startswith("loki_") and name.endswith("_amd64.deb"):
|
|
ver = name.split("_")[1]
|
|
return ver, url
|
|
return None, None
|
|
|
|
def install_deb_from_url(url, quiet=False, force=False):
|
|
with tempfile.TemporaryDirectory() as td:
|
|
deb_path = os.path.join(td, os.path.basename(url) or "loki.deb")
|
|
download(url, deb_path, quiet=quiet)
|
|
log("Installing package…", quiet)
|
|
run("apt-get update", check=False, quiet=quiet)
|
|
run(f"DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\" -y install {deb_path}")
|
|
log("Installed loki", quiet)
|
|
|
|
def ensure_systemd(config_path=DEFAULT_CONFIG_PATH, user=DEFAULT_USER, group=DEFAULT_GROUP, quiet=False, force=False):
|
|
unit_path = "/etc/systemd/system/loki.service" if not os.path.isfile(SYSTEMD_UNIT) else None
|
|
if force and os.path.exists("/etc/systemd/system/loki.service"):
|
|
os.remove("/etc/systemd/system/loki.service")
|
|
if force and os.path.exists(SYSTEMD_OVERRIDE_PATH):
|
|
os.remove(SYSTEMD_OVERRIDE_PATH)
|
|
if unit_path:
|
|
unit = f"""
|
|
[Unit]
|
|
Description=Grafana Loki
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart={BIN_PATH} -config.file={config_path}
|
|
User={user}
|
|
Group={group}
|
|
Restart=always
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
"""
|
|
with open(unit_path, "w", encoding="utf-8") as f:
|
|
f.write(unit.lstrip())
|
|
else:
|
|
os.makedirs(SYSTEMD_OVERRIDE_DIR, exist_ok=True)
|
|
override = f"""
|
|
[Service]
|
|
User={user}
|
|
Group={group}
|
|
ExecStart=
|
|
ExecStart={BIN_PATH} -config.file={config_path}
|
|
"""
|
|
with open(SYSTEMD_OVERRIDE_PATH, "w", encoding="utf-8") as f:
|
|
f.write(override.lstrip())
|
|
|
|
run("systemctl daemon-reload")
|
|
run("systemctl enable --now loki")
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description="Install/Update Grafana Loki (Debian 13)")
|
|
p.add_argument("--deb-url", default="latest", help="URL to loki amd64 .deb or 'latest'")
|
|
p.add_argument("--update", action="store_true", help="Silent update from GitHub")
|
|
p.add_argument("--force", action="store_true", help="Redo all steps, overwrite config and dirs")
|
|
p.add_argument("--user", default=DEFAULT_USER, help="Service user")
|
|
p.add_argument("--group", default=DEFAULT_GROUP, help="Service group")
|
|
p.add_argument("--config", default=DEFAULT_CONFIG_PATH, help="Path to Loki config")
|
|
p.add_argument("--data-dir", default=DEFAULT_DATA_DIR, help="Loki data dir")
|
|
p.add_argument("--log-dir", default=DEFAULT_LOG_DIR, help="Loki log dir")
|
|
p.add_argument("--quiet", action="store_true", help="Less output")
|
|
args = p.parse_args()
|
|
|
|
quiet = args.quiet or args.update
|
|
|
|
if os.geteuid() != 0:
|
|
print("Run as root.", file=sys.stderr)
|
|
return 1
|
|
|
|
ensure_user(args.user, args.group, quiet=quiet)
|
|
|
|
ensure_dirs([
|
|
os.path.dirname(args.config) or DEFAULT_CONFIG_DIR,
|
|
args.data_dir,
|
|
os.path.join(args.data_dir, "chunks"),
|
|
os.path.join(args.data_dir, "rules"),
|
|
os.path.join(args.data_dir, "db"),
|
|
os.path.join(args.data_dir, "compactor"),
|
|
args.log_dir,
|
|
], owner=(args.user, args.group), quiet=quiet, force=args.force)
|
|
|
|
write_config(args.config, CUSTOM_CONFIG, owner=(args.user, args.group), quiet=quiet, force=args.force)
|
|
|
|
installed = get_installed_version(quiet=quiet)
|
|
deb_url = args.deb_url
|
|
if deb_url == "latest":
|
|
try:
|
|
latest_ver, latest_url = find_latest_loki_deb_asset(quiet=quiet)
|
|
except (URLError, HTTPError):
|
|
return 0
|
|
if not latest_ver or not latest_url:
|
|
return 0
|
|
deb_url = latest_url
|
|
if args.update or args.force:
|
|
try:
|
|
latest_ver, latest_url = find_latest_loki_deb_asset(quiet=quiet)
|
|
except (URLError, HTTPError):
|
|
return 0
|
|
if not latest_ver or not latest_url:
|
|
return 0
|
|
if args.force or not installed or semver_tuple(installed) < semver_tuple(latest_ver):
|
|
try:
|
|
install_deb_from_url(latest_url, quiet=True, force=args.force)
|
|
except SystemExit:
|
|
return 1
|
|
else:
|
|
if not installed:
|
|
install_deb_from_url(deb_url, quiet=quiet)
|
|
|
|
ensure_systemd(config_path=args.config, user=args.user, group=args.group, quiet=quiet, force=args.force)
|
|
|
|
run(f"chown -R {args.user}:{args.group} {os.path.dirname(args.config)} {args.data_dir} {args.log_dir}")
|
|
|
|
log("OK.", quiet)
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|