Technology

Betteruptime Multi-tenant Design

betteruptime multi tenant design
Betteruptime Multi-tenant Design | SiamCafe Blog
2025-08-16· อ. บอม — SiamCafe.net· 8,149 คำ

BetterUptime Monitoring

BetterUptime ตรวจสอบ Website และ API ทุก 30 วินาที แจ้งเตือนเมื่อ Service Down มี Status Pages, Incident Management และ Heartbeat Monitoring สำหรับ Cron Jobs

Multi-tenant Architecture ให้หลาย Tenants ใช้ Infrastructure เดียวกัน ต้อง Isolate Data, Compute, Network ใช้ Monitoring แยกตาม Tenant

BetterUptime Configuration

# betteruptime_setup.py — BetterUptime API Configuration
# pip install requests

import requests
import json
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from datetime import datetime

BETTERUPTIME_API = "https://betteruptime.com/api/v2"

@dataclass
class Monitor:
    url: str
    monitor_type: str = "status"  # status, keyword, ping, heartbeat
    check_frequency: int = 30  # seconds
    regions: List[str] = field(default_factory=lambda: ["us", "eu", "asia"])
    expected_status: int = 200
    alert_channels: List[str] = field(default_factory=list)

class BetterUptimeManager:
    """BetterUptime API Manager"""

    def __init__(self, api_token):
        self.token = api_token
        self.headers = {
            "Authorization": f"Bearer {api_token}",
            "Content-Type": "application/json",
        }

    def create_monitor(self, monitor: Monitor):
        """สร้าง Monitor"""
        payload = {
            "monitor_type": monitor.monitor_type,
            "url": monitor.url,
            "check_frequency": monitor.check_frequency,
            "regions": ",".join(monitor.regions),
            "expected_status_codes": [monitor.expected_status],
        }

        resp = requests.post(
            f"{BETTERUPTIME_API}/monitors",
            headers=self.headers,
            json=payload,
        )
        return resp.json()

    def create_heartbeat(self, name, period=300, grace=60):
        """สร้าง Heartbeat Monitor สำหรับ Cron Jobs"""
        payload = {
            "name": name,
            "period": period,  # Expected interval (seconds)
            "grace": grace,    # Grace period before alert
        }

        resp = requests.post(
            f"{BETTERUPTIME_API}/heartbeats",
            headers=self.headers,
            json=payload,
        )
        return resp.json()

    def create_status_page(self, name, subdomain, resources):
        """สร้าง Status Page"""
        payload = {
            "company_name": name,
            "subdomain": subdomain,
            "timezone": "Asia/Bangkok",
        }

        resp = requests.post(
            f"{BETTERUPTIME_API}/status-pages",
            headers=self.headers,
            json=payload,
        )
        return resp.json()

    def create_incident(self, name, description, affected_resources):
        """สร้าง Incident"""
        payload = {
            "name": name,
            "description": description,
            "status": "investigating",
            "affected_resources": affected_resources,
        }

        resp = requests.post(
            f"{BETTERUPTIME_API}/incidents",
            headers=self.headers,
            json=payload,
        )
        return resp.json()

    def setup_tenant_monitoring(self, tenant_id, services):
        """Setup Monitoring สำหรับ Tenant"""
        print(f"\nSetup monitoring for tenant: {tenant_id}")

        for service in services:
            monitor = Monitor(
                url=f"https://{tenant_id}.app.example.com{service['path']}",
                monitor_type=service.get("type", "status"),
                check_frequency=service.get("interval", 60),
            )
            result = self.create_monitor(monitor)
            print(f"  Monitor: {monitor.url} — Created")

# ตัวอย่าง
# manager = BetterUptimeManager("your-api-token")
# manager.setup_tenant_monitoring("acme", [
#     {"path": "/health", "type": "status", "interval": 30},
#     {"path": "/api/v1/status", "type": "status", "interval": 60},
# ])

tenants = ["acme", "globex", "initech", "umbrella"]
services = ["/health", "/api/v1/status", "/api/v1/metrics"]

print("BetterUptime Tenant Monitoring:")
for tenant in tenants:
    print(f"\n  Tenant: {tenant}")
    for svc in services:
        print(f"    Monitor: https://{tenant}.app.example.com{svc}")

Multi-tenant Database Design

# multi_tenant_db.py — Multi-tenant Database Design
# pip install sqlalchemy asyncpg

from dataclasses import dataclass
from typing import List, Dict, Optional
from enum import Enum

class IsolationLevel(Enum):
    SHARED_DB = "shared_database"      # แชร์ DB, แยก tenant_id
    SCHEMA_PER_TENANT = "schema"       # แยก Schema per tenant
    DB_PER_TENANT = "database"         # แยก Database per tenant

