feat: EwoooC 初始化 — 完整專案推版至 Gitea
Some checks failed
CD Pipeline / deploy (push) Failing after 59s

- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml)
- 部署模式: rsync Python 檔案至 188 → docker restart (volume mount)
- Dockerfile/requirements 變動時自動重建 Docker image
- 部署通知: Telegram (開始/成功/失敗)
- 健康檢查: https://mo.wooo.work/health (最多 5 次重試)
- 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ogt
2026-04-19 01:21:13 +08:00
commit 1b4f3a7bbe
504 changed files with 387725 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
# =============================================================================
# Alertmanager 配置 - 告警通知管理
# =============================================================================
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 1h
receiver: 'telegram-webhook'
routes:
# 高優先級告警 - CPU/RAM 超過 50%
- match:
severity: warning
receiver: 'telegram-webhook'
group_wait: 10s
repeat_interval: 30m
# 嚴重告警 - CPU/RAM 超過 80%
- match:
severity: critical
receiver: 'telegram-webhook'
group_wait: 5s
repeat_interval: 15m
receivers:
- name: 'telegram-webhook'
webhook_configs:
- url: 'http://momo-pro-system:5000/api/alert/webhook'
send_resolved: true
http_config:
basic_auth:
username: 'alertmanager'
password: 'wooo_alert_2026'
inhibit_rules:
# 如果已有 critical 告警,抑制 warning 告警
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'instance']

View File

@@ -0,0 +1,89 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# Blackbox Exporter Configuration
# =============================================================================
modules:
# ---------------------------------------------------------------------------
# HTTP/HTTPS 探測模組
# ---------------------------------------------------------------------------
http_2xx:
prober: http
timeout: 10s
http:
valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
valid_status_codes: [200, 301, 302, 303]
method: GET
follow_redirects: true
fail_if_ssl: false
fail_if_not_ssl: false
tls_config:
insecure_skip_verify: false
http_2xx_insecure:
prober: http
timeout: 10s
http:
valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
valid_status_codes: [200, 301, 302, 303]
method: GET
follow_redirects: true
tls_config:
insecure_skip_verify: true
http_post_2xx:
prober: http
timeout: 10s
http:
method: POST
valid_status_codes: [200, 201, 202]
# ---------------------------------------------------------------------------
# TCP 連接探測模組
# ---------------------------------------------------------------------------
tcp_connect:
prober: tcp
timeout: 5s
tcp:
preferred_ip_protocol: "ip4"
tcp_connect_tls:
prober: tcp
timeout: 5s
tcp:
preferred_ip_protocol: "ip4"
tls: true
tls_config:
insecure_skip_verify: false
# ---------------------------------------------------------------------------
# ICMP Ping 探測模組
# ---------------------------------------------------------------------------
icmp:
prober: icmp
timeout: 5s
icmp:
preferred_ip_protocol: "ip4"
# ---------------------------------------------------------------------------
# DNS 解析探測模組
# ---------------------------------------------------------------------------
dns_check:
prober: dns
timeout: 5s
dns:
preferred_ip_protocol: "ip4"
query_name: "mo.wooo.work"
query_type: "A"
valid_rcodes:
- NOERROR
dns_check_momo:
prober: dns
timeout: 5s
dns:
preferred_ip_protocol: "ip4"
query_name: "momo.wooo.work"
query_type: "A"
valid_rcodes:
- NOERROR

View File

@@ -0,0 +1,18 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# Grafana Dashboard Provisioning Configuration
# =============================================================================
apiVersion: 1
providers:
- name: 'WOOO Dashboards'
orgId: 1
folder: 'WOOO Monitoring'
folderUid: 'wooo-monitoring'
type: file
disableDeletion: false
updateIntervalSeconds: 30
allowUiUpdates: true
options:
path: /etc/grafana/provisioning/dashboards/json

View File

@@ -0,0 +1,497 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
"id": 100,
"panels": [],
"title": "Docker 容器概覽",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null }
]
}
}
},
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 },
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "運行中容器",
"type": "stat",
"targets": [
{
"expr": "count(container_last_seen{name=~\".+\"})",
"legendFormat": "",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 2147483648 },
{ "color": "red", "value": 4294967296 }
]
},
"unit": "bytes"
}
},
"gridPos": { "h": 4, "w": 5, "x": 4, "y": 1 },
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "容器總記憶體使用",
"type": "stat",
"targets": [
{
"expr": "sum(container_memory_usage_bytes{name=~\".+\"})",
"legendFormat": "",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 50 },
{ "color": "red", "value": 80 }
]
},
"unit": "percent"
}
},
"gridPos": { "h": 4, "w": 5, "x": 9, "y": 1 },
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "容器總 CPU 使用",
"type": "stat",
"targets": [
{
"expr": "sum(rate(container_cpu_usage_seconds_total{name=~\".+\"}[5m])) * 100",
"legendFormat": "",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null }
]
},
"unit": "Bps"
}
},
"gridPos": { "h": 4, "w": 5, "x": 14, "y": 1 },
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "網路 RX 流量",
"type": "stat",
"targets": [
{
"expr": "sum(rate(container_network_receive_bytes_total{name=~\".+\"}[5m]))",
"legendFormat": "",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null }
]
},
"unit": "Bps"
}
},
"gridPos": { "h": 4, "w": 5, "x": 19, "y": 1 },
"id": 5,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "網路 TX 流量",
"type": "stat",
"targets": [
{
"expr": "sum(rate(container_network_transmit_bytes_total{name=~\".+\"}[5m]))",
"legendFormat": "",
"refId": "A"
}
]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
"id": 101,
"panels": [],
"title": "各容器資源使用",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "opacity",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "percent"
}
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 },
"id": 10,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"title": "各容器 CPU 使用率",
"type": "timeseries",
"targets": [
{
"expr": "rate(container_cpu_usage_seconds_total{name=~\"momo.*\"}[5m]) * 100",
"legendFormat": "{{name}}",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "opacity",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "bytes"
}
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
"id": 11,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"title": "各容器記憶體使用",
"type": "timeseries",
"targets": [
{
"expr": "container_memory_usage_bytes{name=~\"momo.*\"}",
"legendFormat": "{{name}}",
"refId": "A"
}
]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
"id": 102,
"panels": [],
"title": "網路流量",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "Bps"
}
},
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 15 },
"id": 20,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"title": "容器網路流量",
"type": "timeseries",
"targets": [
{
"expr": "rate(container_network_receive_bytes_total{name=~\"momo.*\"}[5m])",
"legendFormat": "{{name}} RX",
"refId": "A"
},
{
"expr": "rate(container_network_transmit_bytes_total{name=~\"momo.*\"}[5m])",
"legendFormat": "{{name}} TX",
"refId": "B"
}
]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
"id": 103,
"panels": [],
"title": "容器狀態列表",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"custom": {
"align": "auto",
"cellOptions": { "type": "auto" },
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null }
]
}
},
"overrides": [
{
"matcher": { "id": "byName", "options": "Container" },
"properties": [{ "id": "custom.width", "value": 200 }]
},
{
"matcher": { "id": "byName", "options": "CPU %" },
"properties": [
{ "id": "unit", "value": "percent" },
{ "id": "custom.cellOptions", "value": { "mode": "gradient", "type": "gauge" } },
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 50 }, { "color": "red", "value": 80 }] } }
]
},
{
"matcher": { "id": "byName", "options": "Memory" },
"properties": [
{ "id": "unit", "value": "bytes" },
{ "id": "custom.cellOptions", "value": { "mode": "gradient", "type": "gauge" } },
{ "id": "thresholds", "value": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 536870912 }, { "color": "red", "value": 1073741824 }] } }
]
}
]
},
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 },
"id": 30,
"options": {
"cellHeight": "sm",
"footer": { "countRows": false, "fields": "", "reducer": ["sum"], "show": false },
"showHeader": true,
"sortBy": [{ "desc": true, "displayName": "CPU %" }]
},
"title": "容器資源使用表",
"type": "table",
"targets": [
{
"expr": "rate(container_cpu_usage_seconds_total{name=~\"momo.*\"}[5m]) * 100",
"format": "table",
"instant": true,
"legendFormat": "",
"refId": "CPU"
},
{
"expr": "container_memory_usage_bytes{name=~\"momo.*\"}",
"format": "table",
"instant": true,
"legendFormat": "",
"refId": "Memory"
}
],
"transformations": [
{
"id": "seriesToColumns",
"options": { "byField": "name" }
},
{
"id": "organize",
"options": {
"excludeByName": { "Time": true, "Time 1": true, "Time 2": true, "__name__": true, "__name__ 1": true, "__name__ 2": true, "id": true, "id 1": true, "id 2": true, "image": true, "image 1": true, "image 2": true, "instance": true, "instance 1": true, "instance 2": true, "job": true, "job 1": true, "job 2": true },
"renameByName": { "Value #CPU": "CPU %", "Value #Memory": "Memory", "name": "Container" }
}
}
]
}
],
"refresh": "30s",
"schemaVersion": 38,
"tags": ["wooo", "container", "docker"],
"templating": { "list": [] },
"time": { "from": "now-1h", "to": "now" },
"timepicker": {},
"timezone": "Asia/Taipei",
"title": "WOOO 容器監控",
"uid": "wooo-container-monitoring",
"version": 1,
"weekStart": ""
}

View File

@@ -0,0 +1,395 @@
{
"annotations": { "list": [] },
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
"id": 100,
"panels": [],
"title": "資料庫狀態概覽",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "color": "red", "index": 1, "text": "離線" }, "1": { "color": "green", "index": 0, "text": "正常" } }, "type": "value" }
],
"thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] }
}
},
"gridPos": { "h": 4, "w": 3, "x": 0, "y": 1 },
"id": 1,
"options": { "colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "資料庫狀態",
"type": "stat",
"targets": [{ "expr": "momo_database_up", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 536870912 }, { "color": "red", "value": 1073741824 }] },
"unit": "bytes"
}
},
"gridPos": { "h": 4, "w": 4, "x": 3, "y": 1 },
"id": 2,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "資料庫大小",
"type": "stat",
"targets": [{ "expr": "momo_database_size_bytes", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 104857600 }, { "color": "red", "value": 209715200 }] },
"unit": "bytes"
}
},
"gridPos": { "h": 4, "w": 4, "x": 7, "y": 1 },
"id": 3,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "WAL 大小",
"type": "stat",
"targets": [{ "expr": "momo_database_wal_size_bytes", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "short"
}
},
"gridPos": { "h": 4, "w": 3, "x": 11, "y": 1 },
"id": 4,
"options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "今日新增商品",
"type": "stat",
"targets": [{ "expr": "momo_products_today_total", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "short"
}
},
"gridPos": { "h": 4, "w": 3, "x": 14, "y": 1 },
"id": 5,
"options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "今日價格變動",
"type": "stat",
"targets": [{ "expr": "momo_price_records_today_total", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 5 }, { "color": "red", "value": 10 }] },
"unit": "percent"
}
},
"gridPos": { "h": 4, "w": 3, "x": 17, "y": 1 },
"id": 6,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "碎片率",
"type": "stat",
"targets": [{ "expr": "momo_sqlite_fragmentation_percent", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "short"
}
},
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 },
"id": 7,
"options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "SQLite 頁面數",
"type": "stat",
"targets": [{ "expr": "momo_sqlite_page_count", "refId": "A" }]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
"id": 101,
"panels": [],
"title": "查詢效能監控",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "short"
}
},
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 6 },
"id": 10,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "總查詢數",
"type": "stat",
"targets": [{ "expr": "momo_query_total", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 10 }, { "color": "red", "value": 50 }] },
"unit": "short"
}
},
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 6 },
"id": 11,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "慢查詢數 (>1秒)",
"type": "stat",
"targets": [{ "expr": "momo_query_slow_total", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 5 }, { "color": "red", "value": 20 }] },
"unit": "short"
}
},
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 6 },
"id": 12,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "極慢查詢數 (>5秒)",
"type": "stat",
"targets": [{ "expr": "momo_query_very_slow_total", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 500 }, { "color": "red", "value": 1000 }] },
"unit": "ms"
}
},
"gridPos": { "h": 4, "w": 4, "x": 12, "y": 6 },
"id": 13,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "平均查詢時間",
"type": "stat",
"targets": [{ "expr": "momo_query_avg_time_ms", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 5 }, { "color": "red", "value": 10 }] },
"unit": "percent"
}
},
"gridPos": { "h": 4, "w": 4, "x": 16, "y": 6 },
"id": 14,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "慢查詢率",
"type": "stat",
"targets": [{ "expr": "momo_query_slow_rate_percent", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "ms"
}
},
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 6 },
"id": 15,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "累計查詢時間",
"type": "stat",
"targets": [{ "expr": "momo_query_time_total_ms", "refId": "A" }]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 10 },
"id": 102,
"panels": [],
"title": "資料表記錄數",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] }, "unit": "short" } },
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 11 },
"id": 20,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "Products 表",
"type": "stat",
"targets": [{ "expr": "momo_table_rows{table=\"products\"}", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [{ "color": "purple", "value": null }] }, "unit": "short" } },
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 11 },
"id": 21,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "Price Records 表",
"type": "stat",
"targets": [{ "expr": "momo_table_rows{table=\"price_records\"}", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [{ "color": "orange", "value": null }] }, "unit": "short" } },
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 11 },
"id": 22,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "Monthly Summary 表",
"type": "stat",
"targets": [{ "expr": "momo_table_rows{table=\"monthly_summary_analysis\"}", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, "unit": "short" } },
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 11 },
"id": 23,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "Promo Products 表",
"type": "stat",
"targets": [{ "expr": "momo_table_rows{table=\"promo_products\"}", "refId": "A" }]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 15 },
"id": 103,
"panels": [],
"title": "磁碟使用狀況",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"max": 100, "min": 0,
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 70 }, { "color": "red", "value": 85 }] },
"unit": "percent"
}
},
"gridPos": { "h": 6, "w": 8, "x": 0, "y": 16 },
"id": 30,
"options": { "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true },
"title": "磁碟使用率",
"type": "gauge",
"targets": [{ "expr": "(momo_disk_used_bytes / momo_disk_total_bytes) * 100", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, "unit": "bytes" } },
"gridPos": { "h": 6, "w": 8, "x": 8, "y": 16 },
"id": 31,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "磁碟總空間",
"type": "stat",
"targets": [{ "expr": "momo_disk_total_bytes", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "yellow", "value": 5368709120 }, { "color": "green", "value": 10737418240 }] },
"unit": "bytes"
}
},
"gridPos": { "h": 6, "w": 8, "x": 16, "y": 16 },
"id": 32,
"options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "磁碟可用空間",
"type": "stat",
"targets": [{ "expr": "momo_disk_free_bytes", "refId": "A" }]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 },
"id": 104,
"panels": [],
"title": "歷史趨勢",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "bytes"
}
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 23 },
"id": 40,
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } },
"title": "資料庫大小趨勢",
"type": "timeseries",
"targets": [
{ "expr": "momo_database_size_bytes", "legendFormat": "DB Size", "refId": "A" },
{ "expr": "momo_database_wal_size_bytes", "legendFormat": "WAL Size", "refId": "B" }
]
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "opacity", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "ms"
}
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 23 },
"id": 41,
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } },
"title": "查詢效能趨勢",
"type": "timeseries",
"targets": [
{ "expr": "momo_query_avg_time_ms", "legendFormat": "Avg Query Time", "refId": "A" },
{ "expr": "rate(momo_query_slow_total[5m]) * 60", "legendFormat": "Slow Queries/min", "refId": "B" }
]
}
],
"refresh": "30s",
"schemaVersion": 38,
"tags": ["wooo", "database", "sqlite", "slow-query"],
"templating": { "list": [] },
"time": { "from": "now-1h", "to": "now" },
"timepicker": {},
"timezone": "Asia/Taipei",
"title": "WOOO 資料庫監控",
"uid": "wooo-database-monitoring",
"version": 2,
"weekStart": ""
}

View File

@@ -0,0 +1,255 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"gridPos": { "h": 2, "w": 24, "x": 0, "y": 0 },
"id": 1,
"options": {
"code": { "language": "plaintext", "showLineNumbers": false, "showMiniMap": false },
"content": "# 📋 Momo Pro System - 日誌監控中心\n\n實時查看應用程式日誌、訪問記錄和錯誤追蹤",
"mode": "markdown"
},
"type": "text"
},
{
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 2 },
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["sum"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [
{
"datasource": { "type": "loki", "uid": "loki" },
"expr": "sum(count_over_time({job=\"gunicorn-access\"}[$__range])) + sum(count_over_time({job=\"momo-app\"}[$__range])) + sum(count_over_time({job=\"gunicorn-error\"}[$__range]))",
"queryType": "range",
"refId": "A"
}
],
"title": "📊 日誌總數",
"type": "stat"
},
{
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 2 },
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["sum"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [
{
"datasource": { "type": "loki", "uid": "loki" },
"expr": "sum(count_over_time({job=\"gunicorn-access\"} |~ \" 5[0-9][0-9] \"[$__range]))",
"queryType": "range",
"refId": "A"
}
],
"title": "❌ 5xx 錯誤",
"type": "stat",
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 1 },
{ "color": "red", "value": 10 }
]
}
}
}
},
{
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 2 },
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["sum"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [
{
"datasource": { "type": "loki", "uid": "loki" },
"expr": "sum(count_over_time({job=\"gunicorn-access\"} |~ \" 4[0-9][0-9] \"[$__range]))",
"queryType": "range",
"refId": "A"
}
],
"title": "⚠️ 4xx 錯誤",
"type": "stat",
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 10 },
{ "color": "orange", "value": 50 }
]
}
}
}
},
{
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 2 },
"id": 5,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["sum"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [
{
"datasource": { "type": "loki", "uid": "loki" },
"expr": "sum(count_over_time({job=\"gunicorn-access\"} |~ \" 200 \"[$__range]))",
"queryType": "range",
"refId": "A"
}
],
"title": "✅ 成功請求",
"type": "stat",
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null }
]
}
}
}
},
{
"gridPos": { "h": 6, "w": 24, "x": 0, "y": 6 },
"id": 7,
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "single", "sort": "none" }
},
"targets": [
{
"datasource": { "type": "loki", "uid": "loki" },
"expr": "sum(count_over_time({job=\"gunicorn-access\"}[1m]))",
"legendFormat": "HTTP Requests",
"queryType": "range",
"refId": "A"
}
],
"title": "📈 HTTP 請求趨勢",
"type": "timeseries",
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "bars",
"fillOpacity": 80
}
}
}
},
{
"gridPos": { "h": 10, "w": 24, "x": 0, "y": 12 },
"id": 9,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"targets": [
{
"datasource": { "type": "loki", "uid": "loki" },
"expr": "{job=\"gunicorn-access\"}",
"queryType": "range",
"refId": "A"
}
],
"title": "📝 Gunicorn 訪問日誌",
"type": "logs"
},
{
"gridPos": { "h": 10, "w": 24, "x": 0, "y": 22 },
"id": 10,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"targets": [
{
"datasource": { "type": "loki", "uid": "loki" },
"expr": "{job=\"momo-app\"}",
"queryType": "range",
"refId": "A"
}
],
"title": "📝 應用程式日誌",
"type": "logs"
},
{
"gridPos": { "h": 10, "w": 24, "x": 0, "y": 32 },
"id": 6,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": false,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"targets": [
{
"datasource": { "type": "loki", "uid": "loki" },
"expr": "{job=\"gunicorn-error\"}",
"queryType": "range",
"refId": "A"
}
],
"title": "🚨 Gunicorn 錯誤日誌",
"type": "logs"
}
],
"refresh": "30s",
"schemaVersion": 38,
"style": "dark",
"tags": ["momo", "logs", "loki"],
"templating": { "list": [] },
"time": { "from": "now-1h", "to": "now" },
"timepicker": {},
"timezone": "Asia/Taipei",
"title": "Momo Pro - 日誌監控",
"uid": "momo-logs",
"version": 3
}

View File

@@ -0,0 +1,675 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
"id": 100,
"panels": [],
"title": "主機資源 (UAT Server)",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 70 },
{ "color": "red", "value": 85 }
]
},
"unit": "percent"
}
},
"gridPos": { "h": 6, "w": 6, "x": 0, "y": 1 },
"id": 1,
"options": {
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"title": "CPU 使用率",
"type": "gauge",
"targets": [
{
"expr": "100 - (avg(irate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
"legendFormat": "CPU %",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 70 },
{ "color": "red", "value": 85 }
]
},
"unit": "percent"
}
},
"gridPos": { "h": 6, "w": 6, "x": 6, "y": 1 },
"id": 2,
"options": {
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"title": "記憶體使用率",
"type": "gauge",
"targets": [
{
"expr": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100",
"legendFormat": "Memory %",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 70 },
{ "color": "red", "value": 85 }
]
},
"unit": "percent"
}
},
"gridPos": { "h": 6, "w": 6, "x": 12, "y": 1 },
"id": 3,
"options": {
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"title": "磁碟使用率 (/)",
"type": "gauge",
"targets": [
{
"expr": "(1 - (node_filesystem_avail_bytes{mountpoint=\"/\",fstype!=\"rootfs\"} / node_filesystem_size_bytes{mountpoint=\"/\",fstype!=\"rootfs\"})) * 100",
"legendFormat": "Disk %",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null }
]
},
"unit": "dtdurations"
}
},
"gridPos": { "h": 6, "w": 6, "x": 18, "y": 1 },
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "系統運行時間",
"type": "stat",
"targets": [
{
"expr": "node_time_seconds - node_boot_time_seconds",
"legendFormat": "Uptime",
"refId": "A"
}
]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 7 },
"id": 101,
"panels": [],
"title": "網站健康狀態",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "color": "red", "index": 1, "text": "DOWN" }, "1": { "color": "green", "index": 0, "text": "UP" } }, "type": "value" }
],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
}
},
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 8 },
"id": 10,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "mo.wooo.work (UAT)",
"type": "stat",
"targets": [
{
"expr": "probe_success{instance=\"https://mo.wooo.work\"}",
"legendFormat": "",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "color": "red", "index": 1, "text": "DOWN" }, "1": { "color": "green", "index": 0, "text": "UP" } }, "type": "value" }
],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
}
},
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 8 },
"id": 11,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "mo.wooo.work (已廢棄)",
"type": "stat",
"targets": [
{
"expr": "probe_success{instance=\"https://mo.wooo.work\"}",
"legendFormat": "",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "color": "red", "index": 1, "text": "DOWN" }, "1": { "color": "green", "index": 0, "text": "UP" } }, "type": "value" }
],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
}
},
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 8 },
"id": 12,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "wooo.work (公司官網)",
"type": "stat",
"targets": [
{
"expr": "probe_success{instance=\"https://wooo.work\"}",
"legendFormat": "",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "color": "red", "index": 1, "text": "DOWN" }, "1": { "color": "green", "index": 0, "text": "UP" } }, "type": "value" }
],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
}
},
"gridPos": { "h": 4, "w": 4, "x": 12, "y": 8 },
"id": 13,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "UAT Server (Ping)",
"type": "stat",
"targets": [
{
"expr": "probe_success{instance=\"192.168.0.110\", probe_type=\"icmp\"}",
"legendFormat": "",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "color": "red", "index": 1, "text": "DOWN" }, "1": { "color": "green", "index": 0, "text": "UP" } }, "type": "value" }
],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
}
},
"gridPos": { "h": 4, "w": 4, "x": 16, "y": 8 },
"id": 14,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "GCP PROD (Ping)",
"type": "stat",
"targets": [
{
"expr": "probe_success{instance=\"34.80.130.190\", probe_type=\"icmp\"}",
"legendFormat": "",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "options": { "0": { "color": "red", "index": 1, "text": "FAIL" }, "1": { "color": "green", "index": 0, "text": "OK" } }, "type": "value" }
],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
}
},
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 8 },
"id": 15,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"title": "DNS 解析",
"type": "stat",
"targets": [
{
"expr": "probe_success{job=\"blackbox-dns\"}",
"legendFormat": "",
"refId": "A"
}
]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 12 },
"id": 102,
"panels": [],
"title": "系統歷史趨勢",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "opacity",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "percent"
}
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 13 },
"id": 20,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"title": "CPU 使用率趨勢",
"type": "timeseries",
"targets": [
{
"expr": "100 - (avg(irate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
"legendFormat": "CPU %",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "opacity",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "percent"
}
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 13 },
"id": 21,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"title": "記憶體使用率趨勢",
"type": "timeseries",
"targets": [
{
"expr": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100",
"legendFormat": "Memory %",
"refId": "A"
}
]
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 21 },
"id": 103,
"panels": [],
"title": "網路與響應時間",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "s"
}
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 22 },
"id": 30,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"title": "網站響應時間",
"type": "timeseries",
"targets": [
{
"expr": "probe_duration_seconds{job=~\"blackbox-http.*\"}",
"legendFormat": "{{instance}}",
"refId": "A"
}
]
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "Bps"
}
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 22 },
"id": 31,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"title": "網路流量",
"type": "timeseries",
"targets": [
{
"expr": "rate(node_network_receive_bytes_total{device!~\"lo|docker.*|br.*|veth.*\"}[5m])",
"legendFormat": "{{device}} RX",
"refId": "A"
},
{
"expr": "rate(node_network_transmit_bytes_total{device!~\"lo|docker.*|br.*|veth.*\"}[5m])",
"legendFormat": "{{device}} TX",
"refId": "B"
}
]
}
],
"refresh": "30s",
"schemaVersion": 38,
"tags": ["wooo", "system", "overview"],
"templating": { "list": [] },
"time": { "from": "now-1h", "to": "now" },
"timepicker": {},
"timezone": "Asia/Taipei",
"title": "WOOO 系統總覽",
"uid": "wooo-system-overview",
"version": 1,
"weekStart": ""
}

