Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AI-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@

- `docs/observability.rst` — **אובזרווביליות (Observability)**: המטרות וקהלי היעד, התצורה, בחירת Backend ל-Traces, הגדרת OTLP לסביבות, ואינסטרומנטציה ידנית.
- `docs/observability/background-jobs-monitor.rst` — **Background Jobs Monitor**: פיצ'ר ה-Background Jobs Monitor מספק נראות (Observability) מלאה לכל ה-Jobs הרצים ברקע במערכת, כולל פעולות משתמש דינמיות (Drive, Reminders, Batch Operations).
- `docs/observability/daily-morning-report.rst` — **דוח בוקר יומי**: ה-job שמצליב בין מקורות הניטור הקיימים ושולח סיכום יומי לטלגרם — מה נכנס להודעה, איך נשמר הבסיס להשוואה, ולמה השליחה נבדקת מראש.
- `docs/observability/observability_dashboard.md` — **📡 Observability Dashboard & API**: מסך ה-Admin ב-/admin/observability מרכז נתוני ניטור בזמן אמת ל-SRE ולמפתחים: כרטיסי מצב וגרפים, טבלת התראות עם סינון, ו-API מתועד למסלולי alerts, timeseries, aggregations, export, replay, runbook, quickfix ו-ai_explain.
- `docs/observability/query-performance-profiler.rst` — **Query Performance Profiler**: כלי ניטור לשאילתות MongoDB איטיות: דשבורד ב-WebApp שמציג את השאילתות הכבדות, ה-API שמאחוריו, ומה הכלי במפורש אינו עושה.
- `docs/observability/quick_fix_rules.md` — **🧠 Quick Fix חכם (Queue Delay + עומס/DB) – הנחיות למפתחים ולסוכני AI**: המטרה של Quick Fix היא לתת המלצה קצרה, בטוחה ושימושית על “מה לעשות עכשיו”, לפי אותות שאנחנו כבר מודדים.
Expand Down Expand Up @@ -193,4 +194,4 @@

---

עמודי פיגום autodoc שסוננו: 92. עמודים שנסרקו: 229.
עמודי פיגום autodoc שסוננו: 92. עמודים שנסרקו: 230.
18 changes: 17 additions & 1 deletion alert_forwarder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,23 @@ def forward_alerts(alerts: List[Dict[str, Any]]) -> None:
continue
_post_to_slack(text_slack)
# Send to Telegram only if severity >= configured minimum
if _severity_rank(severity) >= min_tg_rank:
if _severity_rank(severity) < min_tg_rank:
# שער החומרה היה עד כה המקום היחיד בצינור שבו הודעה נעלמת
# בלי שום עקבה — בזמן שהחסימה מרשימת ה-suppress שמתחתיו כן
# פולטת אירוע. הפער הזה הופך תקלת קונפיגורציה (למשל
# ALERT_TELEGRAM_MIN_SEVERITY שהועלה ושכחו ממנו) לשקט שנראה
# בדיוק כמו מערכת בריאה.
try:
emit_event(
"alert_telegram_below_min_severity",
severity="info",
alertname=alert_name,
severity_label=str(severity),
min_severity=str(os.getenv("ALERT_TELEGRAM_MIN_SEVERITY", "info")),
)
except Exception:
pass
else:
if _is_telegram_suppressed(alert_name):
try:
emit_event(
Expand Down
8 changes: 8 additions & 0 deletions config/alerts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ alerts:
severity: critical
cooldown_seconds: 600
message: "⚠️ Job {job_id} תקוע כבר {minutes} דקות"

# job מתוזמן שלא התחיל בכלל. job_failed ו-job_stuck מכסים רק ריצות שכבר
# התחילו, ולכן "לא רץ" היה עד היום שקט מוחלט — בדיוק הכשל שאי אפשר לראות.
- name: job_missed_alert
event_pattern: "job_missed"
severity: error
cooldown_seconds: 3600
message: "🔕 Job {job_id} — לא נרשמה לו ריצה כבר {hours} שעות"
46 changes: 46 additions & 0 deletions database/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1972,6 +1972,39 @@ def _create_profiler_indexes(self, safe_create_index) -> None:
name="slow_queries_coll_dur",
)

