Загрузка данных
#!/usr/bin/env python3
"""
opensearch_index_stats.py
Получает список индексов из OpenSearch (через _cat/indices), считает общий
размер хранилища и раскладывает объём данных по дням.
День для каждого индекса определяется так:
1) по дате в имени индекса (напр. logs-2026.07.15, app-2026-07-15, ...);
2) если даты в имени нет — по дате создания индекса (creation.date).
Зависимостей нет — только стандартная библиотека Python 3.
Примеры:
export OS_URL=https://opensearch.example.com:9200
export OS_USER=admin
export OS_PASS=secret
python3 opensearch_index_stats.py # все индексы
python3 opensearch_index_stats.py --pattern 'logs-*'
python3 opensearch_index_stats.py --insecure # не проверять TLS-серт
python3 opensearch_index_stats.py --json # машиночитаемый вывод
"""
import argparse
import base64
import datetime as dt
import json
import os
import re
import ssl
import sys
import urllib.error
import urllib.request
from collections import defaultdict
# --- парсинг даты из имени индекса -------------------------------------------
# ловит YYYY.MM.DD / YYYY-MM-DD / YYYY_MM_DD / YYYYMMDD в конце или в теле имени
DATE_RE = re.compile(r"(\d{4})[.\-_]?(\d{2})[.\-_]?(\d{2})")
def parse_args():
p = argparse.ArgumentParser(description="OpenSearch: размер индексов и объём данных за день")
p.add_argument("--url", default=os.environ.get("OS_URL", "http://localhost:9200"),
help="базовый URL OpenSearch (env OS_URL)")
p.add_argument("--user", default=os.environ.get("OS_USER"),
help="имя пользователя (env OS_USER)")
p.add_argument("--password", default=os.environ.get("OS_PASS"),
help="пароль (env OS_PASS)")
p.add_argument("--pattern", default="*",
help="шаблон индексов, напр. 'logs-*' (по умолчанию все, кроме системных)")
p.add_argument("--include-system", action="store_true",
help="включить системные индексы (начинающиеся с точки)")
p.add_argument("--insecure", action="store_true",
help="не проверять TLS-сертификат")
p.add_argument("--top", type=int, default=15,
help="сколько дней показать в разбивке по дням (0 = все)")
p.add_argument("--json", action="store_true",
help="вывести результат в JSON")
p.add_argument("--timeout", type=float, default=30.0, help="таймаут запроса, сек")
return p.parse_args()
def http_get(url, user, password, insecure, timeout):
req = urllib.request.Request(url)
if user is not None:
token = base64.b64encode(f"{user}:{password or ''}".encode()).decode()
req.add_header("Authorization", f"Basic {token}")
ctx = None
if url.lower().startswith("https"):
ctx = ssl.create_default_context()
if insecure:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as r:
return json.loads(r.read().decode())
except urllib.error.HTTPError as e:
sys.exit(f"HTTP {e.code} при запросе {url}: {e.read().decode(errors='replace')[:300]}")
except urllib.error.URLError as e:
sys.exit(f"Не удалось подключиться к {url}: {e.reason}")
def fetch_indices(args):
# bytes=b -> размеры в байтах; h=... -> нужные колонки
cols = "index,docs.count,store.size,pri.store.size,creation.date"
url = (f"{args.url.rstrip('/')}/_cat/indices/{args.pattern}"
f"?format=json&bytes=b&h={cols}")
return http_get(url, args.user, args.password, args.insecure, args.timeout)
def day_of_index(name, creation_epoch_ms):
"""Определить день индекса: сначала из имени, затем из даты создания."""
m = DATE_RE.search(name)
if m:
y, mo, d = map(int, m.groups())
try:
return dt.date(y, mo, d).isoformat(), "name"
except ValueError:
pass
if creation_epoch_ms:
day = dt.datetime.utcfromtimestamp(int(creation_epoch_ms) / 1000).date()
return day.isoformat(), "created"
return "unknown", "none"
def human(nbytes):
n = float(nbytes)
for unit in ("B", "KiB", "MiB", "GiB", "TiB", "PiB"):
if abs(n) < 1024.0 or unit == "PiB":
return f"{n:,.2f} {unit}".replace(",", " ")
n /= 1024.0
def main():
args = parse_args()
rows = fetch_indices(args)
total_size = 0
total_docs = 0
by_day_size = defaultdict(int)
by_day_docs = defaultdict(int)
by_day_count = defaultdict(int)
src_stats = defaultdict(int) # как определён день: name/created/none
n_indices = 0
for r in rows:
name = r.get("index", "")
if not args.include_system and name.startswith("."):
continue
n_indices += 1
size = int(r.get("store.size") or 0)
docs = int(r.get("docs.count") or 0)
total_size += size
total_docs += docs
day, src = day_of_index(name, r.get("creation.date"))
src_stats[src] += 1
by_day_size[day] += size
by_day_docs[day] += docs
by_day_count[day] += 1
# дни с известной датой (для средних)
dated = {d: s for d, s in by_day_size.items() if d != "unknown"}
n_days = len(dated)
avg_per_day = (sum(dated.values()) / n_days) if n_days else 0
avg_docs_per_day = (sum(by_day_docs[d] for d in dated) / n_days) if n_days else 0
if args.json:
out = {
"indices": n_indices,
"total_size_bytes": total_size,
"total_size_human": human(total_size),
"total_docs": total_docs,
"days_with_date": n_days,
"avg_size_per_day_bytes": round(avg_per_day),
"avg_size_per_day_human": human(avg_per_day),
"avg_docs_per_day": round(avg_docs_per_day),
"by_day": {
d: {
"size_bytes": by_day_size[d],
"size_human": human(by_day_size[d]),
"docs": by_day_docs[d],
"indices": by_day_count[d],
} for d in sorted(by_day_size)
},
}
print(json.dumps(out, ensure_ascii=False, indent=2))
return
# --- человекочитаемый отчёт ---
print(f"OpenSearch: {args.url} шаблон: {args.pattern}")
print("=" * 64)
print(f"Индексов: {n_indices}")
print(f"Общий размер: {human(total_size)} ({total_size:,} B)".replace(",", " "))
print(f"Всего документов: {total_docs:,}".replace(",", " "))
print(f"Дней с известной датой: {n_days}")
print(f"Средний объём в день: {human(avg_per_day)}")
print(f"Средне документов/день: {avg_docs_per_day:,.0f}".replace(",", " "))
print(f"Определение дня: из имени={src_stats['name']}, "
f"по created={src_stats['created']}, не определено={src_stats['none']}")
print("=" * 64)
days_sorted = sorted((d for d in by_day_size), reverse=True)
shown = days_sorted if args.top == 0 else days_sorted[: args.top]
print(f"Разбивка по дням (последние {len(shown)}):")
print(f"{'дата':<12} {'размер':>14} {'документы':>16} {'индексов':>9}")
print("-" * 64)
for d in shown:
print(f"{d:<12} {human(by_day_size[d]):>14} "
f"{by_day_docs[d]:>16,} {by_day_count[d]:>9}".replace(",", " "))
if args.top and len(days_sorted) > args.top:
print(f"... ещё {len(days_sorted) - args.top} дн. (используй --top 0 для полного списка)")
if __name__ == "__main__":
main()