@dataclass
class TenantConfig:
    tenant_id: str
    name: str
    plan: str  # free, pro, enterprise
    isolation: IsolationLevel
    db_host: str = "localhost"
    db_name: str = "app"
    schema_name: str = "public"
    max_users: int = 100
    rate_limit: int = 1000  # requests/min

class MultiTenantManager:
    """Multi-tenant Database Manager"""

    def __init__(self):
        self.tenants: Dict[str, TenantConfig] = {}

    def register_tenant(self, config: TenantConfig):
        self.tenants[config.tenant_id] = config
        print(f"Registered tenant: {config.tenant_id} ({config.isolation.value})")

    def get_connection_string(self, tenant_id):
        """Get Database Connection String สำหรับ Tenant"""
        config = self.tenants.get(tenant_id)
        if not config:
            raise ValueError(f"Tenant not found: {tenant_id}")

        if config.isolation == IsolationLevel.DB_PER_TENANT:
            return f"postgresql://{config.db_host}/{config.tenant_id}_db"
        elif config.isolation == IsolationLevel.SCHEMA_PER_TENANT:
            return f"postgresql://{config.db_host}/{config.db_name}?schema={config.tenant_id}"
        else:
            return f"postgresql://{config.db_host}/{config.db_name}"

    def get_query_filter(self, tenant_id):
        """Get SQL Filter สำหรับ Shared DB"""
        config = self.tenants.get(tenant_id)
        if config and config.isolation == IsolationLevel.SHARED_DB:
            return f"WHERE tenant_id = '{tenant_id}'"
        return ""

    def dashboard(self):
        """Tenant Dashboard"""
        print(f"\n{'='*60}")
        print(f"Multi-tenant Dashboard")
        print(f"{'='*60}")

        for tid, config in self.tenants.items():
            print(f"\n  [{config.plan:>10}] {config.name} ({tid})")
            print(f"    Isolation: {config.isolation.value}")
            print(f"    DB: {self.get_connection_string(tid)}")
            print(f"    Limits: {config.max_users} users, {config.rate_limit} req/min")

# SQL Schema สำหรับ Shared Database
SHARED_DB_SCHEMA = """
-- Shared Database Schema (tenant_id approach)

CREATE TABLE tenants (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    plan VARCHAR(50) DEFAULT 'free',
    settings JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255) NOT NULL,
    role VARCHAR(50) DEFAULT 'member',
    UNIQUE(tenant_id, email)
);

CREATE TABLE projects (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    name VARCHAR(255) NOT NULL,
    status VARCHAR(50) DEFAULT 'active',
    created_by UUID REFERENCES users(id)
);

-- Row-Level Security
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation_users ON users
    USING (tenant_id = current_setting('app.current_tenant')::UUID);

CREATE POLICY tenant_isolation_projects ON projects
    USING (tenant_id = current_setting('app.current_tenant')::UUID);

-- Indexes
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);
"""

# ตัวอย่าง
manager = MultiTenantManager()

manager.register_tenant(TenantConfig(
    "acme", "Acme Corp", "enterprise", IsolationLevel.DB_PER_TENANT,
    max_users=1000, rate_limit=10000))
manager.register_tenant(TenantConfig(
    "globex", "Globex Inc", "pro", IsolationLevel.SCHEMA_PER_TENANT,
    max_users=100, rate_limit=5000))
manager.register_tenant(TenantConfig(
    "initech", "Initech LLC", "free", IsolationLevel.SHARED_DB,
    max_users=10, rate_limit=1000))

manager.dashboard()

Tenant-aware API Middleware

# tenant_middleware.py — Multi-tenant API Middleware
# pip install fastapi uvicorn

from dataclasses import dataclass
from typing import Dict, Optional
from datetime import datetime
import time

@dataclass
class TenantContext:
    tenant_id: str
    plan: str
    rate_limit: int
    request_count: int = 0
    last_reset: float = 0

