# OCI Budget & Over-Usage Telegram Alert Automation Flow
**Portable, Multi-Region & Multi-Account Dynamic Monitoring Architecture**
This operational flow documents the end-to-end architecture, dynamic auto-discovery mechanisms, state watermark anti-spam controls, and dual-layer Telegram alert delivery for Oracle Cloud Infrastructure (OCI) instances across different tenancies, accounts, and regions.
---
## 1. Architectural Principles & Rate-Limit Safety
```text
┌────────────────────────────────────────────────────────────────────────┐
│ OCI TENANCY CONTROL PLANE │
│ (Computes actual spend every 4–8 hours via batch billing engine) │
└───────────────────────────────────┬────────────────────────────────────┘
│
Periodic Query (Every 5 Hours - 4–5 req/day = 0.0001% of limit)
▼
┌────────────────────────────────────────────────────────────────────────┐
│ PORTABLE SERVER RUNTIME ENGINE │
│ │
│ 1. Dynamic Identity Discovery: │
│ • Local IMDS (169.254.169.254): Server Name & Region │
│ • ~/.oci/config: Tenancy OCID & Auth Keys │
│ • OCI CLI: Dynamic Active Budget ID discovery │
│ │
│ 2. Evaluation & State Watermark: │
│ • ~/.hermes/data/oci_budget_state.json │
│ • If Spend == 0.00 SGD: SILENT (Zero chat noise) │
│ • If Spend > 0.00 & Spend > Last Alerted: TRIGGER ALERT │
│ │
│ 3. Dual-Layer Telegram Delivery: │
│ • Primary: Local TDLib Bot API (:8082) │
│ • Fallback: Official Telegram API (api.telegram.org) │
│ • Audience: Target Chat ID with @sunda005 @sunda004 mentions │
└────────────────────────────────────────────────────────────────────────┘
```
### Rate-Limit Analysis
- **OCI API Quota:** Default OCI Control Plane API limit is **50 requests per second**.
- **Execution Cadence:** Running once every 5 hours results in only **4 to 5 requests per day**.
- **Consumption:** Consumes less than **0.0001%** of Oracle's allowed API quota, completely eliminating any risk of HTTP 429 (`TooManyRequests`).
---
## 2. Dynamic Discovery Mechanisms (Zero Hardcoding)
To ensure this script runs seamlessly across any instance, tenancy, region (e.g. `ap-singapore-1`, `ap-batam-1`, `us-ashburn-1`), and account:
### A. Instance Identity & Region (via Local IMDS)
Every OCI compute instance exposes an unauthenticated local metadata service at `http://169.254.169.254/opc/v2/instance/`:
```bash
curl -s -H "Authorization: Bearer Oracle" http://169.254.169.254/opc/v2/instance/
```
Extracts dynamically:
- `displayName` (e.g. `singapore`, `batam-prod`, `backup-node`)
- `region` (e.g. `ap-singapore-1`, `ap-batam-1`)
- `shape` (e.g. `VM.Standard.A1.Flex`)
### B. Tenancy OCID Resolution
Parsed dynamically from `~/.oci/config`:
```python
for line in Path("~/.oci/config").expanduser().read_text().splitlines():
if line.strip().startswith("tenancy="):
tenancy_id = line.strip().split("=", 1)[1]
```
### C. Active Budget Discovery
Automatically discovers the active budget associated with the tenancy:
```bash
~/.local/bin/oci budgets budget budget list --compartment-id "$TENANCY_ID"
```
Selects the active budget containing `reminder` or defaults to the first active budget OCID.
---
## 3. Dynamic Python Implementation (`oci_budget_alert.py`)
Deploy this script to `~/.hermes/scripts/oci_budget_alert.py` (or any utility directory):
```python
#!/usr/bin/env python3
"""
Dynamic OCI Budget & Cost Over-usage Monitor with Telegram Alerts
- Fully portable & dynamic: Auto-discovers Tenancy OCID, active Budget, Instance Region & Hardware specs.
- Works across different regions, accounts, and servers without hardcoded credentials.
- Anti-spam state watermark: Only alerts when actual spend > 0 and increases.
- Rate-limit friendly: Designed for 4-6 hour periodic cron execution (0.0001% of OCI limit).
"""
import os
import sys
import json
import time
import shutil
import subprocess
import requests
from pathlib import Path
from datetime import datetime, timezone, timedelta
WIB = timezone(timedelta(hours=7))
# ── Dynamic Paths ──
HOME_DIR = Path.home()
OCI_CONFIG_FILE = HOME_DIR / ".oci" / "config"
STATE_FILE = HOME_DIR / ".hermes" / "data" / "oci_budget_state.json"
# Find OCI CLI dynamically
OCI_CLI = (
shutil.which("oci")
or (HOME_DIR / ".local" / "bin" / "oci" if (HOME_DIR / ".local" / "bin" / "oci").exists() else None)
or "/usr/local/bin/oci"
)
# ── Telegram Configuration (Overrideable via ENV) ──
TELEGRAM_BOT_TOKEN = os.getenv("OCI_ALERT_BOT_TOKEN", "1018732419:AAE1v2fDEkqbQOzI_EGo8u1EUpC5LGx4Guo")
TARGET_CHAT_ID = os.getenv("OCI_ALERT_CHAT_ID", "-1004327669652")
MENTIONS = os.getenv("OCI_ALERT_MENTIONS", "@sunda005 @sunda004")
LOCAL_BOT_API = os.getenv("LOCAL_BOT_API", "http://localhost:8082")
OFFICIAL_BOT_API = "https://api.telegram.org"
def get_tenancy_ocid() -> str:
"""Dynamically extract tenancy OCID from ~/.oci/config or OCI CLI."""
env_tenancy = os.getenv("OCI_TENANCY_ID")
if env_tenancy:
return env_tenancy
if OCI_CONFIG_FILE.exists():
try:
for line in OCI_CONFIG_FILE.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("tenancy="):
return line.split("=", 1)[1].strip()
except Exception:
pass
try:
cmd = [str(OCI_CLI), "iam", "compartment", "list", "--all", "--compartment-id-in-subtree", "true", "--query", 'data[0]."compartment-id"', "--raw-output"]
res = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if res.returncode == 0 and res.stdout.strip():
return res.stdout.strip()
except Exception:
pass
raise RuntimeError("Cannot resolve Tenancy OCID. Ensure ~/.oci/config exists or set OCI_TENANCY_ID.")
def get_active_budget_id(tenancy_id: str) -> str:
"""Dynamically discover the active budget ID within the tenancy."""
env_budget = os.getenv("OCI_BUDGET_ID")
if env_budget:
return env_budget
cmd = [
str(OCI_CLI), "budgets", "budget", "budget", "list",
"--compartment-id", tenancy_id,
"--output", "json"
]
res = subprocess.run(cmd, capture_output=True, text=True, timeout=25)
if res.returncode != 0:
raise RuntimeError(f"Failed to list budgets: {res.stderr.strip()}")
budgets = json.loads(res.stdout).get("data", [])
if not budgets:
raise RuntimeError("No budgets found in this tenancy. Create a budget first via OCI CLI.")
active_budgets = [b for b in budgets if b.get("lifecycle-state") == "ACTIVE"]
if not active_budgets:
active_budgets = budgets
preferred = next((b for b in active_budgets if "reminder" in b.get("display-name", "").lower()), active_budgets[0])
return preferred.get("id")
def get_instance_identity() -> dict:
"""Fetch instance name, region, and shape from local OCI IMDS or fallback."""
out = {"name": os.uname().nodename, "region": "unknown", "shape": "VM.Standard.A1.Flex", "cores": 4, "ram_gb": 24}
try:
r = requests.get("http://169.254.169.254/opc/v2/instance/", headers={"Authorization": "Bearer Oracle"}, timeout=2)
if r.status_code == 200:
data = r.json()
out["name"] = data.get("displayName") or out["name"]
out["region"] = data.get("region") or out["region"]
out["shape"] = data.get("shape") or out["shape"]
except Exception:
pass
try:
import multiprocessing
out["cores"] = multiprocessing.cpu_count()
out["ram_gb"] = round(os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') / (1024 ** 3))
except Exception:
pass
return out
def format_wib(dt_or_str) -> str:
if isinstance(dt_or_str, str):
try:
dt = datetime.fromisoformat(dt_or_str.replace("Z", "+00:00"))
except Exception:
return dt_or_str
else:
dt = dt_or_str
dt_wib = dt.astimezone(WIB)
months = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"]
return f"{dt_wib.day:02d} {months[dt_wib.month - 1]} {dt_wib.year}, {dt_wib.hour:02d}:{dt_wib.minute:02d} WIB"
def load_state() -> dict:
if STATE_FILE.exists():
try:
return json.loads(STATE_FILE.read_text(encoding="utf-8"))
except Exception:
pass
return {"last_alerted_spend": 0.0, "last_checked_at": None, "last_computed_time": None}
def save_state(state: dict):
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, indent=2), encoding="utf-8")
def send_telegram_alert(text: str) -> bool:
"""Send alert via Local TDLib API (:8082) with fallback to Official API."""
data = {
"chat_id": TARGET_CHAT_ID,
"text": text,
"parse_mode": "HTML",
"disable_web_page_preview": True,
}
# 1. Local TDLib Bot API
try:
url = f"{LOCAL_BOT_API}/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
resp = requests.post(url, data=data, timeout=10)
if resp.status_code == 200 and resp.json().get("ok"):
print(f"[+] Telegram alert dispatched via Local Bot API ({LOCAL_BOT_API}).")
return True
except Exception:
pass
# 2. Official Telegram API Fallback
try:
url = f"{OFFICIAL_BOT_API}/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
resp = requests.post(url, data=data, timeout=15)
if resp.status_code == 200 and resp.json().get("ok"):
print(f"[+] Telegram alert dispatched via Official Telegram API.")
return True
except Exception as e:
print(f"[-] Telegram alert delivery failed: {e}")
return False
def get_oci_budget_info(budget_id: str) -> dict:
cmd = [
str(OCI_CLI), "budgets", "budget", "budget", "get",
"--budget-id", budget_id
]
res = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if res.returncode != 0:
raise RuntimeError(f"OCI CLI returned {res.returncode}: {res.stderr.strip()}")
data = json.loads(res.stdout)["data"]
return {
"name": data.get("display-name", "reminder-usage"),
"actual_spend": float(data.get("actual-spend", 0.0)),
"forecasted_spend": float(data.get("forecasted-spend", 0.0)),
"budget_amount": float(data.get("amount", 1.0)),
"time_computed": data.get("time-spend-computed"),
"lifecycle_state": data.get("lifecycle-state", "ACTIVE"),
}
def build_alert_message(info: dict, inst: dict, is_test: bool = False) -> str:
prefix = "🧪 [SIMULASI TEST NOTIFIKASI OCI]\n" if is_test else ""
spend = info["actual_spend"]
budget = info["budget_amount"]
forecast = info["forecasted_spend"]
computed_str = format_wib(info["time_computed"]) if info["time_computed"] else "Baru saja"
lines = [
f"{prefix}⚠️ PERINGATAN BIAYA ORACLE CLOUD (OCI)",
f"Perhatian: {MENTIONS}\n",
f"Terdeteksi adanya pemakaian komputasi/storage berbayar pada OCI Tenancy:\n",
f"• Nominal Tagihan: SGD {spend:.2f} (Batas Budget: SGD {budget:.2f})",
f"• Perkiraan Akhir Bulan: SGD {forecast:.2f}",
f"• Waktu Perhitungan OCI: {computed_str}",
f"• Server: {inst['name']} (Region: {inst['region']})",
f"• Alokasi Hardware: {inst['cores']} OCPU / {inst['ram_gb']} GB RAM\n",
f"⚡ Tindakan Rekomendasi:",
f"Segera resize instance ke 2.0 OCPU / 12.0 GB RAM jika ingin menghentikan akumulasi overage agar tagihan tetap di bawah $1.",
]
return "\n".join(lines)
def check_and_alert(force: bool = False, is_test: bool = False) -> bool:
try:
tenancy_id = get_tenancy_ocid()
budget_id = get_active_budget_id(tenancy_id)
info = get_oci_budget_info(budget_id)
inst = get_instance_identity()
except Exception as e:
print(f"[!] Failed to inspect OCI Budget: {e}")
return False
state = load_state()
actual_spend = info["actual_spend"]
last_alerted_spend = state.get("last_alerted_spend", 0.0)
print(f"[*] OCI Check [{inst['name']} / {inst['region']}]: spend={actual_spend} SGD, limit={info['budget_amount']} SGD, state={last_alerted_spend}")
should_alert = False
if is_test:
should_alert = True
if actual_spend == 0.0:
info["actual_spend"] = 0.05
info["forecasted_spend"] = 0.85
elif force:
should_alert = True
elif actual_spend > 0.0 and actual_spend > last_alerted_spend:
should_alert = True
if should_alert:
msg = build_alert_message(info, inst, is_test=is_test)
sent = send_telegram_alert(msg)
if sent and not is_test:
state["last_alerted_spend"] = actual_spend
state["last_computed_time"] = info["time_computed"]
state["last_alerted_at"] = datetime.now(WIB).isoformat()
save_state(state)
return sent
else:
print(f"[+] OCI Spend is 0.00 SGD (or unchanged). No alert needed.")
state["last_checked_at"] = datetime.now(WIB).isoformat()
save_state(state)
return False
if __name__ == "__main__":
test_flag = "--test" in sys.argv
force_flag = "--force" in sys.argv
check_and_alert(force=force_flag, is_test=test_flag)
```
---
## 4. Setting Up Budget on a New Server / Account (100% CLI)
If the new server/tenancy does not yet have an OCI budget rule configured, create it in 2 commands via OCI CLI:
```bash
# 1. Resolve Tenancy OCID
TENANCY_ID=$(oci iam compartment list --all --compartment-id-in-subtree true --query "data[0].\"compartment-id\"" --raw-output)
# 2. Create Budget (Limit: $1.00 Monthly)
BUDGET_JSON=$(oci budgets budget budget create \
--compartment-id "$TENANCY_ID" \
--amount 1.0 \
--reset-period MONTHLY \
--display-name "reminder-usage" \
--description "Alert if usage exceeds free threshold" \
--output json)
BUDGET_ID=$(echo "$BUDGET_JSON" | jq -r '.data.id')
# 3. Create Alert Rule (Triggers at 1% actual spend = $0.01)
oci budgets budget alert-rule create \
--budget-id "$BUDGET_ID" \
--type ACTUAL \
--threshold 1.0 \
--threshold-type PERCENTAGE \
--display-name "alert-exceeded" \
--message "OCI Usage Exceeded Free Tier" \
--recipients "admin@example.com"
```
---
## 5. Deployment & Execution Methods
### Method 1: Integrated into Portable Backup Cycle (Adaptive & Dynamic)
The 5-hour backup runner (`cron_hermes_backup.py`) features **dynamic multi-target discovery**. It automatically detects whether the host has only Hermes Agent or additional applications (like `EssaTSM_BOT`). If a server does not have the secondary bot, it cleanly backs up only Hermes Agent (`[1/1]`) without errors, warnings, or missing-file failures:
```python
# 1. Resolve live OCI budget spend dynamically
oci_status_str = "SGD 0.00 / 1.00 (Safe)"
try:
from oci_budget_alert import get_tenancy_ocid, get_active_budget_id, get_oci_budget_info
t_id = get_tenancy_ocid()
b_id = get_active_budget_id(t_id)
b_info = get_oci_budget_info(b_id)
spend = b_info.get("actual_spend", 0.0)
limit = b_info.get("budget_amount", 1.0)
label = "Safe" if spend == 0.0 else "OVERAGE"
oci_status_str = f"SGD {spend:.2f} / {limit:.2f} ({label})"
except Exception:
pass
# 2. Dynamic target discovery (Works with 1, 2, or N components)
has_hermes = HERMES_DIR.exists()
has_bot = BOT_PROJECT_DIR.exists()
total_targets = (1 if has_hermes else 0) + (1 if has_bot else 0)
active_files = []
current_step = 1
if has_hermes:
step_lbl = f"{current_step}/{total_targets}"
hermes_7z = backup_hermes_agent(stamp, now_wib_str, next_wib_str, oci_status_str=oci_status_str, step_label=step_lbl)
if hermes_7z:
active_files.append(hermes_7z)
current_step += 1
if has_bot:
step_lbl = f"{current_step}/{total_targets}"
bot_7z, bot_md = backup_essa_bot(stamp, now_wib_str, next_wib_str, oci_status_str=oci_status_str, step_label=step_lbl)
if bot_7z:
active_files.append(bot_7z)
if bot_md:
active_files.append(bot_md)
current_step += 1
# 3. Dynamic zero-garbage retention
prune_storage(active_files)
# 4. OCI Budget & Cost Over-usage Safety Alert Check
try:
from oci_budget_alert import check_and_alert
print("\n[*] Running OCI Budget & Cost Over-usage check...")
check_and_alert()
except Exception as e_oci:
print(f"[!] OCI Budget check skipped: {e_oci}")
```
#### Sample Telegram Captions Generated:
**Hermes Agent Archive:**
```text
📦 Hermes Agent Portable Backup (20261021_023159)
• Format: 7-Zip LZMA2
• SHA-256: 5e6de36e19af...
• Size: 23.37 MB
• OCI Budget: SGD 0.00 / 1.00 (Safe)
• Next backup: 21 Okt 2026, 07:31 WIB
```
**Essa Feature Bot Archive:**
```text
📦 Essa Feature Bot Portable Backup (@EssaTSM_BOT)
• Format: 7-Zip LZMA2
• SHA-256: 6b60883b6ec1...
• Size: 0.44 MB
• OCI Budget: SGD 0.00 / 1.00 (Safe)
• Next backup: 21 Okt 2026, 07:31 WIB
```
### Method 2: Standalone Systemd Timer & Service
Create service unit `~/.config/systemd/user/oci-budget.service`:
```ini
[Unit]
Description=OCI Budget & Cost Over-usage Monitor
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 %h/.hermes/scripts/oci_budget_alert.py
```
Create timer unit `~/.config/systemd/user/oci-budget.timer`:
```ini
[Unit]
Description=Run OCI Budget Monitor Every 6 Hours
[Timer]
OnCalendar=00/6:00:00
Persistent=true
[Install]
WantedBy=timers.target
```
Enable and start:
```bash
systemctl --user daemon-reload
systemctl --user enable --now oci-budget.timer
```
---
## 6. Emergency Automated Remediation (Self-Resize)
If an alert triggers indicating spend overage (`actual_spend > 0`), run the self-resize command directly from terminal. The control plane updates the hypervisor and reboots the instance in ~90 seconds:
```bash
# Retrieve instance OCID dynamically from local IMDS
INSTANCE_ID=$(curl -s -H "Authorization: Bearer Oracle" http://169.254.169.254/opc/v2/instance/ | jq -r .id)
# Resize to 2 OCPU / 12 GB RAM (Zero-cost safe shape)
~/.local/bin/oci compute instance update \
--instance-id "$INSTANCE_ID" \
--shape-config '{"ocpus": 2.0, "memoryInGbs": 12.0}' \
--force
```
---
## 7. Operational Testing & Verification
1. **Simulated Dry-Run (Verifies Telegram Delivery & Mentions):**
```bash
python3 ~/.hermes/scripts/oci_budget_alert.py --test
```
*Expected output:* Dispatches a simulated alert with mock spend (`SGD 0.05`) to Telegram chat `-1004327669652` mentioning `@sunda005 @sunda004`. State watermark remains unaffected.
2. **Real Production Check (Silent when spend is $0.00):**
```bash
python3 ~/.hermes/scripts/oci_budget_alert.py
```
*Expected output:* Prints `[+] OCI Spend is 0.00 SGD (or unchanged). No alert needed.` with zero chat spam.