View File

@@ -0,0 +1,40 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# Grafana Datasources Configuration
# =============================================================================
apiVersion: 1
datasources:
# ---------------------------------------------------------------------------
# Prometheus - 指標數據源(主要)
# ---------------------------------------------------------------------------
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
jsonData:
timeInterval: "15s"
queryTimeout: "60s"
httpMethod: "POST"
# ---------------------------------------------------------------------------
# Loki - 日誌數據源
# ---------------------------------------------------------------------------
- name: Loki
uid: loki
type: loki
access: proxy
url: http://loki:3100
isDefault: false
editable: false
jsonData:
maxLines: 1000
derivedFields:
- datasourceUid: prometheus
matcherRegex: "traceID=(\\w+)"
name: TraceID
url: "$${__value.raw}"

View File

@@ -0,0 +1,58 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# Loki Configuration
# =============================================================================
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://localhost:9093
# 日誌保留策略
limits_config:
retention_period: 168h # 7 天
enforce_metric_name: false
reject_old_samples: true
reject_old_samples_max_age: 168h
max_entries_limit_per_query: 5000
compactor:
working_directory: /loki/compactor
shared_store: filesystem
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150

View File

@@ -0,0 +1,268 @@
# =============================================================================
# WOOO TECH - Monitoring Services Nginx Configuration (HTTP Only)
# 所有監控服務統一入口
# 使用動態 DNS 解析,避免服務不存在時 nginx 無法啟動
# =============================================================================
# HTTP 監控服務入口
server {
listen 80;
server_name mon.wooo.work localhost;
# Docker 內部 DNS resolver
resolver 127.0.0.11 valid=30s ipv6=off;
# 從上游 nginx 獲取真實客戶端 IP
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.168.0.0/24;
real_ip_header X-Real-IP;
real_ip_recursive on;
# 安全標頭
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# 日誌
access_log /var/log/nginx/monitor-access.log;
error_log /var/log/nginx/monitor-error.log;
# 靜態檔案 (Logo 等)
location /static/ {
alias /usr/share/nginx/html/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# 根路徑 - 顯示服務列表 (使用靜態 HTML 檔案)
location = / {
root /usr/share/nginx/html;
try_files /index.html =404;
}
# =========================================
# 視覺化與分析
# =========================================
# Grafana (代理到 Grafana 的 /grafana/ 路徑,因為 Grafana 配置了 SERVE_FROM_SUB_PATH)
location /grafana/ {
set $grafana_backend "http://momo-grafana:3000";
proxy_pass $grafana_backend$request_uri;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支援 (Grafana Live)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}
# Prometheus (route-prefix=/ 所以要 rewrite 掉 /prometheus 前綴)
location /prometheus/ {
set $prometheus_backend "http://momo-prometheus:9090";
rewrite ^/prometheus/(.*) /$1 break;
proxy_pass $prometheus_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Prometheus 根路徑重定向
location = /prometheus {
return 301 /prometheus/;
}
# Loki API (僅供 Grafana 使用,無 Web UI)
location /loki/ {
set $loki_backend "http://momo-loki:3100";
rewrite ^/loki/(.*) /$1 break;
proxy_pass $loki_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Loki ready 健康檢查
location = /loki/ready {
set $loki_backend "http://momo-loki:3100";
proxy_pass $loki_backend/ready;
proxy_http_version 1.1;
}
# =========================================
# 系統管理
# =========================================
# Portainer (無法正常使用子路徑,建議直接訪問)
location /portainer/ {
set $portainer_backend "http://momo-portainer:9000";
rewrite ^/portainer/(.*) /$1 break;
proxy_pass $portainer_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支援
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# SQLite Web (僅限內網訪問 - 資料庫安全考量)
location /sqlite/ {
# 內網 IP 白名單
allow 192.168.0.0/24;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 127.0.0.1;
deny all;
set $sqlite_backend "http://momo-sqlite-web:8080";
# 移除 /sqlite 前綴後代理到後端
rewrite ^/sqlite/(.*) /$1 break;
proxy_pass $sqlite_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Script-Name /sqlite;
# 重寫內部連結路徑 (順序重要:具體規則在前)
sub_filter 'href="/static/' 'href="/sqlite/static/';
sub_filter 'src="/static/' 'src="/sqlite/static/';
sub_filter 'action="/' 'action="/sqlite/';
sub_filter 'href="/' 'href="/sqlite/';
sub_filter_once off;
sub_filter_types text/html text/css application/javascript;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# SQLite Web 靜態資源 (僅限內網)
location /sqlite/static/ {
allow 192.168.0.0/24;
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 127.0.0.1;
deny all;
set $sqlite_backend "http://momo-sqlite-web:8080";
rewrite ^/sqlite/static/(.*) /static/$1 break;
proxy_pass $sqlite_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
expires 7d;
}
# =========================================
# Exporters
# =========================================
# cAdvisor
location /cadvisor/ {
set $cadvisor_backend "http://momo-cadvisor:8080";
proxy_pass $cadvisor_backend/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Node Exporter
location /node-exporter/ {
set $node_exporter_backend "http://momo-node-exporter:9100";
proxy_pass $node_exporter_backend/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Blackbox Exporter
location /blackbox/ {
set $blackbox_backend "http://momo-blackbox-exporter:9115";
proxy_pass $blackbox_backend/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 健康檢查
location /health {
access_log off;
return 200 'OK';
add_header Content-Type text/plain;
}
# 服務不可用時的友善錯誤頁面
error_page 502 503 504 @service_unavailable;
location @service_unavailable {
default_type 'text/html; charset=utf-8';
return 503 '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Service Unavailable</title>
<style>
body { font-family: -apple-system, sans-serif; background: linear-gradient(135deg, #1e3c72, #2a5298); min-height: 100vh; margin: 0; display: flex; align-items: center; justify-content: center; color: white; }
.container { text-align: center; padding: 40px; }
h1 { margin-bottom: 20px; }
p { opacity: 0.8; }
a { color: #fff; }
</style>
</head>
<body>
<div class="container">
<h1>Service Unavailable</h1>
<p>The requested monitoring service is not running.</p>
<p>Please start monitoring services with:</p>
<p><code>docker-compose --profile monitoring up -d</code></p>
<p><a href="/">Back to Service List</a></p>
</div>
</body>
</html>';
}
}

View File

@@ -0,0 +1,362 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WOOO Monitoring Services</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #1e3c72;
--secondary-color: #2a5298;
--accent-color: #00d4ff;
--card-bg: rgba(255, 255, 255, 0.95);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
min-height: 100vh;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 50%, #1a1a2e 100%);
background-attachment: fixed;
position: relative;
overflow-x: hidden;
}
/* 背景裝飾 */
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 80%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(0, 212, 255, 0.05) 0%, transparent 30%);
pointer-events: none;
z-index: 0;
}
/* 動態粒子效果 */
.particles {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 0;
pointer-events: none;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
background: rgba(0, 212, 255, 0.6);
border-radius: 50%;
animation: float 15s infinite;
}
@keyframes float {
0%, 100% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-100vh) rotate(720deg); opacity: 0; }
}
.main-container {
position: relative;
z-index: 1;
padding: 40px 20px;
min-height: 100vh;
}
/* Header */
.header {
text-align: center;
margin-bottom: 50px;
}
.logo-container {
margin-bottom: 20px;
}
.logo {
width: 200px;
height: auto;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.header h1 {
color: white;
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
}
.header .subtitle {
color: rgba(255, 255, 255, 0.8);
font-size: 1.1rem;
}
/* Section */
.section {
margin-bottom: 40px;
}
.section-title {
color: white;
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid rgba(0, 212, 255, 0.3);
display: flex;
align-items: center;
gap: 10px;
}
.section-title i {
color: var(--accent-color);
}
/* Cards Grid */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
/* Service Card */
.service-card {
background: var(--card-bg);
border-radius: 16px;
padding: 24px;
text-decoration: none;
color: #333;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: flex-start;
gap: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
}
.service-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--accent-color), var(--secondary-color));
transform: scaleX(0);
transition: transform 0.3s ease;
}
.service-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
color: #333;
text-decoration: none;
}
.service-card:hover::before {
transform: scaleX(1);
}
.card-icon {
width: 50px;
height: 50px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
flex-shrink: 0;
}
.card-icon.grafana { background: linear-gradient(135deg, #f46800, #f9b233); color: white; }
.card-icon.prometheus { background: linear-gradient(135deg, #e6522c, #f0a000); color: white; }
.card-icon.portainer { background: linear-gradient(135deg, #13bef9, #0db7ed); color: white; }
.card-icon.sqlite { background: linear-gradient(135deg, #003b57, #044a6c); color: white; }
.card-icon.cadvisor { background: linear-gradient(135deg, #4285f4, #34a853); color: white; }
.card-icon.node { background: linear-gradient(135deg, #e84d3d, #c0392b); color: white; }
.card-icon.blackbox { background: linear-gradient(135deg, #9b59b6, #8e44ad); color: white; }
.card-content {
flex: 1;
}
.card-content h3 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 6px;
color: #1a1a2e;
}
.card-content p {
font-size: 0.9rem;
color: #666;
margin: 0;
line-height: 1.4;
}
.card-badge {
position: absolute;
top: 12px;
right: 12px;
font-size: 0.7rem;
padding: 3px 8px;
border-radius: 20px;
background: rgba(0, 212, 255, 0.1);
color: var(--secondary-color);
font-weight: 500;
}
/* Footer */
.footer {
text-align: center;
margin-top: 60px;
padding: 20px;
color: rgba(255, 255, 255, 0.6);
font-size: 0.85rem;
}
.footer a {
color: var(--accent-color);
text-decoration: none;
}
/* Responsive */
@media (max-width: 768px) {
.header h1 { font-size: 1.8rem; }
.cards-grid { grid-template-columns: 1fr; }
.main-container { padding: 20px 15px; }
}
</style>
</head>
<body>
<!-- 粒子背景 -->
<div class="particles">
<div class="particle" style="left: 10%; animation-delay: 0s;"></div>
<div class="particle" style="left: 20%; animation-delay: 2s;"></div>
<div class="particle" style="left: 30%; animation-delay: 4s;"></div>
<div class="particle" style="left: 40%; animation-delay: 1s;"></div>
<div class="particle" style="left: 50%; animation-delay: 3s;"></div>
<div class="particle" style="left: 60%; animation-delay: 5s;"></div>
<div class="particle" style="left: 70%; animation-delay: 2.5s;"></div>
<div class="particle" style="left: 80%; animation-delay: 4.5s;"></div>
<div class="particle" style="left: 90%; animation-delay: 1.5s;"></div>
</div>
<div class="main-container">
<div class="container">
<!-- Header -->
<div class="header">
<div class="logo-container">
<img src="/static/images/WOOO_Logo_trimmed.jpg" alt="WOOO Logo" class="logo">
</div>
<h1>Monitoring Services</h1>
<p class="subtitle"><i class="fas fa-server me-2"></i>WOOO TECH 監控服務中心</p>
</div>
<!-- 視覺化與分析 -->
<div class="section">
<h2 class="section-title"><i class="fas fa-chart-line"></i>視覺化與分析</h2>
<div class="cards-grid">
<a href="/grafana/" class="service-card">
<div class="card-icon grafana"><i class="fas fa-chart-area"></i></div>
<div class="card-content">
<h3>Grafana</h3>
<p>視覺化儀表板,整合 Loki 日誌查詢與 Prometheus 指標</p>
</div>
</a>
<a href="/prometheus/" class="service-card">
<div class="card-icon prometheus"><i class="fas fa-fire"></i></div>
<div class="card-content">
<h3>Prometheus</h3>
<p>時序資料庫,收集與查詢系統指標</p>
</div>
</a>
</div>
</div>
<!-- 系統管理 -->
<div class="section">
<h2 class="section-title"><i class="fas fa-cogs"></i>系統管理</h2>
<div class="cards-grid">
<a href="http://192.168.0.110:9000/" class="service-card" target="_blank">
<div class="card-icon portainer"><i class="fab fa-docker"></i></div>
<div class="card-content">
<h3>Portainer</h3>
<p>Docker 容器視覺化管理介面</p>
</div>
<span class="card-badge">直連</span>
</a>
<a href="/sqlite/" class="service-card">
<div class="card-icon sqlite"><i class="fas fa-database"></i></div>
<div class="card-content">
<h3>SQLite Web</h3>
<p>資料庫瀏覽與管理工具</p>
</div>
</a>
</div>
</div>
<!-- Exporters -->
<div class="section">
<h2 class="section-title"><i class="fas fa-download"></i>Exporters</h2>
<div class="cards-grid">
<a href="http://192.168.0.110:8080/" class="service-card" target="_blank">
<div class="card-icon cadvisor"><i class="fas fa-cube"></i></div>
<div class="card-content">
<h3>cAdvisor</h3>
<p>容器資源監控與效能分析</p>
</div>
<span class="card-badge">直連</span>
</a>
<a href="/node-exporter/metrics" class="service-card">
<div class="card-icon node"><i class="fas fa-microchip"></i></div>
<div class="card-content">
<h3>Node Exporter</h3>
<p>主機系統指標 (CPU、記憶體、磁碟)</p>
</div>
</a>
<a href="/blackbox/" class="service-card">
<div class="card-icon blackbox"><i class="fas fa-satellite-dish"></i></div>
<div class="card-content">
<h3>Blackbox Exporter</h3>
<p>HTTP/HTTPS/TCP 端點探測監控</p>
</div>
</a>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p>&copy; 2026 WOOO TECH. All rights reserved.</p>
<p><a href="https://mo.wooo.work/">返回 Momo Pro System</a></p>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

View File

@@ -0,0 +1,44 @@
# =============================================================================
# WOOO TECH - Monitoring Nginx Configuration
# 專用於監控服務,與主站分離
# =============================================================================
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日誌格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# 基本設定
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip 壓縮
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml;
# 包含站點配置
include /etc/nginx/conf.d/*.conf;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

View File

@@ -0,0 +1,69 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# Nginx Site Configuration
# =============================================================================
upstream momo_app {
server momo-app:5000;
keepalive 32;
}
server {
listen 80;
server_name localhost;
# 日誌
access_log /var/log/nginx/momo-access.log main;
error_log /var/log/nginx/momo-error.log;
# 靜態檔案
location /static/ {
alias /app/static/;
expires 7d;
add_header Cache-Control "public, immutable";
}
location /web/static/ {
alias /app/web/static/;
expires 7d;
add_header Cache-Control "public, immutable";
}
# 反向代理到 Flask 應用
location / {
proxy_pass http://momo_app;
proxy_http_version 1.1;
# Proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
# Timeout 設定(配合長時間查詢)
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
# Buffer 設定
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
# 健康檢查端點
location /health {
access_log off;
proxy_pass http://momo_app;
proxy_connect_timeout 5s;
proxy_read_timeout 5s;
}
# 錯誤頁面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@@ -0,0 +1,75 @@
#!/bin/bash
# 健康檢查 API - 由 Nginx fcgiwrap 執行
# 用法: /api/health?service=<service_name>
# 設定 HTTP headers
echo "Content-Type: application/json"
echo "Access-Control-Allow-Origin: *"
echo "Access-Control-Allow-Methods: GET"
echo "Cache-Control: no-cache"
echo ""
# 解析查詢參數
SERVICE=$(echo "$QUERY_STRING" | sed -n 's/.*service=\([^&]*\).*/\1/p')
# 定義服務健康檢查 URL
declare -A HEALTH_URLS=(
["momo-uat"]="https://mo.wooo.work/health"
["momo-gcp"]="https://momo.wooo.work/health"
["gitlab"]="http://127.0.0.1:8929/"
["registry"]="http://127.0.0.1:5002/v2/"
["n8n"]="http://127.0.0.1:5678/"
["grafana"]="http://127.0.0.1:30030/"
["prometheus"]="http://10.43.25.78:9090/-/healthy"
["alertmanager"]="http://10.43.79.187:9093/-/healthy"
["superset"]="http://127.0.0.1:8088/health"
["metabase"]="http://127.0.0.1:3030/api/health"
)
# 檢查服務
if [[ -z "$SERVICE" ]]; then
# 返回所有服務狀態
echo '{"services": {'
first=true
for svc in "${!HEALTH_URLS[@]}"; do
url="${HEALTH_URLS[$svc]}"
start_time=$(date +%s%3N)
response=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 "$url" 2>/dev/null)
end_time=$(date +%s%3N)
response_time=$((end_time - start_time))
if [[ "$response" == "200" ]] || [[ "$response" == "302" ]] || [[ "$response" == "401" ]]; then
status="online"
else
status="offline"
fi
if [ "$first" = true ]; then
first=false
else
echo ","
fi
echo -n "\"$svc\": {\"status\": \"$status\", \"code\": $response, \"responseTime\": $response_time}"
done
echo '}}'
else
# 返回單個服務狀態
url="${HEALTH_URLS[$SERVICE]}"
if [[ -z "$url" ]]; then
echo '{"error": "Unknown service", "service": "'"$SERVICE"'"}'
exit 0
fi
start_time=$(date +%s%3N)
response=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 "$url" 2>/dev/null)
end_time=$(date +%s%3N)
response_time=$((end_time - start_time))
if [[ "$response" == "200" ]] || [[ "$response" == "302" ]] || [[ "$response" == "401" ]]; then
status="online"
else
status="offline"
fi
echo "{\"service\": \"$SERVICE\", \"status\": \"$status\", \"code\": $response, \"responseTime\": $response_time}"
fi

View File

@@ -0,0 +1,58 @@
#!/bin/bash
# 健康檢查 API
# 輸出 JSON 格式的服務狀態
echo "Content-Type: application/json"
echo "Access-Control-Allow-Origin: *"
echo "Cache-Control: no-cache, no-store, must-revalidate"
echo ""
check_service() {
local name=$1
local url=$2
local start_time=$(date +%s%N)
local response=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 3 --max-time 5 "$url" 2>/dev/null)
local end_time=$(date +%s%N)
local response_time=$(( (end_time - start_time) / 1000000 ))
if [[ "$response" == "200" ]] || [[ "$response" == "302" ]] || [[ "$response" == "401" ]]; then
echo "\"$name\": {\"status\": \"online\", \"code\": $response, \"responseTime\": $response_time}"
else
echo "\"$name\": {\"status\": \"offline\", \"code\": $response, \"responseTime\": $response_time}"
fi
}
echo '{"services": {'
# 核心服務
check_service "momo-uat" "https://mo.wooo.work/health"
echo ","
check_service "momo-gcp" "https://momo.wooo.work/health"
echo ","
# 開發工具
check_service "gitlab" "http://127.0.0.1:8929/"
echo ","
check_service "registry" "http://127.0.0.1:5002/v2/"
echo ","
check_service "n8n" "http://127.0.0.1:5678/"
echo ","
# 監控服務
check_service "grafana" "http://127.0.0.1:30030/"
echo ","
check_service "prometheus" "http://10.43.25.78:9090/-/healthy"
echo ","
check_service "alertmanager" "http://10.43.79.187:9093/-/healthy"
echo ","
# BI 平台
check_service "superset" "http://127.0.0.1:8088/health"
echo ","
check_service "metabase" "http://127.0.0.1:3030/api/health"
echo '},'
echo "\"timestamp\": \"$(date -Iseconds)\""
echo '}'

View File

@@ -0,0 +1,804 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MOMO Pro - 監控中心</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #17a2b8;
}
body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header {
background: var(--primary-gradient);
padding: 2rem 0;
margin-bottom: 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.header h1 {
font-weight: 700;
margin-bottom: 0.5rem;
}
.header .subtitle {
opacity: 0.9;
font-size: 1.1rem;
}
.section-title {
color: #fff;
font-weight: 600;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
gap: 0.75rem;
}
.section-title i {
font-size: 1.5rem;
}
.service-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.service-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.2);
}
.service-card h5 {
font-weight: 600;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.service-card .description {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
margin-bottom: 1rem;
}
.service-card .btn {
font-size: 0.85rem;
padding: 0.5rem 1rem;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
}
.status-running {
background: rgba(40, 167, 69, 0.2);
color: #28a745;
border: 1px solid rgba(40, 167, 69, 0.3);
}
.status-checking {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
border: 1px solid rgba(255, 193, 7, 0.3);
}
.env-badge {
font-size: 0.7rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.env-uat {
background: #007bff;
color: #fff;
}
.env-gcp {
background: #dc3545;
color: #fff;
}
.btn-service {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
}
.btn-service:hover {
background: rgba(255, 255, 255, 0.25);
color: #fff;
border-color: rgba(255, 255, 255, 0.3);
}
.btn-primary-custom {
background: var(--primary-gradient);
border: none;
color: #fff;
}
.btn-primary-custom:hover {
opacity: 0.9;
color: #fff;
}
.footer {
text-align: center;
padding: 2rem 0;
color: rgba(255, 255, 255, 0.5);
font-size: 0.85rem;
}
.refresh-info {
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.6);
}
.quick-links {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.quick-link {
background: rgba(255, 255, 255, 0.1);
padding: 0.4rem 0.8rem;
border-radius: 6px;
font-size: 0.8rem;
color: #fff;
text-decoration: none;
transition: all 0.2s;
}
.quick-link:hover {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
@media (max-width: 768px) {
.header {
padding: 1.5rem 0;
}
.service-card {
padding: 1rem;
}
}
</style>
</head>
<body>
<div class="header">
<div class="container">
<div class="d-flex justify-content-between align-items-center flex-wrap">
<div>
<h1><i class="fas fa-satellite-dish me-2"></i>MOMO Pro 監控中心</h1>
<p class="subtitle mb-0">統一監控 UAT + GCP 雙環境</p>
</div>
<div class="text-end">
<div class="refresh-info">
<i class="fas fa-sync-alt me-1"></i>
上次更新: <span id="lastUpdate">-</span>
</div>
<button class="btn btn-light btn-sm mt-2" onclick="location.reload()">
<i class="fas fa-refresh me-1"></i>重新整理
</button>
</div>
</div>
</div>
</div>
<div class="container">
<!-- 應用服務 -->
<h3 class="section-title">
<i class="fas fa-rocket text-primary"></i>
應用服務
</h3>
<div class="row">
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-store text-info"></i>
MOMO Pro System
<span class="env-badge env-uat">UAT</span>
</h5>
<p class="description">測試環境 - 商品看板與業績分析系統</p>
<div class="d-flex gap-2 flex-wrap">
<a href="https://mo.wooo.work" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟系統
</a>
<a href="https://mo.wooo.work/health" target="_blank" class="btn btn-service btn-sm">
<i class="fas fa-heartbeat me-1"></i>健康檢查
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-store text-danger"></i>
MOMO Pro System
<span class="env-badge env-gcp">GCP</span>
</h5>
<p class="description">正式環境 - 商品看板與業績分析系統</p>
<div class="d-flex gap-2 flex-wrap">
<a href="https://momo.wooo.work" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟系統
</a>
<a href="https://momo.wooo.work/health" target="_blank" class="btn btn-service btn-sm">
<i class="fas fa-heartbeat me-1"></i>健康檢查
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-chart-bar text-warning"></i>
Apache Superset
</h5>
<p class="description">BI 分析儀表板 - 資料視覺化平台</p>
<div class="d-flex gap-2 flex-wrap">
<a href="/superset/" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Superset
</a>
</div>
</div>
</div>
</div>
<!-- 開發工具 -->
<h3 class="section-title mt-4">
<i class="fas fa-code text-success"></i>
開發工具
</h3>
<div class="row">
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fab fa-gitlab text-warning"></i>
GitLab
</h5>
<p class="description">Git 版本控制 + CI/CD 自動化部署</p>
<div class="d-flex gap-2 flex-wrap">
<a href="http://192.168.0.110:8929" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 GitLab
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fab fa-docker text-info"></i>
Docker Registry
</h5>
<p class="description">私有容器映像倉庫</p>
<div class="d-flex gap-2 flex-wrap">
<a href="https://registry.wooo.work" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Registry
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-project-diagram text-success"></i>
n8n
</h5>
<p class="description">自動化工作流程引擎 (29 個工作流程)</p>
<div class="d-flex gap-2 flex-wrap">
<a href="http://192.168.0.110:5678" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 n8n
</a>
</div>
</div>
</div>
</div>
<!-- 監控服務 -->
<h3 class="section-title mt-4">
<i class="fas fa-chart-line text-danger"></i>
監控服務 (K8s)
</h3>
<div class="row">
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-tachometer-alt text-warning"></i>
Grafana
</h5>
<p class="description">監控儀表板 - K8s 叢集視覺化</p>
<div class="d-flex gap-2 flex-wrap">
<a href="http://192.168.0.110:30030" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Grafana
</a>
</div>
<div class="quick-links">
<span class="quick-link"><i class="fas fa-info-circle me-1"></i>請參考 CLAUDE.md 取得登入資訊</span>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-fire text-danger"></i>
Prometheus
</h5>
<p class="description">時序資料庫 - 指標收集與查詢</p>
<div class="d-flex gap-2 flex-wrap">
<a href="/prometheus/" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Prometheus
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-bell text-info"></i>
Alertmanager
</h5>
<p class="description">告警管理 - 整合 Telegram 通知</p>
<div class="d-flex gap-2 flex-wrap">
<a href="/alertmanager/" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Alertmanager
</a>
</div>
</div>
</div>
</div>
<!-- 容器管理與日誌 -->
<h3 class="section-title mt-4">
<i class="fab fa-docker text-info"></i>
容器管理與日誌
</h3>
<div class="row">
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-cubes text-primary"></i>
Portainer
</h5>
<p class="description">Docker 容器管理平台</p>
<div class="d-flex gap-2 flex-wrap">
<a href="/portainer/" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Portainer
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-scroll text-success"></i>
Loki
</h5>
<p class="description">日誌聚合系統 (Grafana 整合)</p>
<div class="d-flex gap-2 flex-wrap">
<a href="/loki/" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Loki
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-microchip text-warning"></i>
cAdvisor
</h5>
<p class="description">容器資源監控</p>
<div class="d-flex gap-2 flex-wrap">
<a href="http://192.168.0.110:8080" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 cAdvisor
</a>
</div>
</div>
</div>
</div>
<!-- BI 分析平台 -->
<h3 class="section-title mt-4">
<i class="fas fa-chart-pie text-warning"></i>
BI 分析平台
</h3>
<div class="row">
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-table text-info"></i>
Metabase
</h5>
<p class="description">資料分析與視覺化平台</p>
<div class="d-flex gap-2 flex-wrap">
<a href="/metabase/" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Metabase
</a>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-tachometer-alt text-success"></i>
Docker Grafana
</h5>
<p class="description">Docker 版監控儀表板</p>
<div class="d-flex gap-2 flex-wrap">
<a href="/grafana/" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Grafana
</a>
</div>
</div>
</div>
</div>
<!-- 檔案與協作 -->
<h3 class="section-title mt-4">
<i class="fas fa-cloud text-primary"></i>
檔案與協作
</h3>
<div class="row">
<div class="col-md-6 col-lg-4">
<div class="service-card">
<h5>
<i class="fas fa-cloud-upload-alt text-info"></i>
Nextcloud
</h5>
<p class="description">私有雲端檔案儲存</p>
<div class="d-flex gap-2 flex-wrap">
<a href="/nextcloud/" target="_blank" class="btn btn-primary-custom btn-sm">
<i class="fas fa-external-link-alt me-1"></i>開啟 Nextcloud
</a>
</div>
</div>
</div>
</div>
<!-- 系統狀態 -->
<h3 class="section-title mt-4">
<i class="fas fa-server text-info"></i>
系統狀態概覽
</h3>
<div class="row">
<div class="col-12">
<div class="service-card">
<h5><i class="fas fa-cube me-2"></i>K8s Pods 狀態</h5>
<div class="row mt-3">
<div class="col-md-6">
<h6 class="text-muted mb-2"><i class="fas fa-layer-group me-1"></i>momo namespace</h6>
<ul class="list-unstyled">
<li class="mb-1">
<span class="status-badge status-running"><i class="fas fa-circle"></i> Running</span>
momo-app
</li>
<li class="mb-1">
<span class="status-badge status-running"><i class="fas fa-circle"></i> Running</span>
momo-postgres
</li>
<li class="mb-1">
<span class="status-badge status-running"><i class="fas fa-circle"></i> Running</span>
momo-scheduler
</li>
<li class="mb-1">
<span class="status-badge status-running"><i class="fas fa-circle"></i> Running</span>
postgres-exporter
</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-2"><i class="fas fa-layer-group me-1"></i>monitoring namespace</h6>
<ul class="list-unstyled">
<li class="mb-1">
<span class="status-badge status-running"><i class="fas fa-circle"></i> Running</span>
prometheus-grafana
</li>
<li class="mb-1">
<span class="status-badge status-running"><i class="fas fa-circle"></i> Running</span>
alertmanager
</li>
<li class="mb-1">
<span class="status-badge status-running"><i class="fas fa-circle"></i> Running</span>
prometheus
</li>
<li class="mb-1">
<span class="status-badge status-running"><i class="fas fa-circle"></i> Running</span>
node-exporter
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 排程任務總覽 -->
<h3 class="section-title mt-4">
<i class="fas fa-clock text-info"></i>
排程任務總覽
</h3>
<div class="row">
<!-- Cron Jobs -->
<div class="col-lg-6">
<div class="service-card">
<h5><i class="fas fa-terminal me-2 text-warning"></i>Cron 排程</h5>
<div class="table-responsive mt-3">
<table class="table table-sm table-dark table-borderless mb-0">
<thead>
<tr class="text-muted small">
<th>頻率</th>
<th>任務</th>
</tr>
</thead>
<tbody class="small">
<tr>
<td><code>*/5 * * * *</code></td>
<td><i class="fas fa-heartbeat text-success me-1"></i>域名健康監控</td>
</tr>
<tr>
<td><code>*/5 * * * *</code></td>
<td><i class="fas fa-wrench text-warning me-1"></i>主自動修復 (UAT+GCP)</td>
</tr>
<tr>
<td><code>*/5 * * * *</code></td>
<td><i class="fab fa-docker text-info me-1"></i>Docker 健康監控</td>
</tr>
<tr>
<td><code>*/5 * * * *</code></td>
<td><i class="fas fa-dharmachakra text-primary me-1"></i>K8s 健康監控</td>
</tr>
<tr>
<td><code>0 */2 * * *</code></td>
<td><i class="fas fa-newspaper text-info me-1"></i>新聞抓取 (每2小時)</td>
</tr>
<tr>
<td><code>30 */3 * * *</code></td>
<td><i class="fas fa-robot text-purple me-1"></i>AI 處理 (每3小時)</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- n8n Workflows -->
<div class="col-lg-6">
<div class="service-card">
<h5><i class="fas fa-project-diagram me-2 text-success"></i>n8n 工作流程 (29 個)</h5>
<div class="table-responsive mt-3">
<table class="table table-sm table-dark table-borderless mb-0">
<thead>
<tr class="text-muted small">
<th>頻率</th>
<th>工作流程</th>
</tr>
</thead>
<tbody class="small">
<tr>
<td><code>每 5 分鐘</code></td>
<td><i class="fas fa-shield-alt text-success me-1"></i>雙環境健康監控</td>
</tr>
<tr>
<td><code>每 10 分鐘</code></td>
<td><i class="fas fa-cube text-info me-1"></i>K8s Pod 狀態監控</td>
</tr>
<tr>
<td><code>每 15 分鐘</code></td>
<td><i class="fas fa-database text-warning me-1"></i>PostgreSQL 慢查詢監控</td>
</tr>
<tr>
<td><code>每 30 分鐘</code></td>
<td><i class="fab fa-google-drive text-primary me-1"></i>Google Drive 匯入監控</td>
</tr>
<tr>
<td><code>每小時</code></td>
<td><i class="fas fa-hdd text-danger me-1"></i>磁碟空間監控</td>
</tr>
<tr>
<td><code>每日 09:00</code></td>
<td><i class="fas fa-file-alt text-info me-1"></i>每日系統報告</td>
</tr>
<tr>
<td><code>每日 09:00</code></td>
<td><i class="fas fa-certificate text-warning me-1"></i>SSL 證書監控</td>
</tr>
<tr>
<td><code>每週一 09:00</code></td>
<td><i class="fas fa-chart-line text-success me-1"></i>每週業績摘要</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-2">
<a href="http://192.168.0.110:5678" target="_blank" class="btn btn-service btn-sm">
<i class="fas fa-external-link-alt me-1"></i>查看所有工作流程
</a>
</div>
</div>
</div>
</div>
<!-- Python Scheduler -->
<div class="row mt-3">
<div class="col-12">
<div class="service-card">
<h5><i class="fab fa-python me-2 text-info"></i>Python Scheduler (momo-scheduler Pod)</h5>
<div class="row mt-3">
<div class="col-md-3">
<h6 class="text-muted small mb-2">每 30 分鐘</h6>
<ul class="list-unstyled small">
<li><i class="fab fa-google-drive text-primary me-1"></i>Google Drive 自動匯入</li>
<li><i class="fas fa-eye text-warning me-1"></i>網頁白頁監控</li>
</ul>
</div>
<div class="col-md-3">
<h6 class="text-muted small mb-2">每 1 小時</h6>
<ul class="list-unstyled small">
<li><i class="fas fa-store text-success me-1"></i>主站商品爬蟲</li>
<li><i class="fas fa-envelope text-info me-1"></i>EDM 限時搶購爬蟲</li>
</ul>
</div>
<div class="col-md-3">
<h6 class="text-muted small mb-2">每 6 小時</h6>
<ul class="list-unstyled small">
<li><i class="fas fa-gift text-danger me-1"></i>購物節活動爬蟲</li>
</ul>
</div>
<div class="col-md-3">
<h6 class="text-muted small mb-2">每日</h6>
<ul class="list-unstyled small">
<li><i class="fab fa-telegram text-info me-1"></i>每日業績 Telegram 通知</li>
<li><i class="fab fa-line text-success me-1"></i>每日業績 LINE 通知</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 自動修復機制 -->
<h3 class="section-title mt-4">
<i class="fas fa-wrench text-warning"></i>
自動修復機制
</h3>
<div class="row">
<div class="col-md-6">
<div class="service-card">
<h5><i class="fas fa-server me-2"></i><span class="env-badge env-uat me-2">UAT</span>UAT 環境修復</h5>
<div class="mt-3">
<table class="table table-sm table-dark table-borderless mb-0">
<tbody class="small">
<tr>
<td><i class="fas fa-memory text-danger me-1"></i>OOM Handler</td>
<td>每 15 分鐘</td>
<td><span class="status-badge status-running"><i class="fas fa-circle"></i></span></td>
</tr>
<tr>
<td><i class="fas fa-database text-info me-1"></i>PostgreSQL Repair</td>
<td>每 30 分鐘</td>
<td><span class="status-badge status-running"><i class="fas fa-circle"></i></span></td>
</tr>
<tr>
<td><i class="fas fa-undo text-warning me-1"></i>Auto Rollback</td>
<td>每 5 分鐘</td>
<td><span class="status-badge status-running"><i class="fas fa-circle"></i></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="service-card">
<h5><i class="fas fa-cloud me-2"></i><span class="env-badge env-gcp me-2">GCP</span>GCP 環境修復 (遠端)</h5>
<div class="mt-3">
<table class="table table-sm table-dark table-borderless mb-0">
<tbody class="small">
<tr>
<td><i class="fas fa-memory text-danger me-1"></i>OOM Handler GCP</td>
<td>每 15 分鐘</td>
<td><span class="status-badge status-running"><i class="fas fa-circle"></i></span></td>
</tr>
<tr>
<td><i class="fas fa-database text-info me-1"></i>PostgreSQL Repair GCP</td>
<td>每 30 分鐘</td>
<td><span class="status-badge status-running"><i class="fas fa-circle"></i></span></td>
</tr>
<tr>
<td><i class="fas fa-undo text-warning me-1"></i>Auto Rollback GCP</td>
<td>每 5 分鐘</td>
<td><span class="status-badge status-running"><i class="fas fa-circle"></i></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="service-card">
<h5><i class="fas fa-cog me-2"></i>修復能力總覽</h5>
<div class="row mt-3">
<div class="col-md-4">
<h6 class="text-muted small mb-2">記憶體問題</h6>
<p class="small mb-0"><i class="fas fa-check text-success me-1"></i>OOM 自動增加記憶體限制 +50%</p>
<p class="small mb-0"><i class="fas fa-check text-success me-1"></i>自動重啟 Pod</p>
</div>
<div class="col-md-4">
<h6 class="text-muted small mb-2">資料庫問題</h6>
<p class="small mb-0"><i class="fas fa-check text-success me-1"></i>連線失敗自動重啟</p>
<p class="small mb-0"><i class="fas fa-check text-success me-1"></i>死鎖自動終止查詢</p>
<p class="small mb-0"><i class="fas fa-check text-success me-1"></i>表膨脹自動 VACUUM</p>
</div>
<div class="col-md-4">
<h6 class="text-muted small mb-2">應用問題</h6>
<p class="small mb-0"><i class="fas fa-check text-success me-1"></i>5 次健康失敗自動回滾</p>
<p class="small mb-0"><i class="fas fa-check text-success me-1"></i>服務無回應自動重啟</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="footer">
<div class="container">
<p class="mb-1">MOMO Pro System © 2026 WOOO TECH</p>
<p class="mb-0">
<i class="fas fa-code me-1"></i>
UAT: mo.wooo.work | GCP: momo.wooo.work
</p>
</div>
</div>
<script>
// 更新時間顯示
function updateTime() {
const now = new Date();
const timeStr = now.toLocaleString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
document.getElementById('lastUpdate').textContent = timeStr;
}
updateTime();
setInterval(updateTime, 1000);
</script>
</body>
</html>

View File

@@ -0,0 +1,905 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WOOO Monitoring Services</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #1e3c72;
--secondary-color: #2a5298;
--accent-color: #00d4ff;
--card-bg: rgba(255, 255, 255, 0.95);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
min-height: 100vh;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 50%, #1a1a2e 100%);
background-attachment: fixed;
position: relative;
overflow-x: hidden;
}
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 80%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(0, 212, 255, 0.05) 0%, transparent 30%);
pointer-events: none;
z-index: 0;
}
.particles {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 0;
pointer-events: none;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
background: rgba(0, 212, 255, 0.6);
border-radius: 50%;
animation: float 15s infinite;
}
@keyframes float {
0%, 100% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateY(-100vh) rotate(720deg); opacity: 0; }
}
.main-container {
position: relative;
z-index: 1;
padding: 40px 20px;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo-container {
margin-bottom: 15px;
}
.logo {
width: 160px;
height: auto;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.header h1 {
color: white;
font-size: 2.2rem;
font-weight: 700;
margin-bottom: 8px;
text-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
}
.header .subtitle {
color: rgba(255, 255, 255, 0.8);
font-size: 1rem;
}
.last-update {
color: rgba(255, 255, 255, 0.6);
font-size: 0.85rem;
margin-top: 5px;
}
/* Status Overview */
.status-overview {
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 20px;
margin-bottom: 30px;
backdrop-filter: blur(10px);
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
}
.status-item {
text-align: center;
padding: 15px 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
transition: all 0.3s ease;
}
.status-item:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.status-icon {
font-size: 1.8rem;
margin-bottom: 8px;
}
.status-icon.healthy { color: #51cf66; }
.status-icon.unhealthy { color: #ff6b6b; }
.status-icon.loading { color: #ffd43b; animation: pulse 1s infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-label {
color: white;
font-size: 0.85rem;
font-weight: 500;
}
.status-value {
color: rgba(255, 255, 255, 0.7);
font-size: 0.75rem;
margin-top: 3px;
}
/* Alerts Panel */
.alerts-panel {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
margin-bottom: 30px;
}
.alerts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.alerts-title {
font-size: 1.1rem;
font-weight: 600;
color: #1a1a2e;
}
.alerts-count {
background: #ff6b6b;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
}
.alerts-count.zero {
background: #51cf66;
}
.alert-item {
display: flex;
align-items: flex-start;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
background: #fff5f5;
border-left: 4px solid #ff6b6b;
}
.alert-item.warning {
background: #fffbe6;
border-left-color: #ffd43b;
}
.alert-item.critical {
background: #fff0f0;
border-left-color: #e03131;
}
.alert-icon {
margin-right: 12px;
font-size: 1.2rem;
}
.alert-content {
flex: 1;
}
.alert-name {
font-weight: 600;
color: #333;
font-size: 0.95rem;
}
.alert-message {
color: #666;
font-size: 0.85rem;
margin-top: 3px;
}
.alert-instance {
color: #999;
font-size: 0.75rem;
margin-top: 5px;
}
.no-alerts {
text-align: center;
padding: 30px;
color: #51cf66;
}
.no-alerts i {
font-size: 2.5rem;
margin-bottom: 10px;
}
/* n8n Workflows Panel */
.workflows-panel {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
margin-bottom: 30px;
}
.workflow-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
margin-bottom: 8px;
background: #f8f9fa;
border-radius: 8px;
}
.workflow-name {
font-size: 0.9rem;
color: #333;
}
.workflow-status {
padding: 3px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
.workflow-status.active {
background: #d3f9d8;
color: #2b8a3e;
}
.workflow-status.inactive {
background: #ffe3e3;
color: #c92a2a;
}
/* Section */
.section {
margin-bottom: 30px;
}
.section-title {
color: white;
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid rgba(0, 212, 255, 0.3);
display: flex;
align-items: center;
gap: 10px;
}
.section-title i {
color: var(--accent-color);
}
/* Cards Grid */
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 15px;
}
/* Service Card */
.service-card {
background: var(--card-bg);
border-radius: 12px;
padding: 18px;
text-decoration: none;
color: #333;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: flex-start;
gap: 14px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
}
.service-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--accent-color), var(--secondary-color));
transform: scaleX(0);
transition: transform 0.3s ease;
}
.service-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
color: #333;
text-decoration: none;
}
.service-card:hover::before {
transform: scaleX(1);
}
.card-icon {
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
flex-shrink: 0;
}
.card-icon.grafana { background: linear-gradient(135deg, #f46800, #f9b233); color: white; }
.card-icon.prometheus { background: linear-gradient(135deg, #e6522c, #f0a000); color: white; }
.card-icon.portainer { background: linear-gradient(135deg, #13bef9, #0db7ed); color: white; }
.card-icon.pgadmin { background: linear-gradient(135deg, #336791, #0078d7); color: white; }
.card-icon.cadvisor { background: linear-gradient(135deg, #4285f4, #34a853); color: white; }
.card-icon.node { background: linear-gradient(135deg, #e84d3d, #c0392b); color: white; }
.card-icon.blackbox { background: linear-gradient(135deg, #9b59b6, #8e44ad); color: white; }
.card-icon.postgres { background: linear-gradient(135deg, #336791, #2f5e8d); color: white; }
.card-icon.gitlab { background: linear-gradient(135deg, #fc6d26, #e24329); color: white; }
.card-icon.registry { background: linear-gradient(135deg, #4a90d9, #60b044); color: white; }
.card-icon.watchtower { background: linear-gradient(135deg, #00b4d8, #0077b6); color: white; }
.card-icon.n8n { background: linear-gradient(135deg, #ea4b71, #ff6d5a); color: white; }
.card-icon.loki { background: linear-gradient(135deg, #f2c94c, #f2994a); color: white; }
.card-icon.superset { background: linear-gradient(135deg, #1fa8c9, #00A699); color: white; }
.card-icon.metabase { background: linear-gradient(135deg, #509ee3, #2d86d4); color: white; }
.card-icon.nextcloud { background: linear-gradient(135deg, #0082c9, #00639a); color: white; }
.card-icon.grist { background: linear-gradient(135deg, #16b378, #0d8957); color: white; }
.card-icon.alertmanager { background: linear-gradient(135deg, #e6522c, #c0392b); color: white; }
.card-icon.k8s { background: linear-gradient(135deg, #326ce5, #1d4cc4); color: white; }
.card-content {
flex: 1;
}
.card-content h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 4px;
color: #1a1a2e;
}
.card-content p {
font-size: 0.8rem;
color: #666;
margin: 0;
line-height: 1.3;
}
.card-badge {
position: absolute;
top: 10px;
right: 10px;
font-size: 0.65rem;
padding: 2px 6px;
border-radius: 12px;
background: rgba(0, 212, 255, 0.1);
color: var(--secondary-color);
font-weight: 500;
}
.card-status {
position: absolute;
bottom: 10px;
right: 10px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #adb5bd;
}
.card-status.healthy { background: #51cf66; box-shadow: 0 0 8px rgba(81, 207, 102, 0.5); }
.card-status.unhealthy { background: #ff6b6b; box-shadow: 0 0 8px rgba(255, 107, 107, 0.5); }
/* Footer */
.footer {
text-align: center;
margin-top: 40px;
padding: 20px;
color: rgba(255, 255, 255, 0.6);
font-size: 0.85rem;
}
.footer a {
color: var(--accent-color);
text-decoration: none;
}
/* Responsive */
@media (max-width: 768px) {
.header h1 { font-size: 1.6rem; }
.cards-grid { grid-template-columns: 1fr; }
.main-container { padding: 20px 15px; }
.status-grid { grid-template-columns: repeat(3, 1fr); }
}
</style>
</head>
<body>
<div class="particles">
<div class="particle" style="left: 10%; animation-delay: 0s;"></div>
<div class="particle" style="left: 20%; animation-delay: 2s;"></div>
<div class="particle" style="left: 30%; animation-delay: 4s;"></div>
<div class="particle" style="left: 40%; animation-delay: 1s;"></div>
<div class="particle" style="left: 50%; animation-delay: 3s;"></div>
<div class="particle" style="left: 60%; animation-delay: 5s;"></div>
<div class="particle" style="left: 70%; animation-delay: 2.5s;"></div>
<div class="particle" style="left: 80%; animation-delay: 4.5s;"></div>
<div class="particle" style="left: 90%; animation-delay: 1.5s;"></div>
</div>
<div class="main-container">
<div class="container">
<!-- Header -->
<div class="header">
<div class="logo-container">
<img src="/monitor-static/images/WOOO_Logo_trimmed.jpg" alt="WOOO Logo" class="logo">
</div>
<h1>Monitoring Services</h1>
<p class="subtitle"><i class="fas fa-server me-2"></i>WOOO TECH 監控服務中心</p>
<p class="last-update">最後更新: <span id="lastUpdate">-</span></p>
</div>
<!-- Status Overview -->
<div class="status-overview">
<div class="status-grid" id="statusGrid">
<div class="status-item">
<div class="status-icon loading" id="status-registry"><i class="fas fa-spinner fa-spin"></i></div>
<div class="status-label">Registry</div>
<div class="status-value" id="status-registry-val">檢測中...</div>
</div>
<div class="status-item">
<div class="status-icon loading" id="status-grafana"><i class="fas fa-spinner fa-spin"></i></div>
<div class="status-label">Grafana</div>
<div class="status-value" id="status-grafana-val">檢測中...</div>
</div>
<div class="status-item">
<div class="status-icon loading" id="status-prometheus"><i class="fas fa-spinner fa-spin"></i></div>
<div class="status-label">Prometheus</div>
<div class="status-value" id="status-prometheus-val">檢測中...</div>
</div>
<div class="status-item">
<div class="status-icon loading" id="status-n8n"><i class="fas fa-spinner fa-spin"></i></div>
<div class="status-label">n8n</div>
<div class="status-value" id="status-n8n-val">檢測中...</div>
</div>
<div class="status-item">
<div class="status-icon loading" id="status-momo_app"><i class="fas fa-spinner fa-spin"></i></div>
<div class="status-label">MOMO App</div>
<div class="status-value" id="status-momo_app-val">檢測中...</div>
</div>
<div class="status-item">
<div class="status-icon loading" id="status-database"><i class="fas fa-spinner fa-spin"></i></div>
<div class="status-label">Database</div>
<div class="status-value" id="status-database-val">檢測中...</div>
</div>
<div class="status-item">
<div class="status-icon loading" id="status-superset"><i class="fas fa-spinner fa-spin"></i></div>
<div class="status-label">Superset</div>
<div class="status-value" id="status-superset-val">檢測中...</div>
</div>
</div>
</div>
<!-- Alerts Panel -->
<div class="alerts-panel">
<div class="alerts-header">
<span class="alerts-title"><i class="fas fa-bell me-2"></i>即時告警</span>
<span class="alerts-count zero" id="alertsCount">0</span>
</div>
<div id="alertsList">
<div class="no-alerts">
<i class="fas fa-check-circle"></i>
<p>載入中...</p>
</div>
</div>
</div>
<!-- n8n Workflows Panel -->
<div class="workflows-panel">
<div class="alerts-header">
<span class="alerts-title"><i class="fas fa-project-diagram me-2"></i>n8n 監控工作流程</span>
</div>
<div id="workflowsList">
<p class="text-muted text-center py-3">載入中...</p>
</div>
</div>
<!-- CI/CD -->
<div class="section">
<h2 class="section-title"><i class="fas fa-rocket"></i>CI/CD</h2>
<div class="cards-grid">
<a href="http://192.168.0.110:8929/" target="_blank" class="service-card">
<div class="card-icon gitlab"><i class="fab fa-gitlab"></i></div>
<div class="card-content">
<h3>GitLab</h3>
<p>自建 Git 伺服器CI/CD Pipeline</p>
</div>
<span class="card-badge">Self-hosted</span>
</a>
<a href="https://registry.wooo.work/" target="_blank" class="service-card">
<div class="card-icon registry"><i class="fas fa-box"></i></div>
<div class="card-content">
<h3>Registry</h3>
<p>Docker Container Registry</p>
</div>
<span class="card-badge">Self-hosted</span>
<div class="card-status" id="card-status-registry"></div>
</a>
<a href="http://192.168.0.110:5678/" target="_blank" class="service-card">
<div class="card-icon n8n"><i class="fas fa-project-diagram"></i></div>
<div class="card-content">
<h3>n8n</h3>
<p>工作流自動化平台</p>
</div>
<span class="card-badge">UAT</span>
<div class="card-status" id="card-status-n8n"></div>
</a>
</div>
</div>
<!-- 視覺化與分析 -->
<div class="section">
<h2 class="section-title"><i class="fas fa-chart-line"></i>視覺化與分析</h2>
<div class="cards-grid">
<a href="/grafana/" class="service-card">
<div class="card-icon grafana"><i class="fas fa-chart-area"></i></div>
<div class="card-content">
<h3>Grafana (Docker)</h3>
<p>儀表板、Loki 日誌、Prometheus 指標</p>
</div>
<div class="card-status" id="card-status-grafana"></div>
</a>
<a href="/k8s-grafana/" class="service-card">
<div class="card-icon k8s"><i class="fas fa-chart-area"></i></div>
<div class="card-content">
<h3>Grafana (K8s)</h3>
<p>K8s 叢集監控儀表板</p>
</div>
<div class="card-status" id="card-status-k8s-grafana"></div>
</a>
<a href="/prometheus/" class="service-card">
<div class="card-icon prometheus"><i class="fas fa-fire"></i></div>
<div class="card-content">
<h3>Prometheus</h3>
<p>時序資料庫,系統指標收集</p>
</div>
<div class="card-status" id="card-status-prometheus"></div>
</a>
<a href="/alertmanager/" class="service-card">
<div class="card-icon alertmanager"><i class="fas fa-bell"></i></div>
<div class="card-content">
<h3>Alertmanager</h3>
<p>告警路由與通知管理</p>
</div>
<div class="card-status" id="card-status-alertmanager"></div>
</a>
<a href="/loki/" class="service-card">
<div class="card-icon loki"><i class="fas fa-scroll"></i></div>
<div class="card-content">
<h3>Loki</h3>
<p>日誌聚合系統</p>
</div>
</a>
</div>
</div>
<!-- BI 分析平台 -->
<div class="section">
<h2 class="section-title"><i class="fas fa-chart-pie"></i>BI 分析平台</h2>
<div class="cards-grid">
<a href="/superset/" class="service-card">
<div class="card-icon superset"><i class="fas fa-chart-bar"></i></div>
<div class="card-content">
<h3>Apache Superset</h3>
<p>商業智慧儀表板,資料視覺化</p>
</div>
<span class="card-badge">BI</span>
<div class="card-status" id="card-status-superset"></div>
</a>
<a href="/metabase/" class="service-card">
<div class="card-icon metabase"><i class="fas fa-analytics"></i></div>
<div class="card-content">
<h3>Metabase</h3>
<p>資料分析與報表工具</p>
</div>
<span class="card-badge">BI</span>
<div class="card-status" id="card-status-metabase"></div>
</a>
</div>
</div>
<!-- 雲端服務 -->
<div class="section">
<h2 class="section-title"><i class="fas fa-cloud"></i>雲端服務</h2>
<div class="cards-grid">
<a href="http://cloud.wooo.work/" target="_blank" class="service-card">
<div class="card-icon nextcloud"><i class="fas fa-cloud-upload-alt"></i></div>
<div class="card-content">
<h3>Nextcloud</h3>
<p>私有雲端檔案儲存</p>
</div>
<span class="card-badge">內網</span>
</a>
<a href="http://grist.wooo.work/" target="_blank" class="service-card">
<div class="card-icon grist"><i class="fas fa-table"></i></div>
<div class="card-content">
<h3>Grist</h3>
<p>線上試算表與資料庫</p>
</div>
<span class="card-badge">內網</span>
</a>
</div>
</div>
<!-- 系統管理 -->
<div class="section">
<h2 class="section-title"><i class="fas fa-cogs"></i>系統管理</h2>
<div class="cards-grid">
<a href="/portainer/" class="service-card">
<div class="card-icon portainer"><i class="fab fa-docker"></i></div>
<div class="card-content">
<h3>Portainer</h3>
<p>Docker 容器管理介面</p>
</div>
</a>
<a href="/pgadmin/" class="service-card">
<div class="card-icon pgadmin"><i class="fas fa-database"></i></div>
<div class="card-content">
<h3>pgAdmin</h3>
<p>PostgreSQL 管理介面</p>
</div>
</a>
<div class="service-card" style="cursor: default;">
<div class="card-icon watchtower"><i class="fas fa-sync-alt"></i></div>
<div class="card-content">
<h3>Watchtower</h3>
<p>自動偵測映像更新並重啟容器</p>
</div>
<span class="card-badge">Auto</span>
</div>
</div>
</div>
<!-- Exporters -->
<div class="section">
<h2 class="section-title"><i class="fas fa-download"></i>Exporters</h2>
<div class="cards-grid">
<a href="/cadvisor/" class="service-card">
<div class="card-icon cadvisor"><i class="fas fa-cube"></i></div>
<div class="card-content">
<h3>cAdvisor</h3>
<p>容器資源監控</p>
</div>
</a>
<a href="/node-exporter/metrics" class="service-card">
<div class="card-icon node"><i class="fas fa-microchip"></i></div>
<div class="card-content">
<h3>Node Exporter</h3>
<p>主機系統指標</p>
</div>
</a>
<a href="/blackbox/" class="service-card">
<div class="card-icon blackbox"><i class="fas fa-satellite-dish"></i></div>
<div class="card-content">
<h3>Blackbox Exporter</h3>
<p>端點探測監控</p>
</div>
</a>
<a href="/postgres-exporter/metrics" class="service-card">
<div class="card-icon postgres"><i class="fas fa-elephant"></i></div>
<div class="card-content">
<h3>PostgreSQL Exporter</h3>
<p>資料庫效能指標</p>
</div>
</a>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p>&copy; 2026 WOOO TECH. All rights reserved.</p>
<p><a href="https://mo.wooo.work/">返回 Momo Pro System</a></p>
</div>
</div>
</div>
<script>
// 監控資料 API (使用相對路徑,由 Nginx 代理到 mo.wooo.work)
const MONITOR_API = '/api/system/monitor/overview';
// 更新狀態圖示
function updateStatusIcon(service, healthy) {
const iconEl = document.getElementById(`status-${service}`);
const valEl = document.getElementById(`status-${service}-val`);
const cardStatus = document.getElementById(`card-status-${service}`);
if (iconEl) {
iconEl.className = `status-icon ${healthy ? 'healthy' : 'unhealthy'}`;
iconEl.innerHTML = healthy
? '<i class="fas fa-check-circle"></i>'
: '<i class="fas fa-times-circle"></i>';
}
if (valEl) {
valEl.textContent = healthy ? '運行中' : '異常';
}
if (cardStatus) {
cardStatus.className = `card-status ${healthy ? 'healthy' : 'unhealthy'}`;
}
}
// 更新告警列表
function updateAlerts(alerts) {
const listEl = document.getElementById('alertsList');
const countEl = document.getElementById('alertsCount');
countEl.textContent = alerts.length;
countEl.className = alerts.length === 0 ? 'alerts-count zero' : 'alerts-count';
if (alerts.length === 0) {
listEl.innerHTML = `
<div class="no-alerts">
<i class="fas fa-check-circle"></i>
<p>所有服務正常運行</p>
</div>
`;
return;
}
listEl.innerHTML = alerts.map(alert => `
<div class="alert-item ${alert.severity}">
<i class="alert-icon fas fa-exclamation-triangle" style="color: ${alert.severity === 'critical' ? '#e03131' : '#ffd43b'}"></i>
<div class="alert-content">
<div class="alert-name">${escapeHtml(alert.name)}</div>
<div class="alert-message">${escapeHtml(alert.message || '無詳細資訊')}</div>
${alert.instance ? `<div class="alert-instance">${escapeHtml(alert.instance)}</div>` : ''}
</div>
</div>
`).join('');
}
// 更新 n8n 工作流程列表
function updateWorkflows(workflows) {
const listEl = document.getElementById('workflowsList');
if (!workflows || workflows.length === 0) {
listEl.innerHTML = '<p class="text-muted text-center py-3">無監控工作流程</p>';
return;
}
// 只顯示監控相關的工作流程
const monitorWorkflows = workflows.filter(wf =>
wf.name && (
wf.name.includes('監控') ||
wf.name.includes('Monitor') ||
wf.name.includes('告警') ||
wf.name.includes('Alert') ||
wf.name.includes('Health') ||
wf.name.includes('健康')
)
);
if (monitorWorkflows.length === 0) {
listEl.innerHTML = '<p class="text-muted text-center py-3">無監控工作流程</p>';
return;
}
listEl.innerHTML = monitorWorkflows.map(wf => `
<div class="workflow-item">
<span class="workflow-name">${escapeHtml(wf.name)}</span>
<span class="workflow-status ${wf.active ? 'active' : 'inactive'}">
${wf.active ? '運行中' : '已停用'}
</span>
</div>
`).join('');
}
// HTML 轉義
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 載入監控資料
async function loadMonitorData() {
try {
const response = await fetch(MONITOR_API, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
// 更新服務狀態
if (data.services) {
Object.entries(data.services).forEach(([service, info]) => {
updateStatusIcon(service, info.healthy);
});
}
// 更新告警
if (data.alerts) {
updateAlerts(data.alerts);
}
// 更新工作流程
if (data.n8n_workflows) {
updateWorkflows(data.n8n_workflows);
}
// 更新最後更新時間
document.getElementById('lastUpdate').textContent =
new Date().toLocaleString('zh-TW', { timeZone: 'Asia/Taipei' });
} catch (error) {
console.error('載入監控資料失敗:', error);
// 顯示錯誤狀態
['registry', 'grafana', 'prometheus', 'n8n', 'momo_app', 'database', 'superset'].forEach(service => {
const valEl = document.getElementById(`status-${service}-val`);
if (valEl) valEl.textContent = '連線失敗';
});
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadMonitorData();
// 每 30 秒更新一次
setInterval(loadMonitorData, 30000);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,382 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MOMO Pro - 監控中心 (即時狀態)</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #17a2b8;
}
body {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header {
background: var(--primary-gradient);
padding: 1.5rem 0;
margin-bottom: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.header h1 { font-weight: 700; margin-bottom: 0.3rem; }
.header .subtitle { opacity: 0.9; font-size: 1rem; }
.refresh-bar {
background: rgba(0,0,0,0.3);
padding: 0.5rem 1rem;
border-radius: 8px;
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.85rem;
}
.section-title {
color: #fff;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
gap: 0.75rem;
}
.service-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.service-card {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.25rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
position: relative;
}
.service-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}
.service-card.status-online { border-left: 4px solid var(--success-color); }
.service-card.status-offline { border-left: 4px solid var(--danger-color); }
.service-card.status-checking { border-left: 4px solid var(--warning-color); }
.service-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.service-name {
font-weight: 600;
font-size: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 600;
}
.status-online-badge {
background: rgba(40, 167, 69, 0.2);
color: #28a745;
border: 1px solid rgba(40, 167, 69, 0.4);
}
.status-offline-badge {
background: rgba(220, 53, 69, 0.2);
color: #dc3545;
border: 1px solid rgba(220, 53, 69, 0.4);
}
.status-checking-badge {
background: rgba(255, 193, 7, 0.2);
color: #ffc107;
border: 1px solid rgba(255, 193, 7, 0.4);
}
.service-info {
font-size: 0.8rem;
color: rgba(255,255,255,0.6);
margin-bottom: 0.5rem;
}
.service-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
color: rgba(255,255,255,0.5);
}
.response-time { font-family: monospace; }
.response-time.fast { color: #28a745; }
.response-time.medium { color: #ffc107; }
.response-time.slow { color: #dc3545; }
.env-badge {
font-size: 0.65rem;
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-weight: 600;
}
.env-uat { background: #007bff; color: #fff; }
.env-gcp { background: #dc3545; color: #fff; }
.env-local { background: #6c757d; color: #fff; }
.btn-open {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 0.75rem;
padding: 0.3rem 0.6rem;
border-radius: 6px;
text-decoration: none;
}
.btn-open:hover { background: rgba(255, 255, 255, 0.25); color: #fff; }
.summary-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.summary-card {
background: rgba(255,255,255,0.1);
border-radius: 12px;
padding: 1rem;
text-align: center;
}
.summary-card .number { font-size: 2rem; font-weight: 700; }
.summary-card .label { font-size: 0.85rem; color: rgba(255,255,255,0.7); }
.summary-card.online .number { color: #28a745; }
.summary-card.offline .number { color: #dc3545; }
.summary-card.warning .number { color: #ffc107; }
.summary-card.total .number { color: #17a2b8; }
.footer {
text-align: center;
padding: 1.5rem 0;
color: rgba(255, 255, 255, 0.5);
font-size: 0.85rem;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.checking-animation { animation: pulse 1s infinite; }
@media (max-width: 768px) {
.summary-cards { grid-template-columns: repeat(2, 1fr); }
.service-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="header">
<div class="container">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h1><i class="fas fa-heartbeat me-2"></i>MOMO Pro 監控中心</h1>
<p class="subtitle mb-0">即時服務狀態監控 - UAT + GCP 雙環境</p>
</div>
<div class="refresh-bar">
<span id="lastUpdate">載入中...</span>
<button class="btn btn-sm btn-light" onclick="checkAllServices()">
<i class="fas fa-sync-alt"></i> 立即刷新
</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="summary-cards">
<div class="summary-card online">
<div class="number" id="onlineCount">-</div>
<div class="label"><i class="fas fa-check-circle me-1"></i>運行中</div>
</div>
<div class="summary-card offline">
<div class="number" id="offlineCount">-</div>
<div class="label"><i class="fas fa-times-circle me-1"></i>離線</div>
</div>
<div class="summary-card warning">
<div class="number" id="warningCount">-</div>
<div class="label"><i class="fas fa-exclamation-triangle me-1"></i>檢查中</div>
</div>
<div class="summary-card total">
<div class="number" id="totalCount">-</div>
<div class="label"><i class="fas fa-server me-1"></i>總服務數</div>
</div>
</div>
<h3 class="section-title"><i class="fas fa-rocket text-primary"></i>核心應用服務</h3>
<div class="service-grid" id="coreServices"></div>
<h3 class="section-title mt-4"><i class="fas fa-tools text-warning"></i>開發工具</h3>
<div class="service-grid" id="devTools"></div>
<h3 class="section-title mt-4"><i class="fas fa-chart-line text-success"></i>監控服務</h3>
<div class="service-grid" id="monitoringServices"></div>
<h3 class="section-title mt-4"><i class="fas fa-chart-pie text-info"></i>BI 分析平台</h3>
<div class="service-grid" id="biServices"></div>
</div>
<div class="footer">
<div class="container">
<p class="mb-1">MOMO Pro System &copy; 2026 WOOO TECH</p>
<p class="mb-0">自動刷新間隔: 30 秒 | UAT: mo.wooo.work | GCP: momo.wooo.work</p>
</div>
</div>
<script>
const services = {
core: [
{ id: 'momo-uat', name: 'MOMO App', env: 'uat', link: 'https://mo.wooo.work', icon: 'fas fa-store', description: 'UAT 測試環境主應用' },
{ id: 'momo-gcp', name: 'MOMO App', env: 'gcp', link: 'https://momo.wooo.work', icon: 'fas fa-store', description: 'GCP 正式環境主應用' }
],
devTools: [
{ id: 'gitlab', name: 'GitLab', env: 'local', link: 'http://192.168.0.110:8929', icon: 'fab fa-gitlab', description: 'Git 版本控制與 CI/CD' },
{ id: 'registry', name: 'Docker Registry', env: 'local', link: 'https://registry.wooo.work', icon: 'fab fa-docker', description: '容器映像倉庫' },
{ id: 'n8n', name: 'n8n', env: 'local', link: 'http://192.168.0.110:5678', icon: 'fas fa-project-diagram', description: '自動化工作流程引擎' }
],
monitoring: [
{ id: 'grafana', name: 'Grafana (K8s)', env: 'local', link: 'http://192.168.0.110:30030', icon: 'fas fa-chart-area', description: 'K8s 監控儀表板' },
{ id: 'prometheus', name: 'Prometheus', env: 'local', link: 'https://monitor.wooo.work/prometheus/', icon: 'fas fa-fire', description: '指標收集與告警' },
{ id: 'alertmanager', name: 'Alertmanager', env: 'local', link: 'https://monitor.wooo.work/alertmanager/', icon: 'fas fa-bell', description: '告警路由管理' }
],
bi: [
{ id: 'superset', name: 'Apache Superset', env: 'local', link: 'https://monitor.wooo.work/superset/', icon: 'fas fa-tachometer-alt', description: 'BI 分析儀表板' },
{ id: 'metabase', name: 'Metabase', env: 'local', link: 'http://192.168.0.110:3030', icon: 'fas fa-table', description: '資料分析平台' }
]
};
let serviceStatus = {};
function renderServiceCard(service, status) {
const statusClass = status ? (status.online ? 'status-online' : 'status-offline') : 'status-checking';
const statusBadgeClass = status ? (status.online ? 'status-online-badge' : 'status-offline-badge') : 'status-checking-badge';
const statusText = status ? (status.online ? '運行中' : '離線') : '檢查中...';
const statusIcon = status ? (status.online ? 'fa-check-circle' : 'fa-times-circle') : 'fa-spinner fa-spin';
const envBadgeClass = service.env === 'uat' ? 'env-uat' : service.env === 'gcp' ? 'env-gcp' : 'env-local';
let responseTimeHtml = '';
if (status && status.responseTime) {
const rtClass = status.responseTime < 500 ? 'fast' : status.responseTime < 2000 ? 'medium' : 'slow';
responseTimeHtml = `<span class="response-time ${rtClass}">${status.responseTime}ms</span>`;
}
return `
<div class="service-card ${statusClass}" id="card-${service.id}">
<div class="service-header">
<div class="service-name">
<i class="${service.icon}"></i>
<span class="env-badge ${envBadgeClass}">${service.env.toUpperCase()}</span>
${service.name}
</div>
<span class="status-indicator ${statusBadgeClass} ${status ? '' : 'checking-animation'}">
<i class="fas ${statusIcon}"></i>
${statusText}
</span>
</div>
<div class="service-info">${service.description}</div>
<div class="service-meta">
${responseTimeHtml}
<a href="${service.link}" target="_blank" class="btn-open">
<i class="fas fa-external-link-alt me-1"></i>開啟
</a>
</div>
</div>
`;
}
function renderAllServices() {
document.getElementById('coreServices').innerHTML = services.core.map(s => renderServiceCard(s, serviceStatus[s.id])).join('');
document.getElementById('devTools').innerHTML = services.devTools.map(s => renderServiceCard(s, serviceStatus[s.id])).join('');
document.getElementById('monitoringServices').innerHTML = services.monitoring.map(s => renderServiceCard(s, serviceStatus[s.id])).join('');
document.getElementById('biServices').innerHTML = services.bi.map(s => renderServiceCard(s, serviceStatus[s.id])).join('');
updateSummary();
}
function updateSummary() {
const allServices = [...services.core, ...services.devTools, ...services.monitoring, ...services.bi];
let online = 0, offline = 0, checking = 0;
allServices.forEach(s => {
const status = serviceStatus[s.id];
if (status) { status.online ? online++ : offline++; } else { checking++; }
});
document.getElementById('onlineCount').textContent = online;
document.getElementById('offlineCount').textContent = offline;
document.getElementById('warningCount').textContent = checking;
document.getElementById('totalCount').textContent = allServices.length;
}
async function checkAllServices() {
document.getElementById('lastUpdate').innerHTML = '<i class="fas fa-sync-alt fa-spin me-1"></i>正在檢查...';
try {
const response = await fetch('/api/health/all');
if (response.ok) {
const data = await response.json();
if (data.services) {
Object.keys(data.services).forEach(svcId => {
const svcData = data.services[svcId];
serviceStatus[svcId] = {
online: svcData.status === 'online',
responseTime: svcData.responseTime,
statusCode: svcData.code,
lastCheck: new Date()
};
});
}
}
} catch (error) {
console.error('Health check failed:', error);
}
renderAllServices();
const now = new Date().toLocaleTimeString('zh-TW');
document.getElementById('lastUpdate').innerHTML = `<i class="fas fa-clock me-1"></i>最後更新: ${now}`;
}
document.addEventListener('DOMContentLoaded', () => {
renderAllServices();
checkAllServices();
setInterval(checkAllServices, 30000);
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -0,0 +1,430 @@
# =============================================================================
# WOOO TECH - Monitor Dashboard
# Nginx 配置 - UAT Server (192.168.0.110)
# 所有監控工具統一入口
# 2026-02-08 整理版本 - 移除 Harbor其他服務保留
# =============================================================================
# 上游服務定義
upstream grafana_backend {
server 127.0.0.1:3000;
}
upstream prometheus_backend {
# K8s Prometheus ClusterIP
server 10.43.25.78:9090;
}
upstream alertmanager_backend {
# K8s Alertmanager ClusterIP
server 10.43.79.187:9093;
}
upstream portainer_backend {
server 127.0.0.1:9000;
}
upstream n8n_backend {
server 127.0.0.1:5678;
}
upstream superset_backend {
server 127.0.0.1:8088;
}
upstream gitlab_backend {
server 127.0.0.1:8929;
}
upstream nextcloud_backend {
server 127.0.0.1:8081;
}
upstream loki_backend {
server 127.0.0.1:3100;
}
upstream metabase_backend {
server 127.0.0.1:3001;
}
upstream grist_backend {
server 127.0.0.1:8484;
}
upstream cadvisor_backend {
server 127.0.0.1:8080;
}
upstream blackbox_backend {
server 127.0.0.1:9115;
}
upstream node_exporter_backend {
server 127.0.0.1:9100;
}
upstream postgres_exporter_backend {
server 127.0.0.1:9187;
}
# K8s Grafana (NodePort)
upstream k8s_grafana_backend {
server 127.0.0.1:30030;
}
# Docker Registry (HTTPS 通過 Nginx 代理)
upstream registry_backend {
server 127.0.0.1:5002;
}
# =============================================================================
# monitor.wooo.work - 監控入口 (HTTP -> HTTPS 重定向)
# =============================================================================
server {
listen 80;
server_name monitor.wooo.work;
# HSTS - 強制 HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
return 301 https://$server_name$request_uri;
}
# =============================================================================
# monitor.wooo.work - 監控入口 (HTTPS)
# =============================================================================
server {
listen 443 ssl http2;
server_name monitor.wooo.work;
# HSTS - 強制 HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# SSL 證書
ssl_certificate /etc/letsencrypt/live/monitor.wooo.work/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/monitor.wooo.work/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# 監控首頁 (靜態頁面)
root /var/www/monitor;
index index.html;
# 首頁
location = / {
try_files /index.html =404;
}
# =========================================================================
# Docker Grafana (Port 3000)
# =========================================================================
location /grafana/ {
proxy_pass http://grafana_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支援
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# =========================================================================
# K8s Grafana (NodePort 30030)
# =========================================================================
location /k8s-grafana/ {
proxy_pass http://k8s_grafana_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /k8s-grafana/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
sub_filter_once off;
sub_filter_types text/html application/javascript;
sub_filter 'src="/' 'src="/k8s-grafana/';
sub_filter '"/api/' '"/k8s-grafana/api/';
}
# =========================================================================
# Prometheus (Port 9090)
# =========================================================================
location /prometheus/ {
proxy_pass http://prometheus_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /prometheus/;
}
# =========================================================================
# Alertmanager (Port 9093)
# =========================================================================
location /alertmanager/ {
proxy_pass http://alertmanager_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /alertmanager/;
}
# =========================================================================
# Portainer (Port 9000)
# =========================================================================
location /portainer/ {
proxy_pass http://portainer_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /portainer/api/ {
proxy_pass http://portainer_backend/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# =========================================================================
# n8n (Port 5678)
# =========================================================================
location /n8n/ {
proxy_pass http://n8n_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
# =========================================================================
# Apache Superset BI (Port 8088)
# =========================================================================
# 認證相關路徑重定向
location = /login/ {
return 302 /superset/login/;
}
location = /logout/ {
return 302 /superset/logout/;
}
location ^~ /lang/ {
return 302 /superset$request_uri;
}
location ^~ /users/ {
return 302 /superset$request_uri;
}
location ^~ /static/ {
return 302 /superset$request_uri;
}
location /superset/ {
proxy_pass http://superset_backend/;
proxy_redirect ~^(/superset/.*)$ $1;
proxy_redirect ~^/(?!superset)(.*)$ /superset/$1;
gzip off;
proxy_set_header Accept-Encoding "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
sub_filter '"/static/' '"/superset/static/';
sub_filter "'/static/" "'/superset/static/";
sub_filter_once off;
sub_filter_types text/html application/javascript text/css;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
}
# =========================================================================
# Loki (Port 3100)
# =========================================================================
location /loki/ {
proxy_pass http://loki_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# =========================================================================
# Metabase (Port 3001)
# =========================================================================
location /metabase/ {
proxy_pass http://metabase_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /metabase/;
}
# =========================================================================
# cAdvisor (Port 8080)
# =========================================================================
location /cadvisor/ {
proxy_pass http://cadvisor_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /cadvisor/;
}
# =========================================================================
# Blackbox Exporter (Port 9115)
# =========================================================================
location /blackbox/ {
proxy_pass http://blackbox_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# =========================================================================
# Node Exporter (Port 9100)
# =========================================================================
location /node-exporter/ {
proxy_pass http://node_exporter_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# =========================================================================
# PostgreSQL Exporter (Port 9187)
# =========================================================================
location /postgres-exporter/ {
proxy_pass http://postgres_exporter_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# =========================================================================
# Docker Registry (Port 5002)
# =========================================================================
location /registry/ {
proxy_pass http://registry_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Registry 需要大檔案上傳
client_max_body_size 0;
proxy_read_timeout 900;
proxy_send_timeout 900;
}
}
# =============================================================================
# gitlab.wooo.work - GitLab (僅內網)
# =============================================================================
server {
listen 80;
server_name gitlab.wooo.work;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://gitlab_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffers 8 32k;
proxy_buffer_size 64k;
client_max_body_size 0;
proxy_read_timeout 600s;
}
}
# =============================================================================
# cloud.wooo.work - Nextcloud (僅內網)
# =============================================================================
server {
listen 80;
server_name cloud.wooo.work;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://nextcloud_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 10G;
proxy_read_timeout 600s;
}
}
# =============================================================================
# grist.wooo.work - Grist (僅內網)
# =============================================================================
server {
listen 80;
server_name grist.wooo.work;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://grist_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

56
docker/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,56 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# Nginx Configuration
# =============================================================================
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日誌格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main;
# 效能優化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip 壓縮
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript
application/xml application/xml+rss text/javascript application/x-javascript;
# 安全標頭
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# 上傳限制
client_max_body_size 50M;
# 包含其他配置
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -0,0 +1,449 @@
# =============================================================================
# WOOO TECH - Monitor Dashboard
# Nginx 配置 - UAT Server (192.168.0.110)
# 所有監控工具統一入口
# 2026-02-08 整理版本 - 移除 Harbor其他服務保留
# =============================================================================
# 上游服務定義
upstream grafana_backend {
server 127.0.0.1:3000;
}
upstream prometheus_backend {
# K8s Prometheus ClusterIP
server 10.43.25.78:9090;
}
upstream alertmanager_backend {
# K8s Alertmanager ClusterIP
server 10.43.79.187:9093;
}
upstream portainer_backend {
server 127.0.0.1:9000;
}
upstream n8n_backend {
server 10.43.193.218:5678;
}
upstream superset_backend {
server 127.0.0.1:8088;
}
upstream gitlab_backend {
server 127.0.0.1:8929;
}
upstream nextcloud_backend {
server 127.0.0.1:8081;
}
upstream loki_backend {
server 127.0.0.1:3100;
}
upstream metabase_backend {
server 127.0.0.1:3001;
}
upstream grist_backend {
server 127.0.0.1:8484;
}
upstream cadvisor_backend {
server 127.0.0.1:8080;
}
upstream blackbox_backend {
server 127.0.0.1:9115;
}
upstream node_exporter_backend {
server 127.0.0.1:9100;
}
upstream postgres_exporter_backend {
server 127.0.0.1:9187;
}
# K8s Grafana (NodePort)
upstream k8s_grafana_backend {
server 127.0.0.1:30030;
}
# Docker Registry (HTTPS 通過 Nginx 代理)
upstream registry_backend {
server 127.0.0.1:5002;
}
# =============================================================================
# monitor.wooo.work - 監控入口 (HTTP -> HTTPS 重定向)
# =============================================================================
server {
listen 80;
server_name monitor.wooo.work;
# HSTS - 強制 HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
return 301 https://$server_name$request_uri;
}
# =============================================================================
# monitor.wooo.work - 監控入口 (HTTPS)
# =============================================================================
server {
listen 443 ssl http2;
server_name monitor.wooo.work;
# HSTS - 強制 HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# SSL 證書
ssl_certificate /etc/letsencrypt/live/monitor.wooo.work/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/monitor.wooo.work/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# 監控首頁 (靜態頁面)
root /var/www/monitor;
index index.html;
# 首頁
# API 代理 - 轉發到 MOMO App
# API 代理 - 轉發到 MOMO App
location /api/ {
proxy_pass https://mo.wooo.work/api/;
proxy_set_header Host mo.wooo.work;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_verify off;
}
location = / {
try_files /index.html =404;
}
# =========================================================================
# Docker Grafana (Port 3000)
# =========================================================================
location /grafana/ {
proxy_pass http://grafana_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支援
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# =========================================================================
# K8s Grafana (NodePort 30030)
# =========================================================================
location /k8s-grafana/ {
proxy_pass http://k8s_grafana_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /k8s-grafana/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
sub_filter_once off;
sub_filter_types text/html application/javascript;
sub_filter 'src="/' 'src="/k8s-grafana/';
sub_filter '"/api/' '"/k8s-grafana/api/';
}
# =========================================================================
# Prometheus (Port 9090)
# =========================================================================
location /prometheus/ {
proxy_pass http://prometheus_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /prometheus/;
}
# =========================================================================
# Alertmanager (Port 9093)
# =========================================================================
location /alertmanager/ {
proxy_pass http://alertmanager_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /alertmanager/;
}
# =========================================================================
# Portainer (Port 9000)
# =========================================================================
location /portainer/ {
proxy_pass http://portainer_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /portainer/api/ {
proxy_pass http://portainer_backend/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# =========================================================================
# n8n (Port 5678)
# =========================================================================
location /n8n/ {
proxy_pass http://n8n_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
# =========================================================================
# Apache Superset BI (Port 8088)
# =========================================================================
# 認證相關路徑重定向
location = /login/ {
return 302 /superset/login/;
}
location = /logout/ {
return 302 /superset/logout/;
}
location ^~ /lang/ {
return 302 /superset$request_uri;
}
location ^~ /users/ {
return 302 /superset$request_uri;
}
location ^~ /static/ {
return 302 /superset$request_uri;
}
# Superset 首頁特殊處理
# Superset 登入頁面特殊處理
location = /superset/login/ {
proxy_pass http://superset_backend/login/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /superset/ {
# 根路徑重定向到 welcome
if ($request_uri = /superset/) {
return 302 /superset/welcome/;
}
proxy_pass http://superset_backend;
proxy_redirect ~^(/superset/.*)$ $1;
proxy_redirect ~^/(?!superset)(.*)$ /superset/$1;
gzip off;
proxy_set_header Accept-Encoding "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
sub_filter '"/static/' '"/superset/static/';
sub_filter "'/static/" "'/superset/static/";
sub_filter_once off;
sub_filter_types text/html application/javascript text/css;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
}
# =========================================================================
# Loki (Port 3100)
# =========================================================================
location /loki/ {
proxy_pass http://loki_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# =========================================================================
# Metabase (Port 3001)
# =========================================================================
location /metabase/ {
proxy_pass http://metabase_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /metabase/;
}
# =========================================================================
# cAdvisor (Port 8080)
# =========================================================================
location /cadvisor/ {
proxy_pass http://cadvisor_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect / /cadvisor/;
}
# =========================================================================
# Blackbox Exporter (Port 9115)
# =========================================================================
location /blackbox/ {
proxy_pass http://blackbox_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# =========================================================================
# Node Exporter (Port 9100)
# =========================================================================
location /node-exporter/ {
proxy_pass http://node_exporter_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# =========================================================================
# PostgreSQL Exporter (Port 9187)
# =========================================================================
location /postgres-exporter/ {
proxy_pass http://postgres_exporter_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# =========================================================================
# Docker Registry (Port 5002)
# =========================================================================
location /registry/ {
proxy_pass http://registry_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Registry 需要大檔案上傳
client_max_body_size 0;
proxy_read_timeout 900;
proxy_send_timeout 900;
}
}
# =============================================================================
# gitlab.wooo.work - GitLab (僅內網)
# =============================================================================
server {
listen 80;
server_name gitlab.wooo.work;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://gitlab_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffers 8 32k;
proxy_buffer_size 64k;
client_max_body_size 0;
proxy_read_timeout 600s;
}
}
# =============================================================================
# cloud.wooo.work - Nextcloud (僅內網)
# =============================================================================
server {
listen 80;
server_name cloud.wooo.work;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://nextcloud_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 10G;
proxy_read_timeout 600s;
}
}
# =============================================================================
# grist.wooo.work - Grist (僅內網)
# =============================================================================
server {
listen 80;
server_name grist.wooo.work;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
}

View File

@@ -0,0 +1,67 @@
-- =============================================================================
-- PostgreSQL 初始化腳本
-- WOOO TECH - Momo Pro System
-- =============================================================================
-- 建立 Metabase 專用資料庫
CREATE DATABASE metabase;
-- 建立分析用資料表 (從 SQLite 同步)
-- 這些表結構對應 SQLite 的主要資料表
-- 即時銷售月報表
CREATE TABLE IF NOT EXISTS realtime_sales_monthly (
id SERIAL PRIMARY KEY,
DATE,
VARCHAR(50),
TEXT,
VARCHAR(50),
INTEGER,
DECIMAL(15, 2),
DECIMAL(15, 2),
VARCHAR(200),
VARCHAR(200),
VARCHAR(200),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 建立索引以加速查詢
CREATE INDEX idx_sales_date ON realtime_sales_monthly();
CREATE INDEX idx_sales_vendor ON realtime_sales_monthly();
CREATE INDEX idx_sales_category ON realtime_sales_monthly();
CREATE INDEX idx_sales_brand ON realtime_sales_monthly();
-- EDM 資料表
CREATE TABLE IF NOT EXISTS edm_data (
id SERIAL PRIMARY KEY,
VARCHAR(500),
DATE,
DATE,
VARCHAR(100),
VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 商品資料表
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
VARCHAR(50) UNIQUE,
TEXT,
VARCHAR(200),
VARCHAR(200),
VARCHAR(200),
DECIMAL(10, 2),
DECIMAL(10, 2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 授權給 momo 用戶
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO momo;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO momo;
-- 顯示初始化完成訊息
DO $$
BEGIN
RAISE NOTICE '✅ PostgreSQL 初始化完成 - WOOO Analytics';
END $$;

View File

@@ -0,0 +1,81 @@
# =============================================================================
# PostgreSQL 效能優化配置
# WOOO TECH - Momo Pro System
# 針對 8GB RAM 伺服器優化
# =============================================================================
# -----------------------------------------------------------------------------
# 連線設定
# -----------------------------------------------------------------------------
listen_addresses = '*'
max_connections = 100
# -----------------------------------------------------------------------------
# 記憶體配置 (針對 8GB RAM 優化)
# -----------------------------------------------------------------------------
# shared_buffers: 建議設為總 RAM 的 25% (8GB * 0.25 = 2GB)
shared_buffers = 2GB
# work_mem: 每個排序/Hash 操作的記憶體 (大型查詢需要更多)
# 計算: (RAM - shared_buffers) / (max_connections * 2)
work_mem = 64MB
# maintenance_work_mem: VACUUM, CREATE INDEX 等維護操作使用
maintenance_work_mem = 512MB
# effective_cache_size: 告訴 planner 系統總共有多少快取可用
# 建議設為總 RAM 的 75%
effective_cache_size = 6GB
# -----------------------------------------------------------------------------
# 磁碟 I/O 配置
# -----------------------------------------------------------------------------
# 使用 SSD 時建議調高
random_page_cost = 1.1
effective_io_concurrency = 200
# 預讀設定 (對於大表 Seq Scan 很重要)
seq_page_cost = 1.0
# -----------------------------------------------------------------------------
# WAL (Write-Ahead Log) 配置
# -----------------------------------------------------------------------------
wal_buffers = 64MB
checkpoint_completion_target = 0.9
max_wal_size = 2GB
min_wal_size = 1GB
# -----------------------------------------------------------------------------
# 查詢計劃器配置
# -----------------------------------------------------------------------------
# 鼓勵使用索引
enable_seqscan = on
enable_indexscan = on
enable_bitmapscan = on
# 並行查詢 (利用多核心)
max_parallel_workers_per_gather = 2
max_parallel_workers = 4
max_worker_processes = 8
parallel_tuple_cost = 0.01
parallel_setup_cost = 1000
# -----------------------------------------------------------------------------
# 自動 VACUUM 配置
# -----------------------------------------------------------------------------
autovacuum = on
autovacuum_vacuum_scale_factor = 0.1
autovacuum_analyze_scale_factor = 0.05
# -----------------------------------------------------------------------------
# 日誌配置
# -----------------------------------------------------------------------------
log_min_duration_statement = 1000
log_checkpoints = on
log_lock_waits = on
# -----------------------------------------------------------------------------
# 統計收集
# -----------------------------------------------------------------------------
track_activities = on
track_counts = on

View File

@@ -0,0 +1,223 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# Prometheus Alert Rules
# Version: 1.0
# =============================================================================
#
# 告警嚴重程度定義:
# - critical: 需要立即處理的嚴重問題
# - warning: 需要關注但不緊急的問題
# - info: 資訊性通知
#
# =============================================================================
groups:
# ===========================================================================
# 主機資源監控告警
# ===========================================================================
- name: host_alerts
rules:
# -----------------------------------------------------------------------
# CPU 使用率告警
# -----------------------------------------------------------------------
- alert: HostHighCpuUsage
expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 50
for: 5m
labels:
severity: warning
category: cpu
annotations:
summary: "主機 CPU 使用率過高"
description: "主機 {{ $labels.instance }} CPU 使用率超過 50% 持續 5 分鐘,當前值: {{ $value | printf \"%.1f\" }}%"
value: "{{ $value | printf \"%.1f\" }}%"
- alert: HostCriticalCpuUsage
expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: critical
category: cpu
annotations:
summary: "主機 CPU 使用率嚴重過高"
description: "主機 {{ $labels.instance }} CPU 使用率超過 80%,當前值: {{ $value | printf \"%.1f\" }}%"
value: "{{ $value | printf \"%.1f\" }}%"
# -----------------------------------------------------------------------
# 記憶體使用率告警
# -----------------------------------------------------------------------
- alert: HostHighMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 50
for: 5m
labels:
severity: warning
category: memory
annotations:
summary: "主機記憶體使用率過高"
description: "主機 {{ $labels.instance }} 記憶體使用率超過 50% 持續 5 分鐘,當前值: {{ $value | printf \"%.1f\" }}%"
value: "{{ $value | printf \"%.1f\" }}%"
- alert: HostCriticalMemoryUsage
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 5m
labels:
severity: critical
category: memory
annotations:
summary: "主機記憶體使用率嚴重過高"
description: "主機 {{ $labels.instance }} 記憶體使用率超過 85%,當前值: {{ $value | printf \"%.1f\" }}%"
value: "{{ $value | printf \"%.1f\" }}%"
# -----------------------------------------------------------------------
# 磁碟使用率告警
# -----------------------------------------------------------------------
- alert: HostHighDiskUsage
expr: (1 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"})) * 100 > 80
for: 5m
labels:
severity: warning
category: disk
annotations:
summary: "主機磁碟使用率過高"
description: "主機 {{ $labels.instance }} 磁碟 {{ $labels.mountpoint }} 使用率超過 80%,當前值: {{ $value | printf \"%.1f\" }}%"
value: "{{ $value | printf \"%.1f\" }}%"
- alert: HostCriticalDiskUsage
expr: (1 - (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"})) * 100 > 90
for: 5m
labels:
severity: critical
category: disk
annotations:
summary: "主機磁碟空間嚴重不足"
description: "主機 {{ $labels.instance }} 磁碟 {{ $labels.mountpoint }} 使用率超過 90%,當前值: {{ $value | printf \"%.1f\" }}%"
value: "{{ $value | printf \"%.1f\" }}%"
# -----------------------------------------------------------------------
# 系統負載告警
# -----------------------------------------------------------------------
- alert: HostHighLoadAverage
expr: node_load5 / count without(cpu, mode) (node_cpu_seconds_total{mode="idle"}) > 0.8
for: 5m
labels:
severity: warning
category: load
annotations:
summary: "主機系統負載過高"
description: "主機 {{ $labels.instance }} 5分鐘負載平均值過高當前值: {{ $value | printf \"%.2f\" }}"
value: "{{ $value | printf \"%.2f\" }}"
# ===========================================================================
# 容器監控告警
# ===========================================================================
- name: container_alerts
rules:
# -----------------------------------------------------------------------
# 容器 CPU 使用率
# -----------------------------------------------------------------------
- alert: ContainerHighCpuUsage
expr: (rate(container_cpu_usage_seconds_total{name!=""}[5m]) * 100) > 50
for: 5m
labels:
severity: warning
category: container_cpu
annotations:
summary: "容器 CPU 使用率過高"
description: "容器 {{ $labels.name }} CPU 使用率超過 50%,當前值: {{ $value | printf \"%.1f\" }}%"
container: "{{ $labels.name }}"
value: "{{ $value | printf \"%.1f\" }}%"
# -----------------------------------------------------------------------
# 容器記憶體使用率
# -----------------------------------------------------------------------
- alert: ContainerHighMemoryUsage
expr: (container_memory_usage_bytes{name!=""} / container_spec_memory_limit_bytes{name!=""}) * 100 > 50
for: 5m
labels:
severity: warning
category: container_memory
annotations:
summary: "容器記憶體使用率過高"
description: "容器 {{ $labels.name }} 記憶體使用率超過 50%,當前值: {{ $value | printf \"%.1f\" }}%"
container: "{{ $labels.name }}"
value: "{{ $value | printf \"%.1f\" }}%"
# ===========================================================================
# 網站健康監控告警
# ===========================================================================
- name: website_alerts
rules:
# -----------------------------------------------------------------------
# 網站無法訪問
# -----------------------------------------------------------------------
- alert: WebsiteDown
expr: probe_success{job=~"blackbox-http.*"} == 0
for: 1m
labels:
severity: critical
category: website
annotations:
summary: "網站無法訪問"
description: "網站 {{ $labels.instance }} 無法訪問,請立即檢查"
# -----------------------------------------------------------------------
# 網站響應時間過長
# -----------------------------------------------------------------------
- alert: WebsiteSlowResponse
expr: probe_http_duration_seconds{job=~"blackbox-http.*"} > 5
for: 2m
labels:
severity: warning
category: website
annotations:
summary: "網站響應緩慢"
description: "網站 {{ $labels.instance }} 響應時間超過 5 秒,當前值: {{ $value | printf \"%.2f\" }} 秒"
value: "{{ $value | printf \"%.2f\" }}s"
# ===========================================================================
# 網路連通性告警
# ===========================================================================
- name: network_alerts
rules:
# -----------------------------------------------------------------------
# 主機無法 Ping
# -----------------------------------------------------------------------
- alert: HostUnreachable
expr: probe_success{job="blackbox-icmp"} == 0
for: 1m
labels:
severity: critical
category: network
annotations:
summary: "主機無法連通"
description: "主機 {{ $labels.instance }} 無法 ping 通,可能已離線"
# -----------------------------------------------------------------------
# TCP 端口無法連接
# -----------------------------------------------------------------------
- alert: ServicePortDown
expr: probe_success{job=~"blackbox-tcp.*"} == 0
for: 1m
labels:
severity: critical
category: network
annotations:
summary: "服務端口無法連接"
description: "服務 {{ $labels.instance }} 無法連接,請檢查服務狀態"
# ===========================================================================
# PostgreSQL 資料庫監控告警
# ===========================================================================
- name: postgres_alerts
rules:
# -----------------------------------------------------------------------
# PostgreSQL 無法連接
# -----------------------------------------------------------------------
- alert: PostgresDown
expr: pg_up == 0
for: 1m
labels:
severity: critical
category: database
annotations:
summary: "PostgreSQL 無法連接"
description: "PostgreSQL 資料庫無法連接,請立即檢查"

View File

@@ -0,0 +1,326 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# Prometheus Configuration
# Version: 3.0 - With Alerting
# =============================================================================
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: 'momo-pro-system'
# =============================================================================
# 告警規則
# =============================================================================
rule_files:
- '/etc/prometheus/alert_rules.yml'
# =============================================================================
# Alertmanager 配置
# =============================================================================
alerting:
alertmanagers:
- static_configs:
- targets:
- momo-alertmanager:9093
# =============================================================================
# Scrape Configurations
# =============================================================================
scrape_configs:
# ===========================================================================
# 基礎設施監控
# ===========================================================================
# ---------------------------------------------------------------------------
# Prometheus 自身監控
# ---------------------------------------------------------------------------
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
labels:
instance: 'prometheus'
service: 'monitoring'
# ---------------------------------------------------------------------------
# Node Exporter - UAT 主機監控CPU, Memory, Disk, Network
# ---------------------------------------------------------------------------
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
labels:
instance: 'uat-server'
env: 'uat'
host: '192.168.0.110'
service: 'infrastructure'
# ---------------------------------------------------------------------------
# cAdvisor - Docker 容器指標監控
# ---------------------------------------------------------------------------
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
labels:
instance: 'docker-host'
env: 'uat'
service: 'container'
# ===========================================================================
# 應用服務監控
# ===========================================================================
# ---------------------------------------------------------------------------
# Momo Flask 應用 - 資料庫與應用指標
# ---------------------------------------------------------------------------
# Momo Flask 應用 - 健康檢查 (應用未提供 /metrics改用 /health)
# ---------------------------------------------------------------------------
- job_name: 'momo-app'
static_configs:
- targets: ['192.168.0.110:5001']
labels:
instance: 'momo-flask'
env: 'uat'
service: 'application'
metrics_path: /metrics
scrape_interval: 30s
scrape_timeout: 10s
# ===========================================================================
# 網站健康監控 (HTTP/HTTPS)
# ===========================================================================
# ---------------------------------------------------------------------------
# Blackbox HTTP - UAT 網站
# ---------------------------------------------------------------------------
- job_name: 'blackbox-http-uat'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://mo.wooo.work
- https://mo.wooo.work/health
- http://192.168.0.110:5001
- http://192.168.0.110:5001/health
labels:
env: 'uat'
probe_type: 'http'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# ---------------------------------------------------------------------------
# Blackbox HTTP - PROD 網站
# ---------------------------------------------------------------------------
- job_name: 'blackbox-http-prod'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://momo.wooo.work
- https://momo.wooo.work/health
labels:
env: 'prod'
probe_type: 'http'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# ---------------------------------------------------------------------------
# Blackbox HTTP - 公司官網
# ---------------------------------------------------------------------------
- job_name: 'blackbox-http-corporate'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://wooo.work
labels:
env: 'prod'
probe_type: 'http'
service: 'corporate'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# ===========================================================================
# 端口連通性監控 (TCP)
# ===========================================================================
# ---------------------------------------------------------------------------
# Blackbox TCP - UAT 服務端口
# ---------------------------------------------------------------------------
- job_name: 'blackbox-tcp-uat'
metrics_path: /probe
params:
module: [tcp_connect]
static_configs:
- targets:
- 192.168.0.110:5001 # Flask 應用 (正確 port)
- 192.168.0.110:22 # SSH
- 192.168.0.110:9090 # Prometheus
- 192.168.0.110:3000 # Grafana
- 192.168.0.110:3100 # Loki
- 192.168.0.110:9000 # Portainer HTTP
labels:
env: 'uat'
probe_type: 'tcp'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# ---------------------------------------------------------------------------
# Blackbox TCP - PROD 服務端口
# ---------------------------------------------------------------------------
- job_name: 'blackbox-tcp-prod'
metrics_path: /probe
params:
module: [tcp_connect]
static_configs:
- targets:
- 34.80.130.190:22 # GCP SSH
- 34.80.130.190:80 # GCP HTTP
- 34.80.130.190:443 # GCP HTTPS
labels:
env: 'prod'
probe_type: 'tcp'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# ===========================================================================
# 網路連通性監控 (ICMP Ping)
# ===========================================================================
# ---------------------------------------------------------------------------
# Blackbox ICMP - 所有主機
# ---------------------------------------------------------------------------
- job_name: 'blackbox-icmp'
metrics_path: /probe
params:
module: [icmp]
static_configs:
- targets:
- 192.168.0.110 # UAT Server
labels:
env: 'uat'
probe_type: 'icmp'
- targets:
- 34.80.130.190 # GCP PROD Server
labels:
env: 'prod'
probe_type: 'icmp'
- targets:
- 8.8.8.8 # Google DNS
- 1.1.1.1 # Cloudflare DNS
labels:
env: 'external'
probe_type: 'icmp'
service: 'network-check'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# ===========================================================================
# DNS 解析監控
# ===========================================================================
# ---------------------------------------------------------------------------
# Blackbox DNS - 域名解析檢查
# ---------------------------------------------------------------------------
- job_name: 'blackbox-dns'
metrics_path: /probe
params:
module: [dns_check]
static_configs:
- targets:
- 8.8.8.8 # Google DNS - mo.wooo.work
labels:
domain: 'mo.wooo.work'
probe_type: 'dns'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
- job_name: 'blackbox-dns-momo'
metrics_path: /probe
params:
module: [dns_check_momo]
static_configs:
- targets:
- 8.8.8.8 # Google DNS - momo.wooo.work
labels:
domain: 'momo.wooo.work'
probe_type: 'dns'
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# ===========================================================================
# 監控系統自身
# ===========================================================================
# ---------------------------------------------------------------------------
# Loki 日誌系統
# ---------------------------------------------------------------------------
- job_name: 'loki'
static_configs:
- targets: ['loki:3100']
labels:
instance: 'loki'
service: 'logging'
# ---------------------------------------------------------------------------
# Grafana 視覺化
# ---------------------------------------------------------------------------
- job_name: 'grafana'
static_configs:
- targets: ['grafana:3000']
labels:
instance: 'grafana'
service: 'visualization'
# ---------------------------------------------------------------------------
# Blackbox Exporter 自身
# ---------------------------------------------------------------------------
- job_name: 'blackbox-exporter'
static_configs:
- targets: ['blackbox-exporter:9115']
labels:
instance: 'blackbox'
service: 'monitoring'

View File

@@ -0,0 +1,119 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# Promtail Configuration
# =============================================================================
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# ==========================================================================
# Flask/Gunicorn Application Logs
# ==========================================================================
- job_name: momo-app
static_configs:
- targets:
- localhost
labels:
job: momo-app
env: production
__path__: /var/log/app/*.log
pipeline_stages:
- multiline:
firstline: '^\d{4}-\d{2}-\d{2}'
max_wait_time: 3s
- regex:
expression: '^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - (?P<level>\w+) - (?P<message>.*)$'
- labels:
level:
- timestamp:
source: timestamp
format: '2006-01-02 15:04:05,000'
# ==========================================================================
# Gunicorn Access Log
# ==========================================================================
- job_name: gunicorn-access
static_configs:
- targets:
- localhost
labels:
job: gunicorn-access
env: production
__path__: /var/log/app/gunicorn-access.log
pipeline_stages:
- regex:
expression: '^(?P<remote_addr>\S+) - - \[(?P<time_local>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<body_bytes>\d+) "(?P<referer>[^"]*)" "(?P<user_agent>[^"]*)"'
- labels:
method:
status:
path:
# ==========================================================================
# Gunicorn Error Log
# ==========================================================================
- job_name: gunicorn-error
static_configs:
- targets:
- localhost
labels:
job: gunicorn-error
env: production
__path__: /var/log/app/gunicorn-error.log
pipeline_stages:
- multiline:
firstline: '^\[\d{4}-\d{2}-\d{2}'
max_wait_time: 3s
# ==========================================================================
# Nginx Access Log
# ==========================================================================
- job_name: nginx-access
static_configs:
- targets:
- localhost
labels:
job: nginx-access
env: production
__path__: /var/log/nginx/*access*.log
pipeline_stages:
- regex:
expression: '^(?P<remote_addr>\S+) - (?P<remote_user>\S+) \[(?P<time_local>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<body_bytes>\d+) "(?P<referer>[^"]*)" "(?P<user_agent>[^"]*)"'
- labels:
method:
status:
- metrics:
http_request_total:
type: Counter
description: "Total HTTP requests"
source: status
config:
action: inc
# ==========================================================================
# Nginx Error Log
# ==========================================================================
- job_name: nginx-error
static_configs:
- targets:
- localhost
labels:
job: nginx-error
env: production
__path__: /var/log/nginx/*error*.log
pipeline_stages:
- multiline:
firstline: '^\d{4}/\d{2}/\d{2}'
max_wait_time: 3s

View File

@@ -0,0 +1,28 @@
# =============================================================================
# Docker Registry 配置
# =============================================================================
version: 0.1
log:
level: info
formatter: text
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
cache:
blobdescriptor: inmemory
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

View File

@@ -0,0 +1,74 @@
# =============================================================================
# WOOO TECH - Docker Registry
# 自建私有 Container Registry (取代 Harbor)
# =============================================================================
#
# 部署方式:
# cd /home/wooo/registry
# docker compose up -d
#
# 測試:
# curl -u admin:password https://registry.wooo.work/v2/_catalog
#
# =============================================================================
version: '3.8'
services:
registry:
image: registry:2
container_name: docker-registry
restart: unless-stopped
ports:
- "127.0.0.1:5002:5000" # 僅本地連線,透過 Nginx 反向代理 (避免與 Harbor 衝突)
volumes:
- registry-data:/var/lib/registry
- ./config.yml:/etc/docker/registry/config.yml:ro
environment:
- REGISTRY_STORAGE_DELETE_ENABLED=true
- TZ=Asia/Taipei
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:5000/v2/"]
interval: 30s
timeout: 10s
retries: 3
networks:
- registry-network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Registry UI (可選,提供 Web 介面)
registry-ui:
image: joxit/docker-registry-ui:latest
container_name: docker-registry-ui
restart: unless-stopped
profiles:
- ui # 使用 --profile ui 啟用
ports:
- "127.0.0.1:5001:80"
environment:
- REGISTRY_TITLE=WOOO Registry
- REGISTRY_URL=http://registry:5000
- SINGLE_REGISTRY=true
- DELETE_IMAGES=true
depends_on:
- registry
networks:
- registry-network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
registry-network:
driver: bridge
name: registry-network
volumes:
registry-data:
name: docker-registry-data

186
docker/registry/setup.sh Normal file
View File

@@ -0,0 +1,186 @@
#!/bin/bash
# =============================================================================
# Docker Registry 安裝腳本
# =============================================================================
set -e
# 顏色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
# 配置
REGISTRY_USER="${REGISTRY_USER:-admin}"
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-Wooo_Registry_2026}"
DOMAIN="registry.wooo.work"
# =============================================================================
# 1. 建立認證檔案 (htpasswd)
# =============================================================================
setup_auth() {
log "建立認證檔案..."
# 安裝 htpasswd 工具
if ! command -v htpasswd &> /dev/null; then
apt-get update && apt-get install -y apache2-utils
fi
# 建立 htpasswd 檔案
mkdir -p /etc/nginx/conf.d
htpasswd -Bbn "$REGISTRY_USER" "$REGISTRY_PASSWORD" > /etc/nginx/conf.d/.htpasswd
log "認證檔案已建立: /etc/nginx/conf.d/.htpasswd"
log "帳號: $REGISTRY_USER"
}
# =============================================================================
# 2. 設定 Nginx
# =============================================================================
setup_nginx() {
log "設定 Nginx..."
# 複製配置
cp /home/wooo/momo_pro_system/config/nginx/sites-available/registry /etc/nginx/sites-available/
# 啟用網站
ln -sf /etc/nginx/sites-available/registry /etc/nginx/sites-enabled/
# 測試並重載
nginx -t && systemctl reload nginx
log "Nginx 配置完成"
}
# =============================================================================
# 3. 申請 SSL 證書
# =============================================================================
setup_ssl() {
log "申請 SSL 證書..."
if [[ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]]; then
log "SSL 證書已存在"
return
fi
# 先用 HTTP 配置
cat > /tmp/registry-http.conf << 'EOF'
server {
listen 80;
server_name registry.wooo.work;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
EOF
cp /tmp/registry-http.conf /etc/nginx/sites-available/registry
ln -sf /etc/nginx/sites-available/registry /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
# 申請證書
certbot certonly --webroot -w /var/www/certbot -d "$DOMAIN" --non-interactive --agree-tos --email admin@wooo.work
# 恢復完整配置
cp /home/wooo/momo_pro_system/config/nginx/sites-available/registry /etc/nginx/sites-available/
nginx -t && systemctl reload nginx
log "SSL 證書申請完成"
}
# =============================================================================
# 4. 啟動 Registry
# =============================================================================
start_registry() {
log "啟動 Docker Registry..."
cd /home/wooo/registry
docker compose up -d
# 等待啟動
sleep 5
# 健康檢查
if curl -s http://127.0.0.1:5000/v2/ | grep -q "{}"; then
log "Registry 啟動成功"
else
error "Registry 啟動失敗"
fi
}
# =============================================================================
# 5. 測試
# =============================================================================
test_registry() {
log "測試 Registry..."
# 登入測試
echo "$REGISTRY_PASSWORD" | docker login "$DOMAIN" -u "$REGISTRY_USER" --password-stdin
# 推送測試映像
docker pull alpine:latest
docker tag alpine:latest "$DOMAIN/test/alpine:latest"
docker push "$DOMAIN/test/alpine:latest"
# 拉取測試
docker rmi "$DOMAIN/test/alpine:latest"
docker pull "$DOMAIN/test/alpine:latest"
# 清理
docker rmi "$DOMAIN/test/alpine:latest"
log "Registry 測試通過!"
}
# =============================================================================
# 主程式
# =============================================================================
main() {
echo ""
echo "=========================================="
echo " Docker Registry 安裝"
echo "=========================================="
echo ""
# 檢查 root
if [[ $EUID -ne 0 ]]; then
error "請使用 root 執行: sudo $0"
fi
# 建立目錄
mkdir -p /home/wooo/registry
cp -r /home/wooo/momo_pro_system/docker/registry/* /home/wooo/registry/
setup_auth
setup_ssl
setup_nginx
start_registry
test_registry
echo ""
echo "=========================================="
echo " 安裝完成!"
echo "=========================================="
echo ""
echo "Registry URL: https://$DOMAIN"
echo "帳號: $REGISTRY_USER"
echo "密碼: $REGISTRY_PASSWORD"
echo ""
echo "使用方式:"
echo " docker login $DOMAIN"
echo " docker push $DOMAIN/wooo/momo-pro-system:latest"
echo ""
}
# 執行
main "$@"

View File

@@ -0,0 +1,321 @@
# Superset 儀表板建置指南
> MOMO Pro System - BI 分析平台
> 建立日期: 2026-02-07
---
## 存取資訊
| 項目 | 值 |
|------|-----|
| URL | https://monitor.wooo.work/superset/ |
| 帳號 | admin |
| 密碼 | Wooo_Superset_2026 |
---
## 已建立的資料集
| 資料集 | 說明 | 主要時間欄位 |
|--------|------|--------------|
| `daily_sales_snapshot` | 每日銷售快照 | snapshot_date |
| `realtime_sales_monthly` | 即時業績月度資料 | - |
| `monthly_summary_analysis` | 月度總結分析 | report_month |
| `products` | 商品資料 | updated_at |
| `price_records` | 價格記錄 | timestamp |
---
## 需建立的儀表板
### 1. 銷售分析總覽 (Sales Analysis Dashboard)
**對應頁面**: `/sales_analysis`
**建議圖表**:
| 圖表名稱 | 圖表類型 | 資料集 | 說明 |
|----------|----------|--------|------|
| 每日銷售趨勢 | Line Chart | daily_sales_snapshot | X軸: snapshot_date, Y軸: SUM(金額) |
| 銷售額 TOP 10 商品 | Bar Chart | daily_sales_snapshot | 依商品名稱群組,取前 10 名 |
| 銷售通路分佈 | Pie Chart | daily_sales_snapshot | 依通路群組 |
| 星期銷售熱力圖 | Heatmap | daily_sales_snapshot | X軸: 星期, Y軸: 時段 |
| 銷售數據表格 | Table | daily_sales_snapshot | 詳細銷售記錄 |
**建立步驟**:
1. 前往 **Charts** > **+ Chart**
2. 選擇資料集 `daily_sales_snapshot`
3. 選擇圖表類型 (如 Line Chart)
4. 設定 X 軸、Y 軸、分組欄位
5. 點擊 **Save** 儲存圖表
6. 將圖表加入儀表板
---
### 2. 當日業績追蹤 (Daily Sales Dashboard)
**對應頁面**: `/daily_sales`
**建議圖表**:
| 圖表名稱 | 圖表類型 | 資料集 | 說明 |
|----------|----------|--------|------|
| 當日業績總覽 | Big Number | daily_sales_snapshot | 顯示今日總銷售額 |
| 業績達成率 | Gauge Chart | daily_sales_snapshot | 對比目標達成率 |
| 時段業績分佈 | Area Chart | daily_sales_snapshot | X軸: 時段, Y軸: 金額 |
| 商品銷售排行 | Bar Chart | daily_sales_snapshot | 今日銷售 TOP 20 |
| 業績明細表 | Table | daily_sales_snapshot | 可篩選日期的明細 |
**篩選器設定**:
- 新增 **Time Filter** 設定為 `snapshot_date`
- 預設顯示今天的資料
---
### 3. 成長分析 (Growth Analysis Dashboard)
**對應頁面**: `/growth_analysis`
**建議圖表**:
| 圖表名稱 | 圖表類型 | 資料集 | 說明 |
|----------|----------|--------|------|
| 月度成長趨勢 | Line Chart | realtime_sales_monthly | 顯示月度成長率 |
| 年增率比較 | Bar Chart | realtime_sales_monthly | YoY 比較 |
| 成長率 KPI | Big Number with Trendline | realtime_sales_monthly | 月成長率指標 |
| 品類成長分析 | Treemap | realtime_sales_monthly | 各品類成長貢獻 |
**計算欄位** (Metrics):
```sql
-- 月增長率
(SUM() - SUM()) / SUM() * 100
```
---
### 4. 月度總結 (Monthly Summary Dashboard)
**對應頁面**: `/monthly_summary_analysis`
**建議圖表**:
| 圖表名稱 | 圖表類型 | 資料集 | 說明 |
|----------|----------|--------|------|
| 月度業績總覽 | Big Number | monthly_summary_analysis | 當月總業績 |
| 月度趨勢比較 | Line Chart | monthly_summary_analysis | 12 個月趨勢 |
| 月度業績表格 | Pivot Table | monthly_summary_analysis | 月份 x 指標 |
| 月環比分析 | Bar Chart | monthly_summary_analysis | MoM 比較 |
---
### 5. ABC 分析 (ABC Analysis Dashboard)
**對應頁面**: `/abc_analysis/detail`
**建議圖表**:
| 圖表名稱 | 圖表類型 | 資料集 | 說明 |
|----------|----------|--------|------|
| ABC 分類圓餅圖 | Pie Chart | products | A/B/C 類商品佔比 |
| 帕累托曲線 | Dual Line Chart | products | 累計銷售貢獻 |
| ABC 商品列表 | Table | products | 可篩選分類的商品表 |
| 分類銷售佔比 | Sunburst Chart | products | 階層式銷售分佈 |
**計算欄位** (需在 SQL Lab 建立虛擬資料集):
```sql
SELECT
i_code,
product_name,
total_sales,
SUM(total_sales) OVER (ORDER BY total_sales DESC) as cumulative_sales,
SUM(total_sales) OVER () as grand_total,
CASE
WHEN SUM(total_sales) OVER (ORDER BY total_sales DESC) / SUM(total_sales) OVER () <= 0.7 THEN 'A'
WHEN SUM(total_sales) OVER (ORDER BY total_sales DESC) / SUM(total_sales) OVER () <= 0.9 THEN 'B'
ELSE 'C'
END as abc_class
FROM products
WHERE total_sales > 0
ORDER BY total_sales DESC
```
---
### 6. 商品價格趨勢 (Price Trends Dashboard)
**對應頁面**: 商品看板的價格趨勢
**建議圖表**:
| 圖表名稱 | 圖表類型 | 資料集 | 說明 |
|----------|----------|--------|------|
| 價格變動時間線 | Line Chart | price_records | 選定商品的價格歷史 |
| 今日價格變動 | Table | price_records | 今日有變動的商品 |
| 漲價/降價統計 | Bar Chart | price_records | 漲降價商品數量 |
| 價格變動熱力圖 | Heatmap | price_records | 時間 x 商品類別 |
**篩選器**:
- 商品篩選器 (product_id)
- 時間範圍篩選器 (timestamp)
---
## 建立儀表板步驟
### Step 1: 建立圖表
1. 登入 Superset
2. 點擊 **Charts** > **+ Chart**
3. 選擇資料集 (如 `daily_sales_snapshot`)
4. 選擇圖表類型
5. 設定維度 (Dimensions) 和指標 (Metrics)
6. 設定篩選條件
7. 點擊 **Run** 預覽
8. 點擊 **Save** 儲存
### Step 2: 建立儀表板
1. 點擊 **Dashboards** > **+ Dashboard**
2. 輸入儀表板名稱 (如「銷售分析總覽」)
3. 點擊 **Edit dashboard**
4. 從右側 Charts 清單拖曳圖表到畫布
5. 調整圖表大小和位置
6. 新增篩選器 (Filter box)
7. 點擊 **Save**
### Step 3: 設定篩選器
1. 在儀表板編輯模式
2. 點擊 **+ Add filter** (左上角)
3. 選擇篩選類型:
- **Time Filter**: 時間範圍
- **Select Filter**: 下拉選單
- **Range Filter**: 數值範圍
4. 選擇要影響的圖表
---
## SQL Lab 進階查詢
對於複雜的分析需求,可以使用 SQL Lab 建立虛擬資料集:
### 範例: 銷售成長分析虛擬表
```sql
-- 建立虛擬資料集: sales_growth_analysis
WITH monthly_sales AS (
SELECT
DATE_TRUNC('month', snapshot_date) as month,
SUM() as total_amount,
COUNT(DISTINCT ) as product_count,
COUNT(*) as order_count
FROM daily_sales_snapshot
GROUP BY DATE_TRUNC('month', snapshot_date)
)
SELECT
month,
total_amount,
product_count,
order_count,
LAG(total_amount) OVER (ORDER BY month) as prev_month_amount,
(total_amount - LAG(total_amount) OVER (ORDER BY month)) /
NULLIF(LAG(total_amount) OVER (ORDER BY month), 0) * 100 as growth_rate
FROM monthly_sales
ORDER BY month DESC
```
**使用步驟**:
1. 點擊 **SQL Lab** > **SQL Editor**
2. 貼上 SQL 查詢
3. 執行查詢確認結果
4. 點擊 **Save** > **Save Dataset**
5. 使用此虛擬資料集建立圖表
---
## 權限設定
### 建立唯讀角色
1. 前往 **Settings** > **List Roles**
2. 點擊 **+ Add**
3. 角色名稱: `MOMO_Viewer`
4. 權限設定:
- `can read on Chart`
- `can read on Dashboard`
- `datasource access on [MOMO_UAT].[daily_sales_snapshot]`
- (其他需要的資料集權限)
### 建立用戶
1. 前往 **Settings** > **List Users**
2. 點擊 **+ Add**
3. 設定帳號密碼
4. 指派角色 `MOMO_Viewer`
---
## 嵌入儀表板到現有系統
### iframe 嵌入
```html
<!-- 嵌入完整儀表板 -->
<iframe
src="https://monitor.wooo.work/superset/superset/dashboard/1/?standalone=true"
width="100%"
height="800px"
frameborder="0">
</iframe>
```
### Superset 嵌入設定
1. 前往 **Settings** > **Feature Flags**
2. 啟用 `ENABLE_DASHBOARD_EMBEDDING`
3. 在儀表板設定中允許嵌入
---
## 排程報告 (未來功能)
Superset 支援排程發送報告:
1. 前往儀表板
2. 點擊 **...** > **Schedule Report**
3. 設定:
- 收件人 (Email)
- 排程頻率 (Daily/Weekly)
- 報告格式 (PDF/Image)
> 注意: 需要額外設定 SMTP 和 Celery Beat
---
## 常見問題
### Q1: 圖表顯示「No data」
- 檢查時間篩選器範圍
- 確認資料集有資料
- 檢查 SQL 查詢條件
### Q2: 連線到 MOMO_UAT 失敗
- 確認 PostgreSQL Pod IP 正確
- 檢查 superset_readonly 用戶權限
- 驗證網路連通性
### Q3: 儀表板載入緩慢
- 減少單一儀表板的圖表數量
- 使用時間範圍限制資料量
- 考慮建立物化視圖
---
## 下一步
1. 依照本指南建立 6 個儀表板
2. 測試所有圖表功能
3. 設定用戶權限
4. 評估是否嵌入或取代現有頁面

View File

@@ -0,0 +1,466 @@
# Superset 功能實作指南
> 本指南詳細說明如何在 Superset 中複製現有頁面的分析功能
> 建立日期: 2026-02-08
---
## 存取資訊
| 項目 | 值 |
|------|-----|
| URL | https://monitor.wooo.work/superset/ |
| 帳號 | admin |
| 密碼 | Wooo_Superset_2026 |
| 資料庫 | MOMO_UAT |
---
## 已建立的資料集
| 資料集 | 資料表 | 用途 |
|--------|--------|------|
| daily_sales_snapshot | public.daily_sales_snapshot | 當日業績 |
| realtime_sales_monthly | public.realtime_sales_monthly | 銷售分析、成長分析 |
| monthly_summary_analysis | public.monthly_summary_analysis | 月度總結 |
| products | public.products | 商品資料、ABC 分析 |
| price_records | public.price_records | 價格趨勢 |
---
## 第一部分:當日業績 (Daily Sales)
**對應頁面**: `/daily_sales`
### 1.1 SQL Lab 建立虛擬資料集
在 SQL Lab 執行以下查詢,然後儲存為資料集:
```sql
-- 資料集名稱: daily_sales_kpi
-- 用途: 每日 KPI 彙總
SELECT
snapshot_date,
DATE_TRUNC('month', snapshot_date) as month,
EXTRACT(DOW FROM snapshot_date) as day_of_week,
COUNT(DISTINCT "商品代碼") as sku_count,
SUM("銷售金額") as total_revenue,
SUM("總成本") as total_cost,
SUM("銷售金額") - SUM("總成本") as gross_margin,
SUM("銷售數量") as total_qty,
CASE
WHEN SUM("銷售金額") > 0
THEN (SUM("銷售金額") - SUM("總成本")) / SUM("銷售金額") * 100
ELSE 0
END as margin_rate,
CASE
WHEN SUM("銷售數量") > 0
THEN SUM("銷售金額") / SUM("銷售數量")
ELSE 0
END as avg_price
FROM daily_sales_snapshot
GROUP BY snapshot_date
ORDER BY snapshot_date DESC
```
### 1.2 建議圖表
| 圖表名稱 | 類型 | 說明 |
|----------|------|------|
| 當日業績 Big Number | Big Number with Trendline | 顯示最新日期的 total_revenue |
| 當日毛利 Big Number | Big Number with Trendline | 顯示最新日期的 gross_margin |
| 30 天業績趨勢 | Line Chart | X軸: snapshot_date, Y軸: total_revenue |
| DoD 比較 | Bar Chart | 比較今日與昨日 |
| WoW 比較 | Bar Chart | 比較今日與上週同日 |
| 分類業績圓餅圖 | Pie Chart | 依商品分類分組 |
| 日曆熱力圖 | Calendar Heatmap | 每日業績視覺化 |
### 1.3 建立步驟
1. **SQL Lab** → 執行上述 SQL
2. 點擊 **Save****Save Dataset**
3. 命名為 `daily_sales_kpi`
4. 前往 **Charts****+ Chart**
5. 選擇 `daily_sales_kpi` 資料集
6. 依序建立各圖表
---
## 第二部分:銷售分析 (Sales Analysis)
**對應頁面**: `/sales_analysis`
### 2.1 SQL Lab 建立虛擬資料集
```sql
-- 資料集名稱: sales_analysis_detail
-- 用途: 銷售明細分析
SELECT
"日期" as order_date,
"商品名稱" as product_name,
"商品代碼" as product_code,
"館別" as category,
"品牌" as brand,
"廠商名稱" as vendor,
"總業績" as amount,
"總成本" as cost,
"總業績" - "總成本" as profit,
"銷量" as qty,
CASE
WHEN "總業績" > 0
THEN ("總業績" - "總成本") / "總業績" * 100
ELSE 0
END as margin_rate,
EXTRACT(DOW FROM "日期"::date) as day_of_week,
EXTRACT(HOUR FROM "訂單時間"::time) as order_hour,
DATE_TRUNC('month', "日期"::date) as month,
DATE_TRUNC('week', "日期"::date) as week
FROM realtime_sales_monthly
WHERE "日期" IS NOT NULL
```
### 2.2 建議圖表
| 圖表名稱 | 類型 | 設定 |
|----------|------|------|
| 總業績 KPI | Big Number | SUM(amount) |
| 總毛利 KPI | Big Number | SUM(profit) |
| 毛利率 KPI | Big Number | AVG(margin_rate) |
| 業績 TOP 20 商品 | Bar Chart (Horizontal) | GROUP BY product_name, ORDER BY SUM(amount) DESC LIMIT 20 |
| 分類業績分佈 | Pie Chart | GROUP BY category |
| 品牌業績排行 | Bar Chart | GROUP BY brand |
| 廠商業績排行 | Bar Chart | GROUP BY vendor |
| 星期銷售熱力圖 | Heatmap | X: day_of_week, Y: order_hour, Value: SUM(amount) |
| 月度趨勢 | Line Chart | X: month, Y: SUM(amount) |
| 週趨勢 | Line Chart | X: week, Y: SUM(amount) |
| 價格區間分佈 | Histogram | amount 分佈 |
| BCG 矩陣 | Scatter Plot | X: SUM(qty), Y: margin_rate, Size: SUM(amount) |
| 樹狀圖 | Treemap | 分類 → 品牌 階層 |
### 2.3 篩選器設定
建立以下 Filter Box:
- 日期範圍 (order_date)
- 分類 (category)
- 品牌 (brand)
- 廠商 (vendor)
- 星期 (day_of_week)
- 時段 (order_hour)
---
## 第三部分:成長分析 (Growth Analysis)
**對應頁面**: `/growth_analysis`
### 3.1 SQL Lab 建立虛擬資料集
```sql
-- 資料集名稱: growth_analysis_monthly
-- 用途: 月度成長分析 (MoM, YoY, AOV)
WITH monthly_data AS (
SELECT
DATE_TRUNC('month', "日期"::date) as month,
SUM("總業績") as revenue,
SUM("總成本") as cost,
SUM("總業績") - SUM("總成本") as profit,
COUNT(DISTINCT "訂單編號") as orders
FROM realtime_sales_monthly
WHERE "日期" IS NOT NULL
GROUP BY DATE_TRUNC('month', "日期"::date)
),
with_growth AS (
SELECT
month,
revenue,
profit,
orders,
revenue / NULLIF(orders, 0) as aov,
profit / NULLIF(revenue, 0) * 100 as margin_rate,
-- MoM (月增率)
(revenue - LAG(revenue) OVER (ORDER BY month)) /
NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100 as mom,
-- YoY (年增率)
(revenue - LAG(revenue, 12) OVER (ORDER BY month)) /
NULLIF(LAG(revenue, 12) OVER (ORDER BY month), 0) * 100 as yoy
FROM monthly_data
)
SELECT
month,
revenue,
profit,
orders,
ROUND(aov::numeric, 0) as aov,
ROUND(margin_rate::numeric, 1) as margin_rate,
COALESCE(ROUND(mom::numeric, 2), 0) as mom,
COALESCE(ROUND(yoy::numeric, 2), 0) as yoy
FROM with_growth
ORDER BY month DESC
```
### 3.2 建議圖表
| 圖表名稱 | 類型 | 說明 |
|----------|------|------|
| YTD 業績 | Big Number | 今年累計業績 |
| YTD 成長率 | Big Number | 與去年同期比較 |
| 近 30 天客單價 | Big Number | 最近客單價 |
| 月度業績趨勢 | Line Chart | X: month, Y: revenue |
| 月度毛利趨勢 | Line Chart | X: month, Y: profit |
| MoM 月增率 | Bar Chart | X: month, Y: mom (紅/綠顏色區分正負) |
| YoY 年增率 | Bar Chart | X: month, Y: yoy |
| 客單價趨勢 | Line Chart | X: month, Y: aov |
| 毛利率趨勢 | Line Chart | X: month, Y: margin_rate |
| 綜合指標雙軸圖 | Mixed Chart | 左軸: revenue, 右軸: margin_rate |
---
## 第四部分:月度總結 (Monthly Summary)
**對應頁面**: `/monthly_summary_analysis`
### 4.1 使用現有資料集
直接使用 `monthly_summary_analysis` 資料集。
### 4.2 建議圖表
| 圖表名稱 | 類型 | 說明 |
|----------|------|------|
| 本月業績 | Big Number | 最新月份的業績 |
| 月度業績比較 | Bar Chart | 12 個月業績對比 |
| 月環比 | Line Chart | MoM 變化 |
| 月度彙總表 | Pivot Table | 月份 x 各項指標 |
| 季度彙總 | Bar Chart | 依季度分組 |
---
## 第五部分ABC 分析
**對應頁面**: `/abc_analysis/detail`
### 5.1 SQL Lab 建立虛擬資料集
```sql
-- 資料集名稱: abc_analysis
-- 用途: 商品 ABC 分類
WITH product_sales AS (
SELECT
p.i_code,
p.name as product_name,
p.category,
COALESCE(SUM(d."銷售金額"), 0) as total_sales,
COALESCE(SUM(d."銷售數量"), 0) as total_qty
FROM products p
LEFT JOIN daily_sales_snapshot d ON p.i_code = d."商品代碼"
GROUP BY p.i_code, p.name, p.category
HAVING COALESCE(SUM(d."銷售金額"), 0) > 0
),
ranked AS (
SELECT
*,
SUM(total_sales) OVER (ORDER BY total_sales DESC) as cumulative_sales,
SUM(total_sales) OVER () as grand_total,
ROW_NUMBER() OVER (ORDER BY total_sales DESC) as rank
FROM product_sales
)
SELECT
i_code,
product_name,
category,
total_sales,
total_qty,
cumulative_sales,
grand_total,
rank,
cumulative_sales / grand_total * 100 as cumulative_pct,
CASE
WHEN cumulative_sales / grand_total <= 0.7 THEN 'A'
WHEN cumulative_sales / grand_total <= 0.9 THEN 'B'
ELSE 'C'
END as abc_class
FROM ranked
ORDER BY rank
```
### 5.2 建議圖表
| 圖表名稱 | 類型 | 說明 |
|----------|------|------|
| ABC 分類圓餅圖 | Pie Chart | GROUP BY abc_class |
| ABC 分類商品數 | Bar Chart | COUNT BY abc_class |
| 帕累托曲線 | Dual Axis Line | 銷售額 + 累計百分比 |
| A 類商品列表 | Table | FILTER abc_class = 'A' |
| B 類商品列表 | Table | FILTER abc_class = 'B' |
| C 類商品列表 | Table | FILTER abc_class = 'C' |
| 分類 ABC 分佈 | Stacked Bar | X: category, Y: COUNT, Color: abc_class |
---
## 第六部分:價格趨勢
**對應頁面**: 商品看板價格歷史
### 6.1 使用現有資料集
直接使用 `price_records` 資料集。
### 6.2 建議圖表
| 圖表名稱 | 類型 | 說明 |
|----------|------|------|
| 價格歷史折線圖 | Line Chart | X: timestamp, Y: current_price, Filter: product_id |
| 今日價格變動 | Table | WHERE DATE(timestamp) = CURRENT_DATE |
| 漲價商品數 | Big Number | COUNT WHERE price_change > 0 |
| 降價商品數 | Big Number | COUNT WHERE price_change < 0 |
| 價格變動分佈 | Histogram | price_change_pct 分佈 |
---
## 儀表板建立順序
### 建議順序(由簡到繁)
1. **成長分析儀表板** - 圖表較少,資料結構簡單
2. **月度總結儀表板** - 使用現有資料集
3. **當日業績儀表板** - 需要 DoD/WoW 計算
4. **銷售分析儀表板** - 圖表最多,篩選器複雜
5. **ABC 分析儀表板** - 需要進階 SQL
6. **價格趨勢儀表板** - 需要時間序列處理
---
## 驗證對照表
每個儀表板建立完成後,請與現有頁面比對以下項目:
| 驗證項目 | 檢查點 |
|----------|--------|
| 數據一致性 | KPI 數值是否與現有頁面一致 |
| 圖表呈現 | 圖表類型是否適當呈現資料 |
| 篩選功能 | 篩選器是否正常運作 |
| 效能 | 載入時間是否可接受 |
| 互動性 | 點擊鑽取是否正常 |
---
## 常用 Superset 操作
### 建立圖表快速步驟
1. **Charts****+ Chart**
2. 選擇資料集
3. 選擇圖表類型
4. 設定 Metrics (指標) 和 Dimensions (維度)
5. 設定篩選條件
6. **Run Query** 預覽
7. **Save** 儲存
### 建立儀表板
1. **Dashboards****+ Dashboard**
2. 輸入名稱
3. **Edit Dashboard**
4. 從右側拖曳圖表
5. 調整佈局
6. 新增篩選器
7. **Save**
### 設定篩選器
1. 在儀表板編輯模式
2. 點擊 **+ Add filter**
3. 選擇欄位和類型
4. 設定影響的圖表
---
## 注意事項
1. **欄位名稱**: PostgreSQL 區分大小寫,中文欄位需用雙引號包起來
2. **日期格式**: 確保日期欄位正確轉換為 DATE 類型
3. **效能**: 大資料集建議加入時間篩選限制
4. **快取**: Superset 有快取機制,測試時可能需要清除快取
---
## 故障排除
### 頁面無限載入 (Infinite Loading)
**症狀**: 訪問 Superset 頁面時,畫面顯示無限載入中
**原因**: Superset Docker 映像 (3.1.0/3.1.1) 中的 `theme.5ab95322dc4a489d8e8f.entry.js` 檔案大小為 0 bytes (映像構建問題)
**解決方案**: 已在 docker-compose.yml 的啟動命令中自動修復:
```bash
echo '(function(){console.log("Theme loaded");})();' > /app/superset/static/assets/theme.5ab95322dc4a489d8e8f.entry.js
```
**手動修復** (如果需要):
```bash
docker exec momo-superset sh -c 'echo "(function(){console.log(\"Theme loaded\");})();" > /app/superset/static/assets/theme.5ab95322dc4a489d8e8f.entry.js'
```
### 子路徑 404 錯誤
**症狀**: 訪問 `/superset/` 返回 404
**原因**: Nginx 子路徑配置需要特別處理 URL 重寫
**解決方案**: 參考 `nginx-superset.conf` 配置,關鍵設定:
- `proxy_redirect / /superset/;` - 重寫重定向
- `sub_filter` - 重寫 HTML 中的靜態資源路徑
- `gzip off;` - 禁用 gzip 讓 sub_filter 生效
### 雙重前綴問題 (/superset/superset/) (2026-02-08 修復)
**症狀**:
- 訪問 `https://monitor.wooo.work/superset/` 被重定向到 `/superset/superset/welcome/`
- 頁面無限載入
**根本原因**:
Superset 內部 Flask blueprints 路由已經是 `/superset/...`(例如 `/superset/welcome/`)。
如果 Nginx 使用 `proxy_redirect / /superset/;`,會把 `/superset/welcome/` 再次加前綴變成 `/superset/superset/welcome/`
**解決方案**:
1. **superset_config.py** - 禁用 x_prefix:
```python
ENABLE_PROXY_FIX = True
PROXY_FIX_CONFIG = {
"x_for": 1,
"x_proto": 1,
"x_host": 1,
"x_prefix": 0, # 必須為 0
}
```
2. **Nginx 配置** - 智能 proxy_redirect:
```nginx
location /superset/ {
proxy_pass http://127.0.0.1:8088/;
# 關鍵:已是 /superset/ 開頭的路徑保持不變
proxy_redirect /superset/ /superset/;
# 其他路徑才添加 /superset/ 前綴
proxy_redirect ~^/(?!superset)(.*)$ /superset/$1;
# 只重寫 static 路徑
sub_filter '"/static/' '"/superset/static/';
sub_filter "'/static/" "'/superset/static/";
sub_filter_once off;
}
```
3. **驗證**:
```bash
# 應該返回 302 到 /superset/welcome/ (不是 /superset/superset/welcome/)
curl -sI https://monitor.wooo.work/superset/ | grep -i location
```

202
docker/superset/README.md Normal file
View File

@@ -0,0 +1,202 @@
# Apache Superset 部署指南
## 概述
Apache Superset 是 MOMO Pro System 的 BI 分析平台,用於建立進階分析儀表板。
## 架構
```
┌─────────────────────────────────────┐
│ Apache Superset (UAT) │
│ https://monitor.wooo.work/superset│
└──────────────┬──────────────────────┘
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ UAT 資料庫 │ │ GCP 資料庫 │ │ DEV 資料庫 │
│ 192.168.0.110 │ │35.194.233.141 │ │ 127.0.0.1 │
│ momo_analytics│ │ momo_analytics│ │ momo_database │
└───────────────┘ └───────────────┘ └───────────────┘
```
## 快速部署
```bash
# 1. SSH 到 UAT 主機
ssh wooo@192.168.0.110
# 2. 進入 Superset 目錄
cd /home/wooo/momo_pro_system/docker/superset
# 3. 執行部署腳本
chmod +x deploy.sh
./deploy.sh deploy
# 4. 設定 Nginx 反向代理 (見下方)
# 5. 設定資料庫唯讀用戶 (見下方)
```
## 服務管理
```bash
# 查看狀態
./deploy.sh status
# 查看日誌
./deploy.sh logs
# 重啟服務
./deploy.sh restart
# 停止服務
./deploy.sh stop
# 清除所有資料 (危險)
./deploy.sh clean
```
## 訪問資訊
| 項目 | 值 |
|------|-----|
| 內部 URL | `http://127.0.0.1:8088` |
| 外部 URL | `https://monitor.wooo.work/superset/` |
| 帳號 | `admin` |
| 密碼 | `Wooo_Superset_2026` |
## Nginx 配置
將以下內容加入 `/etc/nginx/sites-available/monitor`:
```nginx
# Superset BI 分析平台
location /superset/ {
proxy_pass http://127.0.0.1:8088/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Script-Name /superset;
# WebSocket 支援
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超時設定
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
}
```
然後重啟 Nginx:
```bash
sudo nginx -t && sudo systemctl reload nginx
```
## 資料庫連線設定
### 1. 建立唯讀用戶
在 UAT PostgreSQL 執行:
```bash
kubectl exec -it momo-postgres-0 -n momo -- psql -U momo -d momo_analytics
```
執行 SQL:
```sql
CREATE ROLE superset_readonly WITH LOGIN PASSWORD 'Wooo_Superset_RO_2026';
GRANT CONNECT ON DATABASE momo_analytics TO superset_readonly;
GRANT USAGE ON SCHEMA public TO superset_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO superset_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO superset_readonly;
```
### 2. 在 Superset 新增資料庫連線
登入 Superset 後:
1. 點擊右上角 `+``Database`
2. 選擇 `PostgreSQL`
3. 輸入連線資訊
**UAT 環境連線:**
```
postgresql+psycopg2://superset_readonly:Wooo_Superset_RO_2026@host.docker.internal:5432/momo_analytics
```
> 注意: 使用 `host.docker.internal` 連接主機上的 K8s PostgreSQL
**GCP 環境連線:**
```
postgresql+psycopg2://superset_readonly:Wooo_Superset_RO_2026@35.194.233.141:5432/momo_analytics
```
> 注意: GCP 需要先設定防火牆規則允許 UAT IP (114.32.151.246)
## 預計實作的儀表板
| 儀表板 | 對應功能 | 資料表 |
|--------|---------|--------|
| 銷售分析總覽 | `/sales_analysis` | daily_sales_snapshot, realtime_sales_monthly |
| 當日業績追蹤 | `/daily_sales` | daily_sales_snapshot |
| 成長分析 | `/growth_analysis` | realtime_sales_monthly |
| 月度總結 | `/monthly_summary_analysis` | monthly_summary_analysis |
| ABC 分析 | `/abc_analysis/detail` | products, price_records |
| 商品價格趨勢 | `/` (首頁看板) | products, price_records |
## 資源需求
| 項目 | 最低需求 | 建議配置 |
|------|----------|----------|
| CPU | 2 核心 | 4 核心 |
| RAM | 4 GB | 8 GB |
| 硬碟 | 10 GB | 20 GB |
## 故障排除
### 問題: 容器啟動失敗
```bash
# 查看日誌
docker compose logs superset
# 檢查資料庫連線
docker compose logs superset-db
```
### 問題: 無法連線到外部資料庫
1. 確認防火牆規則
2. 確認資料庫用戶權限
3. 測試連線:
```bash
docker exec -it momo-superset bash
pip install psycopg2-binary
python -c "import psycopg2; conn = psycopg2.connect('postgresql://...')"
```
### 問題: SQL Lab 查詢超時
修改 `superset_config.py`:
```python
SQLLAB_TIMEOUT = 600 # 秒
```
## 備份
```bash
# 備份 Superset 資料
docker exec superset-postgres pg_dump -U superset superset > superset_backup.sql
# 還原
docker exec -i superset-postgres psql -U superset superset < superset_backup.sql
```
## 更新日誌
- **2026-02-08**: 初始部署

View File

@@ -0,0 +1,12 @@
/*
* Licensed to the Apache Software Foundation (ASF)
* Minimal theme entry point - fixes 0-byte bug in official images
* MOMO Pro System custom fix
*/
(function() {
'use strict';
console.log('[Superset] Theme module loaded');
if (typeof module !== 'undefined' && module.exports) {
module.exports = {};
}
})();

186
docker/superset/deploy.sh Executable file
View File

@@ -0,0 +1,186 @@
#!/bin/bash
# =============================================================================
# Apache Superset 部署腳本
# MOMO Pro System - UAT 環境
# =============================================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 顯示使用方式
usage() {
echo "使用方式: $0 [命令]"
echo ""
echo "命令:"
echo " deploy 部署 Superset (預設)"
echo " stop 停止 Superset"
echo " restart 重啟 Superset"
echo " logs 查看日誌"
echo " status 查看狀態"
echo " clean 清除所有資料 (危險)"
echo ""
}
# 部署 Superset
deploy() {
log_info "開始部署 Apache Superset..."
# 檢查 Docker
if ! command -v docker &> /dev/null; then
log_error "Docker 未安裝"
exit 1
fi
# 拉取映像
log_info "拉取 Docker 映像..."
docker compose pull
# 啟動服務
log_info "啟動 Superset 服務..."
docker compose up -d
# 等待健康檢查
log_info "等待服務啟動 (約 2 分鐘)..."
local max_wait=180
local waited=0
local interval=10
while [ $waited -lt $max_wait ]; do
if docker compose ps | grep -q "healthy"; then
log_success "Superset 啟動成功!"
break
fi
# 檢查是否有容器失敗
if docker compose ps | grep -q "Exit"; then
log_error "容器啟動失敗"
docker compose logs --tail=50
exit 1
fi
sleep $interval
waited=$((waited + interval))
log_info "等待中... ($waited/$max_wait 秒)"
done
if [ $waited -ge $max_wait ]; then
log_warn "等待超時,請手動檢查服務狀態"
docker compose ps
fi
# 顯示訪問資訊
echo ""
log_success "=========================================="
log_success "Apache Superset 部署完成!"
log_success "=========================================="
echo ""
echo "內部訪問: http://127.0.0.1:8088"
echo "外部訪問: https://monitor.wooo.work/superset/"
echo ""
echo "登入帳號: admin"
echo "登入密碼: Wooo_Superset_2026"
echo ""
echo "下一步:"
echo " 1. 設定 Nginx 反向代理"
echo " 2. 新增資料庫連線 (UAT/GCP)"
echo " 3. 建立資料集和儀表板"
echo ""
}
# 停止服務
stop() {
log_info "停止 Superset 服務..."
docker compose down
log_success "服務已停止"
}
# 重啟服務
restart() {
log_info "重啟 Superset 服務..."
docker compose restart
log_success "服務已重啟"
}
# 查看日誌
logs() {
docker compose logs -f --tail=100
}
# 查看狀態
status() {
echo ""
log_info "Superset 服務狀態:"
echo ""
docker compose ps
echo ""
# 檢查健康狀態
if docker compose ps | grep -q "healthy"; then
log_success "所有服務運行正常"
elif docker compose ps | grep -q "unhealthy"; then
log_warn "有服務不健康"
fi
}
# 清除所有資料
clean() {
log_warn "這將刪除所有 Superset 資料,包括:"
log_warn " - 儀表板"
log_warn " - 圖表"
log_warn " - 資料集"
log_warn " - 資料庫連線設定"
echo ""
read -p "確定要繼續嗎? (輸入 YES 確認): " confirm
if [ "$confirm" = "YES" ]; then
log_info "停止並清除服務..."
docker compose down -v
log_success "已清除所有資料"
else
log_info "已取消"
fi
}
# 主程式
case "${1:-deploy}" in
deploy)
deploy
;;
stop)
stop
;;
restart)
restart
;;
logs)
logs
;;
status)
status
;;
clean)
clean
;;
-h|--help)
usage
;;
*)
log_error "未知命令: $1"
usage
exit 1
;;
esac

View File

@@ -0,0 +1,113 @@
# =============================================================================
# Apache Superset - Docker Compose 配置
# MOMO Pro System - UAT 環境
# 用途BI 分析平台,連接 UAT、GCP、DEV 環境資料庫
# =============================================================================
#
# 重要Apache Superset 官方映像存在 theme.js 0 bytes bug
# 所有版本1.5.3, 2.0.x, 2.1.x, 3.x, 4.x都有這個問題
# 此配置包含啟動時自動修復腳本
# =============================================================================
services:
superset:
# 使用 2.1.3 版本 (3.x/4.x 的 theme.js 有 0 bytes bug)
image: apache/superset:2.1.3
container_name: momo-superset
restart: unless-stopped
ports:
- "127.0.0.1:8088:8088"
environment:
# 基本設定
- SUPERSET_SECRET_KEY=wooo_superset_secret_key_2026_momo_pro
- SUPERSET_LOAD_EXAMPLES=no
- TZ=Asia/Taipei
# 資料庫連線 (Superset 內部 metadata)
- DATABASE_HOST=superset-db
- DATABASE_PORT=5432
- DATABASE_USER=superset
- DATABASE_PASSWORD=Wooo_Superset_DB_2026
- DATABASE_DB=superset
# Redis 快取
- REDIS_HOST=superset-redis
- REDIS_PORT=6379
volumes:
- superset_home:/app/superset_home
- ./superset_config.py:/app/pythonpath/superset_config.py:ro
# theme.js 修復文件
- ./custom-assets/theme.entry.js:/tmp/theme-fix.js:ro
depends_on:
superset-db:
condition: service_healthy
superset-redis:
condition: service_healthy
networks:
- superset-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8088/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
command: >
bash -c "
echo '等待資料庫初始化...' &&
sleep 10 &&
echo '修復 theme.js 0-byte bug...' &&
for f in /app/superset/static/assets/theme.*.entry.js; do
if [ -f \"$$f\" ] && [ ! -s \"$$f\" ]; then
cp /tmp/theme-fix.js \"$$f\" &&
echo \"已修復: $$f\"
fi
done &&
superset db upgrade &&
superset fab create-admin --username admin --firstname Admin --lastname User --email admin@wooo.work --password Wooo_Superset_2026 || true &&
superset init &&
echo 'Superset 啟動中...' &&
gunicorn --bind 0.0.0.0:8088 --workers 4 --timeout 120 --access-logfile - 'superset.app:create_app()'
"
superset-db:
image: postgres:15-alpine
container_name: superset-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=superset
- POSTGRES_PASSWORD=Wooo_Superset_DB_2026
- POSTGRES_DB=superset
- TZ=Asia/Taipei
volumes:
- superset_db:/var/lib/postgresql/data
networks:
- superset-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U superset"]
interval: 10s
timeout: 5s
retries: 5
superset-redis:
image: redis:7-alpine
container_name: superset-redis
restart: unless-stopped
volumes:
- superset_redis:/data
networks:
- superset-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
superset_home:
superset_db:
superset_redis:
networks:
superset-network:
driver: bridge

View File

@@ -0,0 +1,118 @@
# =============================================================================
# Nginx 反向代理配置 - Apache Superset
# 複製此配置到 /etc/nginx/sites-available/superset
# =============================================================================
# 將此內容加入到 monitor.wooo.work 的 server 區塊中
# 2026-02-08 更新:修復登入重定向問題
# =============================================================================
# 關鍵設定 0重定向 Superset 相關路徑
# 解決 Superset 生成的 URL 沒有 /superset/ 前綴的問題
# =============================================================================
# 認證相關路徑重定向
location = /login/ {
return 302 /superset/login/;
}
location = /logout/ {
return 302 /superset/logout/;
}
# 語言切換路徑重定向
location ^~ /lang/ {
return 302 /superset$request_uri;
}
# 用戶資訊路徑重定向
location ^~ /users/ {
return 302 /superset$request_uri;
}
# 靜態資源路徑重定向 (SPA 動態載入)
location ^~ /static/ {
return 302 /superset$request_uri;
}
# Superset BI 分析平台
# 重要Superset 內部路由已是 /superset/...,不需要再添加前綴
# 2026-02-08 更新:修復雙重前綴問題 (/superset/superset/)
location /superset/ {
proxy_pass http://127.0.0.1:8088/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 注意:不設置 X-Forwarded-Prefix 和 X-Script-Name
# 因為 superset_config.py 中 x_prefix=0Superset 不讀取這些標頭
# 關鍵設定 1Superset 內部路由已是 /superset/,保持不變
# 只對非 /superset/ 開頭的路徑添加前綴
proxy_redirect /superset/ /superset/;
proxy_redirect ~^/(?!superset)(.*)$ /superset/$1;
# 關鍵設定 2禁用 gzip 壓縮,讓 sub_filter 可以正常運作
proxy_set_header Accept-Encoding "";
gzip off;
# 關鍵設定 3重寫 HTML 中的 URL
# 只處理靜態資源路徑(不處理 href/action避免重複添加
sub_filter '"/static/' '"/superset/static/';
sub_filter "'/static/" "'/superset/static/";
sub_filter_once off;
sub_filter_types text/html application/javascript text/javascript text/css;
# WebSocket 支援 (SQL Lab 需要)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 超時設定 (SQL Lab 查詢可能較長)
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
# 緩衝設定
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# Superset 靜態資源 (可選,提升效能)
location /superset/static/ {
proxy_pass http://127.0.0.1:8088/static/;
proxy_cache_valid 200 1d;
proxy_cache_valid any 1m;
expires 1d;
add_header Cache-Control "public, immutable";
}
# =============================================================================
# 或者使用獨立的 server 區塊 (superset.wooo.work)
# =============================================================================
# server {
# listen 443 ssl http2;
# server_name superset.wooo.work;
#
# ssl_certificate /etc/letsencrypt/live/superset.wooo.work/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/superset.wooo.work/privkey.pem;
#
# location / {
# proxy_pass http://127.0.0.1:8088;
# proxy_http_version 1.1;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
#
# # WebSocket 支援
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
#
# # 超時設定
# proxy_connect_timeout 300;
# proxy_send_timeout 300;
# proxy_read_timeout 300;
# }
# }

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
Superset 資料集與儀表板自動設定腳本
MOMO Pro System - UAT 環境
使用方式:
docker exec momo-superset python /app/setup_datasets.py
"""
import sys
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 要建立的資料集
DATASETS = [
{
"table_name": "daily_sales_snapshot",
"description": "每日銷售快照 - 用於當日業績分析",
"main_dttm_col": "snapshot_date",
},
{
"table_name": "realtime_sales_monthly",
"description": "即時業績月度資料 - 用於銷售分析和成長分析",
"main_dttm_col": None,
},
{
"table_name": "monthly_summary_analysis",
"description": "月度總結分析 - 用於月度報表",
"main_dttm_col": "report_month",
},
{
"table_name": "products",
"description": "商品資料 - 用於商品看板和 ABC 分析",
"main_dttm_col": "updated_at",
},
{
"table_name": "price_records",
"description": "價格記錄 - 用於價格趨勢分析",
"main_dttm_col": "timestamp",
},
]
def setup_datasets():
"""建立所有資料集"""
# 在函數內部導入,確保 app context 正確
from superset.app import create_app
app = create_app()
with app.app_context():
from superset.extensions import db
from superset.connectors.sqla.models import SqlaTable
from superset.models.core import Database
# 找到 MOMO_UAT 資料庫
database = db.session.query(Database).filter_by(database_name="MOMO_UAT").first()
if not database:
logger.error("找不到 MOMO_UAT 資料庫,請先新增資料庫連線")
sys.exit(1)
logger.info(f"找到資料庫: {database.database_name} (ID: {database.id})")
created_count = 0
for ds_config in DATASETS:
table_name = ds_config["table_name"]
# 檢查是否已存在
existing = db.session.query(SqlaTable).filter_by(
table_name=table_name,
database_id=database.id
).first()
if existing:
logger.info(f"資料集已存在: {table_name}")
continue
# 建立新資料集
dataset = SqlaTable(
table_name=table_name,
database_id=database.id,
schema="public",
description=ds_config.get("description", ""),
)
# 設定時間欄位
if ds_config.get("main_dttm_col"):
dataset.main_dttm_col = ds_config["main_dttm_col"]
db.session.add(dataset)
logger.info(f"建立資料集: {table_name}")
created_count += 1
db.session.commit()
logger.info(f"完成! 建立了 {created_count} 個資料集")
# 同步資料集欄位
logger.info("同步資料集欄位...")
for ds_config in DATASETS:
table_name = ds_config["table_name"]
dataset = db.session.query(SqlaTable).filter_by(
table_name=table_name,
database_id=database.id
).first()
if dataset:
try:
dataset.fetch_metadata()
logger.info(f"已同步欄位: {table_name}")
except Exception as e:
logger.warning(f"同步欄位失敗 {table_name}: {e}")
db.session.commit()
logger.info("資料集設定完成!")
if __name__ == "__main__":
setup_datasets()

View File

@@ -0,0 +1,52 @@
-- =============================================================================
-- Superset 唯讀用戶設定
-- 用於 UAT 和 GCP 環境的 PostgreSQL 資料庫
-- =============================================================================
-- 建立唯讀用戶 (在 UAT 資料庫執行)
-- 連線到 momo_analytics 資料庫後執行
-- 1. 建立唯讀角色
CREATE ROLE superset_readonly WITH LOGIN PASSWORD 'Wooo_Superset_RO_2026';
-- 2. 授予連線權限
GRANT CONNECT ON DATABASE momo_analytics TO superset_readonly;
-- 3. 授予 schema 使用權限
GRANT USAGE ON SCHEMA public TO superset_readonly;
-- 4. 授予所有現有資料表的 SELECT 權限
GRANT SELECT ON ALL TABLES IN SCHEMA public TO superset_readonly;
-- 5. 設定預設權限 (新建立的資料表也會自動授權)
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO superset_readonly;
-- 6. 授予序列讀取權限 (某些查詢可能需要)
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO superset_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO superset_readonly;
-- =============================================================================
-- 驗證權限
-- =============================================================================
-- 使用 superset_readonly 用戶連線後測試:
-- SELECT * FROM products LIMIT 5;
-- SELECT * FROM daily_sales_snapshot LIMIT 5;
-- SELECT * FROM price_records LIMIT 5;
-- =============================================================================
-- 連線字串 (供 Superset 使用)
-- =============================================================================
-- UAT 環境:
-- postgresql+psycopg2://superset_readonly:Wooo_Superset_RO_2026@momo-postgres:5432/momo_analytics
--
-- GCP 環境 (需要從 UAT 連線):
-- postgresql+psycopg2://superset_readonly:Wooo_Superset_RO_2026@35.194.233.141:5432/momo_analytics
-- =============================================================================
-- 撤銷權限 (如需移除)
-- =============================================================================
-- REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM superset_readonly;
-- REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM superset_readonly;
-- REVOKE USAGE ON SCHEMA public FROM superset_readonly;
-- REVOKE CONNECT ON DATABASE momo_analytics FROM superset_readonly;
-- DROP ROLE superset_readonly;

View File

@@ -0,0 +1,164 @@
# =============================================================================
# Apache Superset Configuration
# MOMO Pro System - UAT 環境
# =============================================================================
import os
# ---------------------------------------------------------
# Superset 基本設定
# ---------------------------------------------------------
ROW_LIMIT = 50000
SUPERSET_WEBSERVER_PORT = 8088
SUPERSET_WEBSERVER_TIMEOUT = 120
# ---------------------------------------------------------
# 反向代理設定 (Nginx 子路徑 /superset/)
# 2026-02-13 更新:使用 APPLICATION_ROOT 徹底修復 URL 問題
# ---------------------------------------------------------
ENABLE_PROXY_FIX = True
PROXY_FIX_CONFIG = {
"x_for": 1, # 信任 X-Forwarded-For
"x_proto": 1, # 信任 X-Forwarded-Proto
"x_host": 1, # 信任 X-Forwarded-Host
"x_prefix": 0, # 禁用!讓 APPLICATION_ROOT 處理
}
# =============================================================================
# 關鍵設定Cookie 路徑
# =============================================================================
# 必須使用 "/" 因為 Superset 的登入頁面是 /login/(不在 /superset/ 下)
# 如果設為 /superset/cookie 不會被發送到 /login/ 頁面
SESSION_COOKIE_PATH = "/"
# Secret key (必須設定)
SECRET_KEY = os.environ.get('SUPERSET_SECRET_KEY', 'wooo_superset_secret_key_2026_momo_pro')
# ---------------------------------------------------------
# 資料庫設定 (Superset Metadata)
# ---------------------------------------------------------
SQLALCHEMY_DATABASE_URI = (
f"postgresql+psycopg2://"
f"{os.environ.get('DATABASE_USER', 'superset')}:"
f"{os.environ.get('DATABASE_PASSWORD', 'Wooo_Superset_DB_2026')}@"
f"{os.environ.get('DATABASE_HOST', 'superset-db')}:"
f"{os.environ.get('DATABASE_PORT', '5432')}/"
f"{os.environ.get('DATABASE_DB', 'superset')}"
)
# ---------------------------------------------------------
# Redis 快取設定
# ---------------------------------------------------------
REDIS_HOST = os.environ.get('REDIS_HOST', 'superset-redis')
REDIS_PORT = os.environ.get('REDIS_PORT', 6379)
CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 300,
'CACHE_KEY_PREFIX': 'superset_',
'CACHE_REDIS_HOST': REDIS_HOST,
'CACHE_REDIS_PORT': REDIS_PORT,
'CACHE_REDIS_DB': 0,
}
DATA_CACHE_CONFIG = {
'CACHE_TYPE': 'RedisCache',
'CACHE_DEFAULT_TIMEOUT': 600,
'CACHE_KEY_PREFIX': 'superset_data_',
'CACHE_REDIS_HOST': REDIS_HOST,
'CACHE_REDIS_PORT': REDIS_PORT,
'CACHE_REDIS_DB': 1,
}
# ---------------------------------------------------------
# 語言和時區設定
# ---------------------------------------------------------
BABEL_DEFAULT_LOCALE = 'zh_Hant_TW'
BABEL_DEFAULT_FOLDER = 'superset/translations'
LANGUAGES = {
'en': {'flag': 'us', 'name': 'English'},
'zh': {'flag': 'cn', 'name': '简体中文'},
'zh_Hant_TW': {'flag': 'tw', 'name': '繁體中文'},
}
# 時區設定
DEFAULT_TIMEZONE = 'Asia/Taipei'
# ---------------------------------------------------------
# 功能開關
# ---------------------------------------------------------
FEATURE_FLAGS = {
'ENABLE_TEMPLATE_PROCESSING': True,
'DASHBOARD_NATIVE_FILTERS': True,
'DASHBOARD_CROSS_FILTERS': True,
'DASHBOARD_NATIVE_FILTERS_SET': True,
'ALERT_REPORTS': True,
'EMBEDDABLE_CHARTS': True,
'EMBEDDED_SUPERSET': True,
# 關閉 Global Async Queries避免 WebSocket 連接問題)
'GLOBAL_ASYNC_QUERIES': False,
}
# =============================================================================
# 禁用 WebSocket避免瀏覽器嘗試連接 ws://127.0.0.1:8080
# =============================================================================
# 這個 URL 會被嵌入到頁面的 JavaScript 中
# 設為空字串來避免瀏覽器嘗試建立 WebSocket 連接
GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = ""
# ---------------------------------------------------------
# 安全設定
# ---------------------------------------------------------
# 允許嵌入 iframe
HTTP_HEADERS = {
'X-Frame-Options': 'SAMEORIGIN',
}
# =============================================================================
# CSRF 設定 - 完全禁用
# =============================================================================
# Superset 6.0 SPA 架構下CSRF 與 React SPA 有衝突
# SPA 無法正確從 HttpOnly session cookie 中讀取 CSRF token
# Superset 內部 API 已有 JWT/Session 認證機制,禁用 CSRF 不會影響安全性
WTF_CSRF_ENABLED = False
# 保留以下設定以防未來需要啟用
WTF_CSRF_EXEMPT_LIST = []
WTF_CSRF_TIME_LIMIT = 60 * 60 * 24 * 7 # 7 天
WTF_CSRF_SSL_STRICT = False
# ---------------------------------------------------------
# 資料庫連線設定
# ---------------------------------------------------------
# 預設允許的資料庫驅動
PREFERRED_DATABASES = [
'PostgreSQL',
]
# SQL Lab 設定
SQL_MAX_ROW = 100000
DISPLAY_MAX_ROW = 10000
# ---------------------------------------------------------
# 日誌設定
# ---------------------------------------------------------
LOG_FORMAT = '%(asctime)s:%(levelname)s:%(name)s:%(message)s'
LOG_LEVEL = 'INFO'
# ---------------------------------------------------------
# 郵件設定 (告警報表用)
# ---------------------------------------------------------
SMTP_HOST = 'smtp.gmail.com'
SMTP_STARTTLS = True
SMTP_SSL = False
SMTP_PORT = 587
SMTP_MAIL_FROM = 'superset@wooo.work'
# ---------------------------------------------------------
# 額外的 Jinja 模板函數
# ---------------------------------------------------------
from flask import g
JINJA_CONTEXT_ADDONS = {
'current_user_id': lambda: g.user.id if hasattr(g, 'user') and g.user else None,
'current_username': lambda: g.user.username if hasattr(g, 'user') and g.user else None,
}