- 建立 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:
45
docker/alertmanager/alertmanager.yml
Normal file
45
docker/alertmanager/alertmanager.yml
Normal 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']
|
||||
89
docker/blackbox/blackbox.yml
Normal file
89
docker/blackbox/blackbox.yml
Normal 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
|
||||
18
docker/grafana/provisioning/dashboards/dashboards.yaml
Normal file
18
docker/grafana/provisioning/dashboards/dashboards.yaml
Normal 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
|
||||
@@ -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": ""
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
255
docker/grafana/provisioning/dashboards/json/logs-dashboard.json
Normal file
255
docker/grafana/provisioning/dashboards/json/logs-dashboard.json
Normal 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
|
||||
}
|
||||
675
docker/grafana/provisioning/dashboards/json/system-overview.json
Normal file
675
docker/grafana/provisioning/dashboards/json/system-overview.json
Normal 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": ""
|
||||
}
|
||||
40
docker/grafana/provisioning/datasources/datasources.yaml
Normal file
40
docker/grafana/provisioning/datasources/datasources.yaml
Normal 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}"
|
||||
58
docker/loki/loki-config.yaml
Normal file
58
docker/loki/loki-config.yaml
Normal 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
|
||||
268
docker/nginx-monitor/conf.d/monitor.conf
Normal file
268
docker/nginx-monitor/conf.d/monitor.conf
Normal 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>';
|
||||
}
|
||||
}
|
||||
362
docker/nginx-monitor/html/index.html
Normal file
362
docker/nginx-monitor/html/index.html
Normal 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>© 2026 WOOO TECH. All rights reserved.</p>
|
||||
<p><a href="https://mo.wooo.work/">返回 Momo Pro System</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
docker/nginx-monitor/html/static/images/WOOO_Logo_trimmed.jpg
Normal file
BIN
docker/nginx-monitor/html/static/images/WOOO_Logo_trimmed.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
BIN
docker/nginx-monitor/html/static/images/WOOO_Main_Logo.jpg
Normal file
BIN
docker/nginx-monitor/html/static/images/WOOO_Main_Logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 394 KiB |
BIN
docker/nginx-monitor/html/static/images/logo_v4_glass.png
Normal file
BIN
docker/nginx-monitor/html/static/images/logo_v4_glass.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 585 KiB |
44
docker/nginx-monitor/nginx.conf
Normal file
44
docker/nginx-monitor/nginx.conf
Normal 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;
|
||||
}
|
||||
BIN
docker/nginx-monitor/static/images/logo_v4_glass.png
Normal file
BIN
docker/nginx-monitor/static/images/logo_v4_glass.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 585 KiB |
69
docker/nginx/conf.d/default.conf
Normal file
69
docker/nginx/conf.d/default.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
75
docker/nginx/html/api/health-check.sh
Normal file
75
docker/nginx/html/api/health-check.sh
Normal 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
|
||||
58
docker/nginx/html/api/health.sh
Normal file
58
docker/nginx/html/api/health.sh
Normal 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 '}'
|
||||
804
docker/nginx/html/monitor-index-clean.html
Normal file
804
docker/nginx/html/monitor-index-clean.html
Normal 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>
|
||||
905
docker/nginx/html/monitor-index.html
Normal file
905
docker/nginx/html/monitor-index.html
Normal 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>© 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>
|
||||
382
docker/nginx/html/monitor-realtime.html
Normal file
382
docker/nginx/html/monitor-realtime.html
Normal 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 © 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>
|
||||
BIN
docker/nginx/html/static/images/WOOO_Logo_trimmed.jpg
Normal file
BIN
docker/nginx/html/static/images/WOOO_Logo_trimmed.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
430
docker/nginx/monitor-clean.conf
Normal file
430
docker/nginx/monitor-clean.conf
Normal 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
56
docker/nginx/nginx.conf
Normal 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;
|
||||
}
|
||||
449
docker/nginx/sites-available/monitor
Normal file
449
docker/nginx/sites-available/monitor
Normal 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;
|
||||
|
||||
}
|
||||
67
docker/postgres/init/01-init.sql
Normal file
67
docker/postgres/init/01-init.sql
Normal 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 $$;
|
||||
81
docker/postgres/postgresql.conf
Normal file
81
docker/postgres/postgresql.conf
Normal 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
|
||||
223
docker/prometheus/alert_rules.yml
Normal file
223
docker/prometheus/alert_rules.yml
Normal 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 資料庫無法連接,請立即檢查"
|
||||
326
docker/prometheus/prometheus.yml
Normal file
326
docker/prometheus/prometheus.yml
Normal 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'
|
||||
119
docker/promtail/promtail-config.yaml
Normal file
119
docker/promtail/promtail-config.yaml
Normal 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
|
||||
28
docker/registry/config.yml
Normal file
28
docker/registry/config.yml
Normal 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
|
||||
74
docker/registry/docker-compose.yml
Normal file
74
docker/registry/docker-compose.yml
Normal 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
186
docker/registry/setup.sh
Normal 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 "$@"
|
||||
321
docker/superset/DASHBOARD_GUIDE.md
Normal file
321
docker/superset/DASHBOARD_GUIDE.md
Normal 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. 評估是否嵌入或取代現有頁面
|
||||
466
docker/superset/IMPLEMENTATION_GUIDE.md
Normal file
466
docker/superset/IMPLEMENTATION_GUIDE.md
Normal 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
202
docker/superset/README.md
Normal 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**: 初始部署
|
||||
12
docker/superset/custom-assets/theme.entry.js
Normal file
12
docker/superset/custom-assets/theme.entry.js
Normal 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
186
docker/superset/deploy.sh
Executable 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
|
||||
113
docker/superset/docker-compose.yml
Normal file
113
docker/superset/docker-compose.yml
Normal 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
|
||||
118
docker/superset/nginx-superset.conf
Normal file
118
docker/superset/nginx-superset.conf
Normal 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=0,Superset 不讀取這些標頭
|
||||
|
||||
# 關鍵設定 1:Superset 內部路由已是 /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;
|
||||
# }
|
||||
# }
|
||||
120
docker/superset/setup_datasets.py
Normal file
120
docker/superset/setup_datasets.py
Normal 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()
|
||||
52
docker/superset/setup_readonly_users.sql
Normal file
52
docker/superset/setup_readonly_users.sql
Normal 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;
|
||||
164
docker/superset/superset_config.py
Normal file
164
docker/superset/superset_config.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user