def _create_daily_report_indexes(self, safe_create_index) -> None:
"""אינדקס TTL לאוסף שדוח הבוקר היומי שומר בו סנאפשוט אחד ליום.

שם האוסף ומשך השמירה נקראים מהשירות עצמו ולא נכתבים כאן, בדיוק כמו
ב-``_create_profiler_indexes`` — שני מקורות לאותו קבוע היו נסחפים.

**למה 45 יום ולא 7 כמו המקורות:** הסנאפשוט הוא מספרים בלבד והוא
זעיר, אבל הוא צריך לשרוד מעבר לחלון של 30 הימים שבו נשמרים הדפוסים
וסוגי השגיאות המוכרים. TTL זהה למקורות היה מוחק את הבסיס להשוואה
לפני שהוא מספיק לשמש.

``enforce=True`` מאותה סיבה שהוא קיים אצל הפרופיילר: מונגו אינו
מעדכן ``expireAfterSeconds`` של אינדקס קיים בקריאה חוזרת ל-
``create_index`` — הוא מחזיר ``IndexOptionsConflict`` וה-retention
הישן שורד בשקט.

⚠️ זהו אינדקס TTL: הוא מוחק נתונים בפועל, במכוון.
"""
try:
from services.daily_report_service import COLLECTION_NAME, TTL_SECONDS # type: ignore
except Exception:
# השירות אינו זמין (סביבה מינימלית) — אין טעם ליצור אינדקס לאוסף
# שאיש לא כותב אליו.
return

safe_create_index(
COLLECTION_NAME,
[("created_at", ASCENDING)],
name="ttl_cleanup",
expire_after_seconds=int(TTL_SECONDS),
enforce=True,
)

