Загрузка данных
#!/usr/bin/env python3
"""
Подсчёт размера индексов OpenSearch по паттерну.
Примеры:
./os_index_size.py 'logs-*'
./os_index_size.py '*' --host https://os.local:9200 -u admin -p secret -k --all
./os_index_size.py 'app-*,audit-*' --sort size --top 20
./os_index_size.py '*' --debug
"""
import argparse
import base64
import json
import os
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
DEBUG = False
def log(msg):
if DEBUG:
print(f"[debug] {msg}", file=sys.stderr)
def human(n: int) -> str:
size = float(n)
for unit in ("B", "KB", "MB", "GB", "TB", "PB"):
if size < 1024 or unit == "PB":
return f"{size:.2f} {unit}"
size /= 1024
def to_int(v) -> int:
try:
return int(v)
except (TypeError, ValueError):
return 0 # закрытые индексы возвращают null
class Client:
def __init__(self, args):
self.host = args.host.rstrip("/")
self.timeout = args.timeout
self.auth = None
if args.user:
self.auth = base64.b64encode(f"{args.user}:{args.password or ''}".encode()).decode()
self.ctx = ssl.create_default_context(cafile=args.cacert) if args.cacert else ssl.create_default_context()
if args.insecure:
self.ctx.check_hostname = False
self.ctx.verify_mode = ssl.CERT_NONE
def get(self, path, params=None):
url = f"{self.host}{path}"
if params:
url += "?" + urllib.parse.urlencode(params)
log(f"GET {url}")
req = urllib.request.Request(url)
if self.auth:
req.add_header("Authorization", f"Basic {self.auth}")
try:
with urllib.request.urlopen(req, context=self.ctx, timeout=self.timeout) as resp:
body = resp.read().decode(errors="replace")
log(f"HTTP {resp.status}, {len(body)} байт: {body[:500]}")
return resp.status, body
except urllib.error.HTTPError as e:
body = e.read().decode(errors="replace")
log(f"HTTP {e.code}: {body[:500]}")
return e.code, body
except urllib.error.URLError as e:
sys.exit(f"Ошибка подключения к {self.host}: {e.reason}")
def parse_json(status, body, what):
try:
return json.loads(body)
except json.JSONDecodeError:
sys.exit(f"{what}: ответ не JSON (HTTP {status}). Проверьте --host — "
f"возможно, это Dashboards/прокси, а не REST API.\nНачало ответа: {body[:300]}")
def fetch_indices(client, pattern, expand):
status, body = client.get(
f"/_cat/indices/{urllib.parse.quote(pattern, safe='*,-')}",
{"format": "json", "bytes": "b", "expand_wildcards": expand,
"h": "index,health,status,pri,rep,docs.count,store.size,pri.store.size"},
)
if status == 401:
sys.exit("HTTP 401: неверный логин/пароль или не переданы -u/-p")
if status == 403:
sys.exit(f"HTTP 403: у пользователя нет прав на _cat/indices.\n{body[:500]}")
if status == 404:
data = parse_json(status, body, "_cat/indices")
err_type = (data.get("error") or {}).get("type") if isinstance(data, dict) else None
if err_type == "index_not_found_exception":
return []
sys.exit(f"HTTP 404, но это не 'индекс не найден' — вероятно, неверный URL.\n{body[:500]}")
if status != 200:
sys.exit(f"HTTP {status}: {body[:500]}")
data = parse_json(status, body, "_cat/indices")
if not isinstance(data, list):
sys.exit(f"Неожиданный формат ответа: {body[:500]}")
return data
def diagnose(client, args):
"""Вызывается, если по паттерну пусто: пытаемся понять почему."""
print(f"Индексы по паттерну '{args.pattern}' не найдены. Диагностика:", file=sys.stderr)
status, body = client.get("/")
if status == 200:
info = parse_json(status, body, "/")
ver = info.get("version", {})
print(f" • Кластер: {info.get('cluster_name')}, "
f"{ver.get('distribution', 'elasticsearch')} {ver.get('number')}", file=sys.stderr)
else:
print(f" • GET / вернул HTTP {status}", file=sys.stderr)
if not args.all:
hidden = fetch_indices(client, args.pattern, "all")
if hidden:
names = ", ".join(r["index"] for r in hidden[:5])
print(f" • С учётом закрытых/скрытых индексов найдено {len(hidden)} "
f"(напр. {names}). Запустите с --all", file=sys.stderr)
return
status, body = client.get("/_plugins/_security/authinfo")
if status == 200:
info = parse_json(status, body, "authinfo")
print(f" • Пользователь: {info.get('user_name')}, роли: {info.get('roles')}", file=sys.stderr)
print(" • Если индексы точно есть — у этих ролей, скорее всего, нет прав "
"indices:monitor/stats на нужные индексы (security plugin скрывает недоступные)",
file=sys.stderr)
print(" • Для подробностей запустите с --debug", file=sys.stderr)
def main():
global DEBUG
p = argparse.ArgumentParser(description="Размер индексов OpenSearch по паттерну")
p.add_argument("pattern", help="Паттерн индексов, напр. 'logs-*' или 'a-*,b-*,-b-old*'")
p.add_argument("--host", default=os.getenv("OS_HOST", "https://localhost:9200"))
p.add_argument("-u", "--user", default=os.getenv("OS_USER"))
p.add_argument("-p", "--password", default=os.getenv("OS_PASS"))
p.add_argument("--cacert", help="Путь к CA-сертификату")
p.add_argument("-k", "--insecure", action="store_true", help="Не проверять TLS-сертификат")
p.add_argument("--all", action="store_true", help="Включать закрытые и скрытые (.*) индексы")
p.add_argument("--sort", choices=["name", "size", "docs"], default="size")
p.add_argument("--top", type=int, default=0, help="Показать только N первых")
p.add_argument("--json", action="store_true", help="Вывод в JSON")
p.add_argument("--timeout", type=int, default=30)
p.add_argument("--debug", action="store_true", help="Печатать запросы и ответы")
args = p.parse_args()
DEBUG = args.debug
client = Client(args)
rows = fetch_indices(client, args.pattern, "all" if args.all else "open")
if not rows:
diagnose(client, args)
sys.exit(1)
for r in rows:
r["_size"] = to_int(r.get("store.size"))
r["_pri"] = to_int(r.get("pri.store.size"))
r["_docs"] = to_int(r.get("docs.count"))
key = {"name": lambda r: r["index"], "size": lambda r: r["_size"], "docs": lambda r: r["_docs"]}[args.sort]
rows.sort(key=key, reverse=args.sort != "name")
total_size = sum(r["_size"] for r in rows)
total_pri = sum(r["_pri"] for r in rows)
total_docs = sum(r["_docs"] for r in rows)
shown = rows[: args.top] if args.top else rows
if args.json:
print(json.dumps({
"pattern": args.pattern,
"count": len(rows),
"total_store_bytes": total_size,
"total_primary_bytes": total_pri,
"total_docs": total_docs,
"indices": [
{"index": r["index"], "health": r.get("health"), "status": r.get("status"),
"store_bytes": r["_size"], "primary_bytes": r["_pri"], "docs": r["_docs"]}
for r in shown
],
}, indent=2, ensure_ascii=False))
return
w = max(len("INDEX"), *(len(r["index"]) for r in shown))
print(f"{'INDEX':<{w}} {'HEALTH':<7} {'STATUS':<6} {'DOCS':>14} {'PRIMARY':>12} {'TOTAL':>12}")
for r in shown:
print(f"{r['index']:<{w}} {str(r.get('health')):<7} {str(r.get('status')):<6} "
f"{r['_docs']:>14,} {human(r['_pri']):>12} {human(r['_size']):>12}")
print("-" * (w + 58))
if args.top and args.top < len(rows):
print(f"(показано {args.top} из {len(rows)})")
print(f"Индексов: {len(rows)}")
print(f"Документов: {total_docs:,}")
print(f"Размер primary-шардов: {human(total_pri)} ({total_pri} B)")
print(f"Полный размер (с репликами): {human(total_size)} ({total_size} B)")
if __name__ == "__main__":
main()