class TenantMiddleware:
    """Middleware สำหรับ Multi-tenant API"""

    def __init__(self):
        self.tenants: Dict[str, TenantContext] = {}
        self.request_log: list = []

    def register(self, tenant_id, plan="free", rate_limit=1000):
        self.tenants[tenant_id] = TenantContext(
            tenant_id=tenant_id, plan=plan,
            rate_limit=rate_limit, last_reset=time.time())

    def resolve_tenant(self, host=None, header=None, path=None):
        """Resolve Tenant จาก Request"""
        tenant_id = None

        # 1. Subdomain: acme.app.example.com
        if host and "." in host:
            tenant_id = host.split(".")[0]

        # 2. Header: X-Tenant-ID
        if not tenant_id and header:
            tenant_id = header

        # 3. Path: /api/v1/tenants/acme/...
        if not tenant_id and path and "/tenants/" in path:
            parts = path.split("/tenants/")
            if len(parts) > 1:
                tenant_id = parts[1].split("/")[0]

        return tenant_id

    def check_rate_limit(self, tenant_id):
        """ตรวจสอบ Rate Limit"""
        ctx = self.tenants.get(tenant_id)
        if not ctx:
            return False, "Tenant not found"

        now = time.time()
        # Reset counter every minute
        if now - ctx.last_reset > 60:
            ctx.request_count = 0
            ctx.last_reset = now

        ctx.request_count += 1

        if ctx.request_count > ctx.rate_limit:
            return False, f"Rate limit exceeded ({ctx.rate_limit}/min)"

        return True, f"OK ({ctx.request_count}/{ctx.rate_limit})"

    def process_request(self, host, path, method="GET"):
        """Process Request ผ่าน Middleware"""
        tenant_id = self.resolve_tenant(host=host)

        if not tenant_id or tenant_id not in self.tenants:
            return {"status": 404, "error": "Tenant not found"}

        ok, msg = self.check_rate_limit(tenant_id)
        if not ok:
            return {"status": 429, "error": msg}

        self.request_log.append({
            "tenant": tenant_id,
            "path": path,
            "method": method,
            "timestamp": datetime.now().isoformat(),
        })

        return {"status": 200, "tenant": tenant_id, "message": msg}

    def metrics(self):
        """แสดง Metrics per Tenant"""
        print(f"\n{'='*55}")
        print(f"Tenant API Metrics")
        print(f"{'='*55}")

        for tid, ctx in self.tenants.items():
            print(f"\n  {tid} [{ctx.plan}]:")
            print(f"    Requests: {ctx.request_count}/{ctx.rate_limit}")
            tenant_logs = [r for r in self.request_log if r["tenant"] == tid]
            print(f"    Total: {len(tenant_logs)}")

# ตัวอย่าง
mw = TenantMiddleware()
mw.register("acme", "enterprise", 10000)
mw.register("globex", "pro", 5000)
mw.register("initech", "free", 100)

# จำลอง Requests
requests_sim = [
    ("acme.app.example.com", "/api/users"),
    ("globex.app.example.com", "/api/projects"),
    ("acme.app.example.com", "/api/orders"),
    ("initech.app.example.com", "/api/users"),
]

for host, path in requests_sim:
    result = mw.process_request(host, path)
    print(f"  {host}{path} -> {result['status']}")

mw.metrics()

Best Practices

BetterUptime คืออะไร

Uptime Monitoring Platform ตรวจสอบ Website API ทุก 30 วินาที แจ้งเตือน SMS Email Slack PagerDuty มี Status Pages Incident Management Heartbeat สำหรับ Cron Jobs

Multi-tenant Architecture คืออะไร

ออกแบบระบบรองรับหลาย Tenants บน Infrastructure เดียว 3 แบบ Shared DB แยก tenant_id Schema per Tenant Database per Tenant เลือกตาม Isolation และ Cost

Tenant Isolation สำคัญอย่างไร

ป้องกัน Tenant เข้าถึงข้อมูลคนอื่น ป้องกัน Noisy Neighbor Resources มาก กระทบคนอื่น Isolate Data Compute Network ใช้ RLS Rate Limiting Resource Quotas

Status Page ช่วยอะไร

แสดงสถานะ Services Real-time ลด Support Tickets เมื่อ Incident แสดง Uptime History Timeline Scheduled Maintenance สร้างความเชื่อมั่นลูกค้า

สรุป

BetterUptime ร่วมกับ Multi-tenant Design ให้ระบบ Monitoring ที่ครอบคลุมทุก Tenant ตั้ง Monitors แยกตาม Tenant Status Pages สำหรับลูกค้า RLS ป้องกัน Data Leak Rate Limiting ตาม Plan Tenant Resolution ด้วย Subdomain เลือก Isolation Level ตาม Compliance

📖 บทความที่เกี่ยวข้อง

Netlify Edge Multi-tenant Designอ่านบทความ → JavaScript Bun Runtime Multi-tenant Designอ่านบทความ → Python httpx Multi-tenant Designอ่านบทความ → QuestDB Time Series Multi-tenant Designอ่านบทความ → Zipkin Tracing Multi-tenant Designอ่านบทความ →

📚 ดูบทความทั้งหมด →