Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
140 lines
4.4 KiB
Python
140 lines
4.4 KiB
Python
"""
|
|
alert.py — Shared alerting module for Performance West monitor scripts.
|
|
Creates an ERPNext Issue when a posting account is broken or automation fails.
|
|
Import and call: alert_account_broken(monitor, platform, error)
|
|
"""
|
|
|
|
import json
|
|
import urllib.request
|
|
import urllib.parse
|
|
import os
|
|
from pathlib import Path
|
|
|
|
# ERPNext config — reads from env
|
|
ERPNEXT_URL = os.environ.get("ERPNEXT_URL", "http://erpnext:8080")
|
|
ERPNEXT_API_KEY = os.environ.get("ERPNEXT_API_KEY", "")
|
|
ERPNEXT_API_SECRET = os.environ.get("ERPNEXT_API_SECRET", "")
|
|
|
|
# Fallback: use Express API to create issues if ERPNext is unreachable
|
|
API_URL = os.environ.get("PW_API_URL", "http://api:3001")
|
|
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")
|
|
|
|
# Only create one alert per monitor+platform per day
|
|
_ALERT_STATE_FILE = Path.home() / ".monitor-alert-state.json"
|
|
|
|
|
|
def _load_alert_state():
|
|
if _ALERT_STATE_FILE.exists():
|
|
try:
|
|
return json.loads(_ALERT_STATE_FILE.read_text())
|
|
except Exception:
|
|
pass
|
|
return {}
|
|
|
|
|
|
def _save_alert_state(state):
|
|
_ALERT_STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
|
|
|
|
def alert_account_broken(monitor: str, platform: str, error: str, detail: str = ""):
|
|
"""
|
|
Create an ERPNext Issue alerting that a posting account is broken.
|
|
|
|
Args:
|
|
monitor: Script name e.g. "reddit-monitor", "formation-worker"
|
|
platform: Platform name e.g. "Reddit", "Wyoming SOS Portal"
|
|
error: Short error description
|
|
detail: Optional longer detail / stack trace
|
|
"""
|
|
from datetime import datetime, timezone
|
|
|
|
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
key = f"{monitor}:{platform}:{today}"
|
|
|
|
state = _load_alert_state()
|
|
if state.get(key):
|
|
return
|
|
|
|
subject = f"[Monitor Alert] {platform} — {monitor}: {error[:80]}"
|
|
description = (
|
|
f"The **{monitor}** script detected a failure on **{platform}**.\n\n"
|
|
f"**Error:** {error}\n\n"
|
|
f"**Detail:**\n```\n{detail or 'No additional detail'}\n```\n\n"
|
|
f"**Action required:** Check the account credentials / API key for {platform} "
|
|
f"and update the configuration.\n\n"
|
|
f"**Date:** {today}"
|
|
)
|
|
|
|
issue_name = None
|
|
|
|
# Try ERPNext first
|
|
if ERPNEXT_API_KEY and ERPNEXT_API_SECRET:
|
|
issue_name = _create_erpnext_issue(subject, description)
|
|
|
|
# Fallback: Express API internal endpoint
|
|
if not issue_name:
|
|
issue_name = _create_api_issue(subject, description)
|
|
|
|
if issue_name:
|
|
print(f"[alert] ERPNext Issue '{issue_name}' created for {platform} failure")
|
|
state[key] = {"issue_name": issue_name, "error": error}
|
|
_save_alert_state(state)
|
|
else:
|
|
print(f"[alert] Failed to create alert for {platform} failure: {error}")
|
|
|
|
|
|
def _create_erpnext_issue(subject: str, description: str) -> str | None:
|
|
"""Create an Issue in ERPNext directly via REST API."""
|
|
payload = json.dumps({
|
|
"data": json.dumps({
|
|
"doctype": "Issue",
|
|
"subject": subject,
|
|
"description": description,
|
|
"issue_type": "Bug",
|
|
"priority": "High",
|
|
})
|
|
}).encode()
|
|
|
|
req = urllib.request.Request(
|
|
f"{ERPNEXT_URL}/api/resource/Issue",
|
|
data=payload,
|
|
headers={
|
|
"Authorization": f"token {ERPNEXT_API_KEY}:{ERPNEXT_API_SECRET}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
method="POST",
|
|
)
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10) as r:
|
|
resp = json.loads(r.read())
|
|
return resp.get("data", {}).get("name", "")
|
|
except Exception as e:
|
|
print(f"[alert] ERPNext Issue creation failed: {e}")
|
|
return None
|
|
|
|
|
|
def _create_api_issue(subject: str, description: str) -> str | None:
|
|
"""Fallback: create a ticket via our Express API."""
|
|
payload = json.dumps({
|
|
"category": "issue",
|
|
"subject": subject,
|
|
"message": description,
|
|
"email": "alerts@performancewest.net",
|
|
"name": "System Monitor",
|
|
}).encode()
|
|
|
|
req = urllib.request.Request(
|
|
f"{API_URL}/api/v1/tickets",
|
|
data=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10) as r:
|
|
resp = json.loads(r.read())
|
|
return resp.get("ticket_id", "")
|
|
except Exception as e:
|
|
print(f"[alert] Express API ticket creation failed: {e}")
|
|
return None
|