def _create_indexes(self):
"""צור *רק* את האינדקסים הקריטיים (ברקע) למניעת COLLSCAN.

Expand Down Expand Up @@ -2046,12 +2079,25 @@ def safe_create_index(*args: Any, **kwargs: Any) -> None:
name="idx_job_runs_id",
unique=True,
)
# job_runs — שאילתות לפי job_id עם מיון לפי זמן.
# משרת את get_job_history, את "/jobs failed" ב-ChatOps, ואת בדיקת
# "job מתוזמן שלא רץ" ב-_jobs_stuck_monitor. האינדקס הזה כבר מוצהר
# ב-database/job_runs_collection.py, אבל אותו קובץ אינו מיובא בשום
# מקום ולכן אף אינדקס שבו לא נוצר בפועל.
safe_create_index(
"job_runs",
[("job_id", ASCENDING), ("started_at", DESCENDING)],
name="idx_job_runs_job_started",
)

# slow_queries_log - האוסף שהפרופיילר כותב אליו.
# קריאה לא-קשורה (unbound) כמו שאר הקובץ, כדי לתמוך גם ב-self דמה מטסטים
# שאין עליו את המתודה (ראה שים התאימות למעלה).
DatabaseManager._create_profiler_indexes(self, safe_create_index)

# daily_report_snapshots - סנאפשוט יומי של דוח הבוקר (קריאה לא-קשורה, כמו למעלה)
DatabaseManager._create_daily_report_indexes(self, safe_create_index)

# scheduler_jobs - אינדקס לשאילתות polling לפי next_run_time (רץ בתדירות גבוהה)
safe_create_index(
"scheduler_jobs",
Expand Down
18 changes: 18 additions & 0 deletions docs/environment-variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2228,6 +2228,24 @@
- ``scheduler_jobs``
- ``scheduler_jobs_eu``
- Bot
* - ``DISABLE_DAILY_REPORT``
- מכבה את דוח הבוקר היומי. ריק = פעיל.
- לא
- (ריק)
- ``true``
- Bot
* - ``DAILY_REPORT_HOUR_LOCAL``
- שעת שליחת דוח הבוקר בפורמט ``HH:MM``, בשעון ישראל. ערך פגום חוזר ל-08:00 ונרשם כאזהרה.
- לא
- ``08:00``
- ``07:30``
- Bot
* - ``DAILY_REPORT_CORRELATION_WINDOW_MINUTES``
- רוחב חלון ההצלבה בין התראה לשאילתה איטית, בדקות. נחסם לטווח 1–120.
- לא
- ``5``
- ``10``
- Bot
* - ``WEEKLY_REPORT_DELAY_SECS``
- כמה זמן להמתין אחרי עלייה לפני הפעלת job הדו"חות השבועיים.
- לא
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ Code Keeper Bot - תיעוד API

observability
observability/background-jobs-monitor
observability/daily-morning-report
observability/observability_dashboard
observability/query-performance-profiler
observability/quick_fix_rules
Expand Down
42 changes: 41 additions & 1 deletion docs/observability/background-jobs-monitor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Background Jobs Monitor
- תבנית הדשבורד
* - ``chatops/jobs_commands.py``
- פקודת ``/jobs`` בטלגרם
* - ``services/daily_report_service.py``
- דוח הבוקר היומי: קריאת המקורות, השוואה מול אתמול ורינדור
* - ``config/alerts.yml``
- הגדרות Alerts ל-Jobs

Expand Down Expand Up @@ -79,6 +81,22 @@ Background Jobs Monitor
{"keys": [("user_id", 1), ("job_id", 1)], "sparse": True},
]

קולקציה: ``daily_report_snapshots``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

דוח הבוקר היומי שומר שורה אחת ליום — מספרים בלבד, בלי טקסט ההודעה — כדי
שיהיה לו מול מה להשוות מחר. ``_id`` הוא התאריך המקומי שהיום מתאר
(``YYYY-MM-DD``), ולכן ההשוואה יכולה לדרוש את D-1 במפורש במקום ליפול על
"השורה האחרונה שיש".

הכתיבה היא ``$setOnInsert``: הכתיבה הראשונה ליום מנצחת, ולכן טריגר ידני
שרץ במקביל לריצה המתוזמנת אינו דורס סנאפשוט תקין.

ה-TTL כאן **ארוך מזה של המקורות** (``slow_queries_log`` — שבעה ימים), כי
הסנאפשוט מחזיק גם זיכרון של דפוסי שאילתה וסוגי שגיאות מוכרים לחלון של 30
יום. שמו של האוסף ומשך השמירה נקראים מ-``services/daily_report_service.py``
ואינם כתובים שוב ב-``database/manager.py``.

UI / WebApp
-----------

Expand Down Expand Up @@ -309,6 +327,12 @@ Alerts
cooldown_seconds: 600
message: "⚠️ Job {job_id} תקוע כבר {minutes} דקות"

- name: job_missed_alert
event_pattern: "job_missed"
severity: error
cooldown_seconds: 3600
message: "🔕 Job {job_id} — לא נרשמה לו ריצה כבר {hours} שעות"

Flow של Alerts
~~~~~~~~~~~~~~

Expand All @@ -317,13 +341,29 @@ Flow של Alerts
3. בדיקה מול ``alerts.yml`` ו-cooldown
4. שליחת התראה לערוץ המוגדר (Telegram/Slack)

**זיהוי Job שלא רץ בכלל:**

``job_failed`` ו-``job_stuck`` מכסים רק הרצות ש**התחילו**. Job שהתזמון שלו
נעלם — משתנה סביבה שנמחק, חריגה בעלייה, restart שבלע את התזמון — היה שקט
מוחלט, וזה נראה בדיוק כמו מערכת בריאה.

Job שמצהיר ב-``metadata`` על ``missed_after_hours`` נבדק באותו background loop
של ה-stuck monitor: שאילתה מצרפית אחת על ``job_runs`` מחזירה מי כן רץ בחלון
(``completed`` או ``failed`` — כשל כבר מכוסה, השאלה כאן היא אי-התחלה), והחסרים
מפיקים ``job_missed``. ההתראה נשלחת פעם אחת ליום לכל Job — השער הוא upsert מותנה על ``admin_reports``, ו"היום כבר נתפס" מגיע ממנו כהתנגשות מפתח ייחודי (``DuplicateKeyError``) ולא כ-``modified_count=0``, כי אופרטור ה-``$ne`` שבשאילתה אינו נכנס למסמך שנוצר ב-upsert.

מה שנבדק הוא היעדר **רשומה** ב-``job_runs``, לא היעדר ריצה: ``JobTracker._persist_run`` רושם כשל כתיבה ללוג ואינו מפיל את ההרצה, ולכן ריצה שקרתה ורשומתה לא נשמרה תיראה כאן כהיעדר. שתי הבדיקות — ה-stuck וה-missed — רצות באותו loop אבל ב-``try`` נפרד: כשל של אחת אינו משתיק את השנייה.

**זיהוי Jobs תקועים:**

מתבצע ב-background loop (כל ``JOBS_STUCK_MONITOR_INTERVAL_SECS`` שניות):

1. סריקת ``job_runs`` עם ``status=running``
2. אם ``started_at`` לפני יותר מ-``JOBS_STUCK_THRESHOLD_MINUTES`` דקות
3. פליטת ``emit_event("job_stuck", ...)``
3. סימון ``stuck_reported_at`` בכתיבה מותנה (``{"run_id": ..., "stuck_reported_at": {"$exists": false}}``)
4. פליטת ``emit_event("job_stuck", ...)`` — **רק אם הסימון באמת שינה מסמך**

הצעד הרביעי אינו קוסמטי. הסריקה והסימון הם check-then-act: שני תהליכים שראו את אותה הרצה בסריקה יגיעו שניהם לכתיבה, ורק אצל אחד ``stuck_reported_at`` ייכתב בפועל. ``modified_count=0`` פירושו "מישהו אחר כבר דיווח", ובלי בדיקת ה-rowcount הסימון היה שדה לוואי במקום שער — ההתראה נפלטה פעמיים. כשל **כתיבה** (חריגה) נשאר fail-open ופולט בכל זאת, כי לא ידוע אם סימנו, והרצה תקועה שאיש אינו יודע עליה גרועה מהתראה כפולה.

Troubleshooting
---------------
Expand Down
Loading
Loading