From 8185275ba6ff0d9a9a5a6db3f9a856f3b93b78a7 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 3 Sep 2025 13:54:08 +0900 Subject: [PATCH] mattermost table report --- gitea-172.16.10.175.crt | 0 run_table.sh | 38 ++++++++ run_main.sh => run_text.sh | 4 +- src/report_table.py | 168 ++++++++++++++++++++++++++++++++ src/{main.py => report_text.py} | 6 +- 5 files changed, 213 insertions(+), 3 deletions(-) delete mode 100644 gitea-172.16.10.175.crt create mode 100644 run_table.sh rename run_main.sh => run_text.sh (92%) create mode 100644 src/report_table.py rename src/{main.py => report_text.py} (92%) diff --git a/gitea-172.16.10.175.crt b/gitea-172.16.10.175.crt deleted file mode 100644 index e69de29..0000000 diff --git a/run_table.sh b/run_table.sh new file mode 100644 index 0000000..790c23b --- /dev/null +++ b/run_table.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -a +source .env +set +a + +LOG_DIR="logs/table" +mkdir -p "${LOG_DIR}" + +ABSOLUTE_RANGE=7d # 24h +TS="$(date +%Y%m%d_%H%M%S)" + +LOG_FILE="${LOG_DIR}/report_${ABSOLUTE_RANGE}_${TS}.log" + +(cd src && python3 -m report_table \ + --range "${ABSOLUTE_RANGE}") \ + >> "${LOG_FILE}" 2>&1 & + +echo "[OK] Started background job." +echo "[OK] Logging to ${LOG_FILE}" + +ABSOLUTE_RANGE=24h # 24h +TS="$(date +%Y%m%d_%H%M%S)" + +LOG_FILE="${LOG_DIR}/report_${ABSOLUTE_RANGE}_${TS}.log" + +(cd src && python3 -m report_table \ + --range "${ABSOLUTE_RANGE}") \ + >> "${LOG_FILE}" 2>&1 & + +echo "[OK] Started background job." +echo "[OK] Logging to ${LOG_FILE}" + +# # crontab -e +# # 매일 09:00 KST에 지난 24시간 보고 +# 0 9 * * * /usr/bin/env bash -lc 'cd /opt/monitor && /usr/bin/python3 grafana_dash_pull_and_alert.py --range 24h' +# # 매주 월요일 09:30 KST에 지난 7일 보고 +# 30 9 * * 1 /usr/bin/env bash -lc 'cd /opt/monitor && /usr/bin/python3 grafana_dash_pull_and_alert.py --range 7d' \ No newline at end of file diff --git a/run_main.sh b/run_text.sh similarity index 92% rename from run_main.sh rename to run_text.sh index c36147a..f7e8283 100644 --- a/run_main.sh +++ b/run_text.sh @@ -4,7 +4,7 @@ set -a source .env set +a -LOG_DIR="logs" +LOG_DIR="logs/text" mkdir -p "${LOG_DIR}" ABSOLUTE_RANGE=7d # 24h @@ -12,7 +12,7 @@ TS="$(date +%Y%m%d_%H%M%S)" LOG_FILE="${LOG_DIR}/report_${ABSOLUTE_RANGE}_${TS}.log" -(cd src && python3 -m main \ +(cd src && python3 -m report_text \ --range "${ABSOLUTE_RANGE}") \ >> "${LOG_FILE}" 2>&1 & diff --git a/src/report_table.py b/src/report_table.py new file mode 100644 index 0000000..09332be --- /dev/null +++ b/src/report_table.py @@ -0,0 +1,168 @@ +import argparse +import logging +import re +from datetime import timedelta +from typing import Dict, List, Tuple + +from clients.grafana_client import GrafanaClient +from clients.loki import is_loki +from services.dashboard import ( + extract_targets, + flatten_panels, + panel_datasource_resolver, +) +from services.reporter import post_mattermost +from services.summarizer import compute_total_for_target +from setting.config import AppConfig +from utils.timeutils import now_kst, to_epoch + +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--range", choices=["7d", "24h", "1d"], required=True) + return p.parse_args() + + +# ✅ 추가: compute_total_for_target 가 반환한 문자열에서 total 값만 추출 +_TOTAL_RX = re.compile(r"total\s+\*\*([^*]+)\*\*", re.IGNORECASE) + + +def _parse_total_str(line: str) -> str | None: + """ + ' - `legend` (loki) → total **123**' 형태에서 123만 추출. + 실패 시 None + """ + if not line: + return None + m = _TOTAL_RX.search(line) + return m.group(1).strip() if m else None + + +# ✅ 추가: 마크다운 테이블에서 안전하게 보이도록 간단 이스케이프 +def _md_escape(s: str) -> str: + return (s or "").replace("|", r"\|").replace("\n", " ") + + +def main(): + args = parse_args() + cfg = AppConfig.load() # env에서 URL, API KEY, 대시보드 UID, 웹훅 + gf = GrafanaClient(cfg.grafana_url, cfg.grafana_api_key) # Grafana REST 호출용 + + # 시간 범위 설정 + now = now_kst() + if args.range in ("24h", "1d"): + start = now - timedelta(days=1) + range_label = "지난 24시간" + else: + start = now - timedelta(days=7) + range_label = "지난 7일" + + start_epoch, end_epoch = to_epoch(start), to_epoch(now) + # step = step_for_range(end_epoch - start_epoch) + step = 21600 # 6시간 + + # 대시보드에서 패널 추출 + dash = gf.get_dashboard_by_uid(cfg.grafana_dashboard_uid) + panels = flatten_panels(dash) + + skipped: List[str] = [] + all_ds = gf.list_datasources() + + grouped: Dict[str, List[Tuple[str, str]]] = {} + + # 모든 패널 순회 + for p in panels: + title = p.get("title") or "(제목 없음)" + # 패널에 연결된 데이터소스 해석 + ds = panel_datasource_resolver(p, gf.get_datasource_by_uid, lambda: all_ds) + + if not ds or ds.get("id") is None: + skipped.append(f"- `{title}` (데이터소스 없음)") + continue + + # Loki 데이터소스가 아니면 건너뜀 (Prometheus 패널 제외) + if not is_loki(ds): + continue + + # 패널에 정의된 쿼리(target) 추출 (expr + legend) + targets = extract_targets(p) + if not targets: + skipped.append(f"- `{title}` (쿼리 없음)") + continue + + # 각 target 쿼리(expr) 순회 + for t in targets: + expr = t["expr"] + legend = t["legend"] or "" + + try: + # Loki 쿼리를 실행해 total 합계 계산 (문자열) + line = compute_total_for_target( + ds=ds, + ds_id=ds["id"], + legend=legend, + expr=expr, + start_epoch=start_epoch, + end_epoch=end_epoch, + step_sec=step, + # prom_query_range_fn=gf.prom_query_range, + loki_query_range_fn=gf.loki_query_range, + ) + if not line: + continue + + # ✅ total **...** 파싱해서 테이블 행으로 적재 + total_str = _parse_total_str(line) + if total_str is not None: + grouped.setdefault(title, []).append( + (_md_escape(legend or "(no legend)"), total_str) + ) + else: + # total 추출 실패한 경우 스킵 목록에 상세 기록 + skipped.append( + f"- `{title}` / `{legend}`: total 파싱 실패 → {line}" + ) + + except Exception as e: + skipped.append(f"- `{title}` / `{legend}`: 오류 - {e}") + + # ✅ Mattermost 메시지 본문 구성 + lines: List[str] = [] + lines.append( + f"**LLM Gateway Unified Monitoring 요약** \n기간: {range_label} " + f"({start.strftime('%Y-%m-%d %H:%M')} ~ {now.strftime('%Y-%m-%d %H:%M')} KST)" + ) + lines.append("") + + if grouped: + # 발견된 순서를 유지(파이썬 3.7+ dict는 삽입순서 유지) + for panel_title, rows in grouped.items(): + if not rows: + continue + + lines.append(f"**• {panel_title}**\n") + lines.append("| 타겟(legend) | 합계 |") + lines.append("|:--|--:|") + for legend, total_str in rows: + lines.append(f"| {legend} | {total_str} |") + lines.append("") # 섹션 간 공백 + else: + lines.append("_표시할 데이터가 없습니다._") + lines.append("") + + if skipped: + lines.append( + "
건너뛴 항목\n\n" + + "\n".join(skipped) + + "\n\n
" + ) + + post_mattermost(cfg.mattermost_webhook, lines) + logger.info(f"[OK] Sent grouped tables for {len(grouped)} panels.") + + +if __name__ == "__main__": + main() diff --git a/src/main.py b/src/report_text.py similarity index 92% rename from src/main.py rename to src/report_text.py index 43b955f..a475b00 100644 --- a/src/main.py +++ b/src/report_text.py @@ -1,4 +1,5 @@ import argparse +import logging from datetime import timedelta from clients.grafana_client import GrafanaClient @@ -13,6 +14,9 @@ from services.summarizer import compute_total_for_target from setting.config import AppConfig from utils.timeutils import now_kst, to_epoch +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + def parse_args(): p = argparse.ArgumentParser() @@ -116,7 +120,7 @@ def main(): ) post_mattermost(cfg.mattermost_webhook, lines) - print(f"[OK] Sent summary for {counted} panels.") + logger.info(f"[OK] Sent summary for {counted} panels.") if __name__ == "__main__":