[#24] 优化alertmanager和web的构建

[#25] 反向代理功能实现
[#14] 前端页面优化
This commit is contained in:
xiuting.xu 2025-10-16 15:34:20 +08:00
parent abc739b1be
commit 084c0a3719
38 changed files with 306 additions and 245 deletions

View File

@ -19,8 +19,11 @@ RUN wget https://github.com/prometheus/alertmanager/releases/download/v${ALERTMA
rm alertmanager-${ALERTMANAGER_VERSION}.linux-amd64.tar.gz rm alertmanager-${ALERTMANAGER_VERSION}.linux-amd64.tar.gz
ENV ALERTMANAGER_BASE_PATH=/private/argus/alert/alertmanager ENV ALERTMANAGER_BASE_PATH=/private/argus/alert/alertmanager
ENV ARGUS_UID=2133
ENV ARGUS_GID=2015 ARG ARGUS_UID=2133
ARG ARGUS_GID=2015
ENV ARGUS_UID=${ARGUS_UID}
ENV ARGUS_GID=${ARGUS_GID}
RUN mkdir -p /usr/share/alertmanager && \ RUN mkdir -p /usr/share/alertmanager && \
mkdir -p ${ALERTMANAGER_BASE_PATH} && \ mkdir -p ${ALERTMANAGER_BASE_PATH} && \

View File

@ -0,0 +1,13 @@
#!/bin/bash
set -euo pipefail
docker pull ubuntu:24.04
source src/alert/tests/.env
docker build \
--build-arg ARGUS_UID=${ARGUS_UID} \
--build-arg ARGUS_GID=${ARGUS_GID} \
-f src/alert/alertmanager/build/Dockerfile \
-t argus-alertmanager:latest .
docker save -o argus-alertmanager.tar argus-alertmanager:latest

View File

@ -18,6 +18,7 @@ DOMAIN=alertmanager.alert.argus.com
IP=$(ifconfig | grep -A 1 eth0 | grep inet | awk '{print $2}') IP=$(ifconfig | grep -A 1 eth0 | grep inet | awk '{print $2}')
echo "current IP: ${IP}" echo "current IP: ${IP}"
echo "${IP}" > /private/argus/etc/${DOMAIN} echo "${IP}" > /private/argus/etc/${DOMAIN}
chmod 755 /private/argus/etc/${DOMAIN}
echo "[INFO] Starting Alertmanager process..." echo "[INFO] Starting Alertmanager process..."

View File

@ -55,6 +55,6 @@ alerting:
alertmanagers: alertmanagers:
- static_configs: - static_configs:
- targets: - targets:
- "localhost:9093" # Alertmanager 地址 - "alertmanager.alert.argus.com:9093" # Alertmanager 地址
``` ```

5
src/alert/tests/.env Normal file
View File

@ -0,0 +1,5 @@
DATA_ROOT=/home/argus/tmp/private/argus
ARGUS_UID=1048
ARGUS_GID=1048
USE_INTRANET=false

View File

@ -1,19 +0,0 @@
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'instance'] # 分组:相同 alertname + instance 的告警合并
group_wait: 30s # 第一个告警后,等 30s 看是否有同组告警一起发
group_interval: 5m # 同组告警变化后,至少 5 分钟再发一次
repeat_interval: 3h # 相同告警3 小时重复提醒一次
receiver: 'null'
receivers:
- name: 'null'
inhibit_rules:
- source_match:
severity: 'critical' # critical 告警存在时
target_match:
severity: 'warning' # 抑制相同 instance 的 warning 告警
equal: ['instance']

View File

@ -1 +0,0 @@
172.18.0.2

View File

@ -1,4 +1,3 @@
version: '3.8'
services: services:
alertmanager: alertmanager:
build: build:
@ -17,20 +16,21 @@ services:
ports: ports:
- "${ARGUS_PORT:-9093}:9093" - "${ARGUS_PORT:-9093}:9093"
volumes: volumes:
- ${DATA_ROOT:-./data}/alertmanager:/private/argus/alert/alertmanager - ${DATA_ROOT:-./data}/alert/alertmanager:/private/argus/alert/alertmanager
- ${DATA_ROOT:-./data}/etc:/private/argus/etc - ${DATA_ROOT:-./data}/etc:/private/argus/etc
networks: networks:
- argus-network - argus-debug-net
restart: unless-stopped restart: unless-stopped
logging: logging:
driver: "json-file" driver: "json-file"
options: options:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
networks: networks:
argus-network: argus-debug-net:
driver: bridge driver: bridge
name: argus-network name: argus-debug-net
volumes: volumes:
alertmanager_data: alertmanager_data:

View File

@ -24,8 +24,10 @@ RUN apt-get update && \
apt-get clean && rm -rf /var/lib/apt/lists/* apt-get clean && rm -rf /var/lib/apt/lists/*
ENV FRONTEND_BASE_PATH=/private/argus/web/frontend ENV FRONTEND_BASE_PATH=/private/argus/web/frontend
ENV ARGUS_UID=2133 ARG ARGUS_UID=2133
ENV ARGUS_GID=2015 ARG ARGUS_GID=2015
ENV ARGUS_UID=${ARGUS_UID}
ENV ARGUS_GID=${ARGUS_GID}
RUN mkdir -p ${FRONTEND_BASE_PATH} && \ RUN mkdir -p ${FRONTEND_BASE_PATH} && \
mkdir -p /private/argus/etc mkdir -p /private/argus/etc
@ -82,7 +84,7 @@ COPY src/web/build_tools/frontend/health-check.sh /usr/local/bin/health-check.sh
RUN chmod +x /usr/local/bin/health-check.sh RUN chmod +x /usr/local/bin/health-check.sh
# 暴露端口 # 暴露端口
EXPOSE 80 EXPOSE 8080
# 保持 root 用户,由 supervisor 控制 user 切换 # 保持 root 用户,由 supervisor 控制 user 切换
USER root USER root

View File

@ -1,4 +1,7 @@
docker pull node:20 docker pull node:20
docker pull ubuntu:24.04 docker pull ubuntu:24.04
docker build -f src/web/build_tools/frontend/Dockerfile -t argus-web:0.1.1 . export ARGUS_UID=1048
rm -f argus-web-0.1.1.tar && sudo docker image save argus-web:0.1.1 > argus-web-0.1.1.tar export ARGUS_GID=1048
docker build -f src/web/build_tools/frontend/Dockerfile -t argus-web-frontend:latest .
docker save -o argus-web-frontend-latest.tar argus-web-frontend:latest

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
URL="http://127.0.0.1:80" URL="http://127.0.0.1:8080"
echo "[INFO] Starting Argus web health check loop for $URL..." echo "[INFO] Starting Argus web health check loop for $URL..."

View File

@ -12,7 +12,7 @@ http {
# React 前端服务 # React 前端服务
server { server {
listen 80; listen 8080;
server_name web.argus.com; server_name web.argus.com;
root /usr/share/nginx/html; root /usr/share/nginx/html;
@ -24,33 +24,4 @@ http {
} }
} }
# Master 服务,需要增加 CORS 支持
server {
listen 80;
server_name master.argus.com;
location / {
proxy_pass http://master.argus.com;
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;
# CORS 支持
add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
}
}
} }

View File

@ -8,21 +8,12 @@ DNS_SCRIPT="${DNS_DIR}/update-dns.sh"
DOMAIN=web.argus.com DOMAIN=web.argus.com
WEB_DOMAIN_FILE="${DNS_DIR}/${DOMAIN}" WEB_DOMAIN_FILE="${DNS_DIR}/${DOMAIN}"
RUNTIME_USER="${ARGUS_RUNTIME_USER:-argus}" RUNTIME_USER="${ARGUS_RUNTIME_USER:-argus}"
RUNTIME_UID="${ARGUS_BUILD_UID:-2133}" RUNTIME_UID="${ARGUS_UID:-2133}"
RUNTIME_GID="${ARGUS_BUILD_GID:-2015}" RUNTIME_GID="${ARGUS_GID:-2015}"
mkdir -p "$DNS_DIR" mkdir -p "$DNS_DIR"
chown -R "$RUNTIME_UID:$RUNTIME_GID" "$DNS_DIR" 2>/dev/null || true chown -R "$RUNTIME_UID:$RUNTIME_GID" "$DNS_DIR" 2>/dev/null || true
if [[ -x "$DNS_SCRIPT" ]]; then
echo "[INFO] Running update-dns.sh before master starts"
# 若脚本存在则执行,保证容器使用 bind 作为 DNS
"$DNS_SCRIPT" || echo "[WARN] update-dns.sh execution failed"
else
echo "[WARN] DNS update script not found or not executable: $DNS_SCRIPT"
fi
# 记录容器 IP # 记录容器 IP
IP=$(ifconfig | grep -A 1 eth0 | grep inet | awk '{print $2}' || true) IP=$(ifconfig | grep -A 1 eth0 | grep inet | awk '{print $2}' || true)
if [[ -n "${IP}" ]]; then if [[ -n "${IP}" ]]; then
@ -32,6 +23,7 @@ if [[ -n "${IP}" ]]; then
else else
echo "[WARN] Failed to detect web IP via ifconfig" echo "[WARN] Failed to detect web IP via ifconfig"
fi fi
chmod 755 "$WEB_DOMAIN_FILE"
echo "[INFO] Launching nginx..." echo "[INFO] Launching nginx..."

View File

@ -8,8 +8,10 @@ RUN apt-get update && \
apt-get clean && rm -rf /var/lib/apt/lists/* apt-get clean && rm -rf /var/lib/apt/lists/*
ENV FRONTEND_BASE_PATH=/private/argus/web/proxy ENV FRONTEND_BASE_PATH=/private/argus/web/proxy
ENV ARGUS_UID=2133 ARG ARGUS_UID=2133
ENV ARGUS_GID=2015 ARG ARGUS_GID=2015
ENV ARGUS_UID=${ARGUS_UID}
ENV ARGUS_GID=${ARGUS_GID}
RUN mkdir -p ${FRONTEND_BASE_PATH} && \ RUN mkdir -p ${FRONTEND_BASE_PATH} && \
mkdir -p /private/argus/etc mkdir -p /private/argus/etc

View File

@ -0,0 +1,6 @@
docker pull ubuntu:24.04
export ARGUS_UID=1048
export ARGUS_GID=1048
docker build -f src/web/build_tools/proxy/Dockerfile -t argus-web-proxy:latest .
docker save -o argus-web-proxy-latest.tar argus-web-proxy:latest

View File

@ -3,6 +3,7 @@ server {
server_name alertmanager.alert.argus.com; server_name alertmanager.alert.argus.com;
location / { location / {
proxy_pass http://alertmanager.alert.argus.com:9093; set $alert_backend http://alertmanager.alert.argus.com:9093;
proxy_pass $alert_backend;
} }
} }

View File

@ -4,7 +4,8 @@ server {
server_name es.log.argus.com; server_name es.log.argus.com;
location / { location / {
proxy_pass http://es.log.argus.com; set $es_backend http://es.log.argus.com:9200;
proxy_pass $es_backend;
} }
} }
@ -14,6 +15,7 @@ server {
server_name kibana.log.argus.com; server_name kibana.log.argus.com;
location / { location / {
proxy_pass http://kibana.log.argus.com; set $kibana_backend http://kibana.log.argus.com:5601;
proxy_pass $kibana_backend;
} }
} }

View File

@ -3,25 +3,25 @@ server {
server_name master.argus.com; server_name master.argus.com;
location / { location / {
# proxy_pass http://master.argus.com; set $master_backend http://master.argus.com:3000;
proxy_pass http://master.argus.com; proxy_pass $master_backend;
# proxy_set_header Host $host; proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# # CORS 支持 # CORS 支持
# add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always; add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always;
# add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
# add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always; add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
# if ($request_method = OPTIONS) { if ($request_method = OPTIONS) {
# add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always; add_header 'Access-Control-Allow-Origin' 'http://web.argus.com' always;
# add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
# add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always; add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Authorization' always;
# add_header 'Content-Length' 0; add_header 'Content-Length' 0;
# add_header 'Content-Type' 'text/plain'; add_header 'Content-Type' 'text/plain';
# return 204; return 204;
# } }
} }
} }

View File

@ -4,16 +4,18 @@ server {
server_name prometheus.metric.argus.com; server_name prometheus.metric.argus.com;
location / { location / {
proxy_pass http://prom.metric.argus.com; set $prom_backend http://prom.metric.argus.com:9090;
proxy_pass $prom_backend;
} }
} }
# # Grafana # Grafana
# server { server {
# listen 80; listen 80;
# server_name grafana.metric.argus.com; server_name grafana.metric.argus.com;
# location / { location / {
# proxy_pass http://grafana.metric.argus.com; set $grafana_backend http://grafana.metric.argus.com:3000;
# } proxy_pass $grafana_backend;
# } }
}

View File

@ -3,6 +3,7 @@ server {
server_name web.argus.com; server_name web.argus.com;
location / { location / {
proxy_pass http://web.argus.com:80; set $web_backend http://web.argus.com:8080;
proxy_pass $web_backend;
} }
} }

View File

@ -5,14 +5,6 @@ events {
worker_connections 1024; worker_connections 1024;
} }
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://web.argus.com:80;
}
}
http { http {
include mime.types; include mime.types;
@ -32,5 +24,16 @@ http {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
server {
listen 80 default_server;
server_name _;
location / {
set $web_backend http://web.argus.com:8080;
proxy_pass $web_backend;
}
}
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
} }

View File

@ -9,8 +9,8 @@ DNS_CONF_PRIVATE="/private/argus/etc/dns.conf"
DNS_CONF_SYSTEM="/etc/resolv.conf" DNS_CONF_SYSTEM="/etc/resolv.conf"
DNS_DIR="/private/argus/etc" DNS_DIR="/private/argus/etc"
DNS_SCRIPT="${DNS_DIR}/update-dns.sh" DNS_SCRIPT="${DNS_DIR}/update-dns.sh"
RUNTIME_UID="${ARGUS_BUILD_UID:-2133}" RUNTIME_UID="${ARGUS_UID:-2133}"
RUNTIME_GID="${ARGUS_BUILD_GID:-2015}" RUNTIME_GID="${ARGUS_GID:-2015}"
mkdir -p "$DNS_DIR" mkdir -p "$DNS_DIR"
chown -R "$RUNTIME_UID:$RUNTIME_GID" "$DNS_DIR" 2>/dev/null || true chown -R "$RUNTIME_UID:$RUNTIME_GID" "$DNS_DIR" 2>/dev/null || true

View File

@ -8,10 +8,10 @@ export function AlertFilters({ filters, setFilters, nodeOptions }) {
value={filters.severity} value={filters.severity}
onChange={(value) => setFilters((f) => ({ ...f, severity: value }))} onChange={(value) => setFilters((f) => ({ ...f, severity: value }))}
data={[ data={[
{ value: "all", label: "全部" }, { value: "all", label: "all" },
{ value: "critical", label: "严重" }, { value: "critical", label: "critical" },
{ value: "warning", label: "警告" }, { value: "warning", label: "warning" },
{ value: "info", label: "信息" }, { value: "info", label: "info" },
]} ]}
w={150} w={150}
/> />
@ -20,7 +20,7 @@ export function AlertFilters({ filters, setFilters, nodeOptions }) {
value={filters.state} value={filters.state}
onChange={(value) => setFilters((f) => ({ ...f, state: value }))} onChange={(value) => setFilters((f) => ({ ...f, state: value }))}
data={[ data={[
{ value: "all", label: "全部" }, { value: "all", label: "all" },
{ value: "active", label: "Active" }, { value: "active", label: "Active" },
{ value: "resolved", label: "Resolved" }, { value: "resolved", label: "Resolved" },
]} ]}

View File

@ -1,5 +1,6 @@
import { Table, Group, ActionIcon, Button } from "@mantine/core"; import { Table, Group, ActionIcon, Button, Code } from "@mantine/core";
import { IconChevronUp, IconChevronDown } from "@tabler/icons-react"; import { IconChevronUp, IconChevronDown, IconInfoCircle } from "@tabler/icons-react";
import { useState } from "react";
export function AlertTable({ export function AlertTable({
alerts, alerts,
@ -16,6 +17,11 @@ export function AlertTable({
formatRelativeTime, formatRelativeTime,
}) { }) {
const totalPages = Math.ceil(sortedAlerts.length / pageSize); const totalPages = Math.ceil(sortedAlerts.length / pageSize);
const [expandedRow, setExpandedRow] = useState(null);
const toggleExpand = (index) => {
setExpandedRow(expandedRow === index ? null : index);
};
return ( return (
<> <>
@ -47,24 +53,52 @@ export function AlertTable({
</Group> </Group>
</Table.Th> </Table.Th>
))} ))}
<Table.Th>更多信息</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{paginatedAlerts.map((alert, i) => ( {paginatedAlerts.map((alert, i) => (
<Table.Tr key={i} style={{ backgroundColor: getRowColor(alert) }}> <>
<Table.Td>{alert.labels?.alertname || "-"}</Table.Td> <Table.Tr key={i} style={{ backgroundColor: getRowColor(alert) }}>
<Table.Td>{alert.labels?.instance || "-"}</Table.Td> <Table.Td>{alert.labels?.alertname || "-"}</Table.Td>
<Table.Td style={{ color: getSeverityColor(alert.labels?.severity) }}> <Table.Td>{alert.labels?.instance || "-"}</Table.Td>
{alert.labels?.severity || "info"} <Table.Td style={{ color: getSeverityColor(alert.labels?.severity) }}>
</Table.Td> {alert.labels?.severity || "info"}
<Table.Td>{getStateBadge(alert.status?.state)}</Table.Td> </Table.Td>
<Table.Td title={alert.startsAt || "-"}>{formatRelativeTime(alert.startsAt)}</Table.Td> <Table.Td>{getStateBadge(alert.status?.state)}</Table.Td>
<Table.Td title={alert.endsAt || "-"}> <Table.Td title={alert.startsAt || "-"}>{formatRelativeTime(alert.startsAt)}</Table.Td>
{alert.endsAt ? new Date(alert.endsAt).toLocaleString() : "-"} <Table.Td title={alert.endsAt || "-"}>
</Table.Td> {alert.endsAt ? new Date(alert.endsAt).toLocaleString() : "-"}
<Table.Td title={alert.updatedAt || "-"}>{formatRelativeTime(alert.updatedAt)}</Table.Td> </Table.Td>
<Table.Td>{alert.annotations?.summary || "-"}</Table.Td> <Table.Td title={alert.updatedAt || "-"}>{formatRelativeTime(alert.updatedAt)}</Table.Td>
</Table.Tr> <Table.Td>{alert.annotations?.summary || "-"}</Table.Td>
<Table.Td>
<ActionIcon
onClick={() => toggleExpand(i)}
variant="subtle"
color="blue"
title="显示/隐藏更多信息"
>
<IconInfoCircle size={18} />
</ActionIcon>
</Table.Td>
</Table.Tr>
{expandedRow === i && (
<Table.Tr key={`details-${i}`} style={{ backgroundColor: "#f9fafb" }}>
<Table.Td colSpan={9}>
<div style={{ fontSize: "0.85rem", lineHeight: 1.5 }}>
{Object.entries(alert.labels || {}).map(([k, v]) => (
<div key={k}>
<Code color="blue">{k}</Code> {v}
</div>
))}
</div>
</Table.Td>
</Table.Tr>
)}
</>
))} ))}
</Table.Tbody> </Table.Tbody>
</Table> </Table>

View File

@ -102,11 +102,6 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) {
{/* 滚动内容 */} {/* 滚动内容 */}
<ScrollArea style={{ flex: 1 }}> <ScrollArea style={{ flex: 1 }}>
<Stack spacing="md"> <Stack spacing="md">
{/* 配置信息 */}
<NodeConfigCard nodeId={node.id} config={node.config || {}} onSaved={() => fetchNodeDetail(node.id)} />
{/* 标签信息 */}
<NodeLabelCard nodeId={node.id} labels={Array.isArray(node.label) ? node.label : []} onSaved={() => fetchNodeDetail(node.id)} />
{/* 元数据 */} {/* 元数据 */}
<NodeMetaCard node={node} /> <NodeMetaCard node={node} />
@ -114,6 +109,12 @@ export default function NodeDetailDrawer({ opened, nodeId, onClose }) {
{/* 健康信息 */} {/* 健康信息 */}
<NodeHealthCard node={node} /> <NodeHealthCard node={node} />
{/* 配置信息 */}
<NodeConfigCard nodeId={node.id} config={node.config || {}} onSaved={() => fetchNodeDetail(node.id)} />
{/* 标签信息 */}
<NodeLabelCard nodeId={node.id} labels={Array.isArray(node.label) ? node.label : []} onSaved={() => fetchNodeDetail(node.id)} />
{/* 其他基础信息展示 */} {/* 其他基础信息展示 */}
<Stack spacing="xs"> <Stack spacing="xs">
<Text fw={500}>注册时间: <Text span c="dimmed">{new Date(node.register_time).toLocaleString()}</Text></Text> <Text fw={500}>注册时间: <Text span c="dimmed">{new Date(node.register_time).toLocaleString()}</Text></Text>

View File

@ -2,54 +2,61 @@ import { useState } from "react";
import { Card, Text, Stack, Group, ActionIcon, Badge, Popover } from "@mantine/core"; import { Card, Text, Stack, Group, ActionIcon, Badge, Popover } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react"; import { IconInfoCircle } from "@tabler/icons-react";
//
function HealthItem({ moduleName, data }) {
const status = data?.status || "unknown";
const color = status === "healthy" ? "green" : status === "unhealthy" ? "red" : "gray";
const [opened, setOpened] = useState(false);
return (
<Group key={moduleName} spacing="xs" align="center">
<Text size="sm" fw={500}>{moduleName}</Text>
<Badge color={color} variant="light">{status}</Badge>
{(data?.error || data?.timestamp) && (
<Popover
opened={opened}
onClose={() => setOpened(false)}
position="bottom"
withArrow
shadow="sm"
>
<Popover.Target>
<ActionIcon
size="xs"
color="blue"
variant="light"
onClick={() => setOpened((o) => !o)}
>
<IconInfoCircle size={14} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
<Stack spacing={4}>
{data.error && <Text size="xs" c="red">Error: {data.error}</Text>}
{data.timestamp && (
<Text size="xs" c="dimmed">
Updated: {new Date(data.timestamp).toLocaleString()}
</Text>
)}
</Stack>
</Popover.Dropdown>
</Popover>
)}
</Group>
);
}
//
export default function NodeHealthCard({ node }) { export default function NodeHealthCard({ node }) {
const health = node.health || {}; const health = node.health || {};
const renderHealthItem = (moduleName, data) => {
const status = data?.status || "unknown";
const color = status === "healthy" ? "green" : status === "unhealthy" ? "red" : "gray";
const [opened, setOpened] = useState(false);
return (
<Group key={moduleName} spacing="xs" align="center">
<Text size="sm" fw={500}>{moduleName}</Text>
<Badge color={color} variant="light">{status}</Badge>
{(data?.error || data?.timestamp) && (
<Popover
opened={opened}
onClose={() => setOpened(false)}
position="bottom"
withArrow
shadow="sm"
>
<Popover.Target>
<ActionIcon size="xs" color="blue" variant="light" onClick={() => setOpened((o) => !o)}>
<IconInfoCircle size={14} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
<Stack spacing={4}>
{data.error && <Text size="xs" c="red">Error: {data.error}</Text>}
{data.timestamp && (
<Text size="xs" c="dimmed">
Updated: {new Date(data.timestamp).toLocaleString()}
</Text>
)}
</Stack>
</Popover.Dropdown>
</Popover>
)}
</Group>
);
};
return ( return (
<Card shadow="xs" radius="md" withBorder> <Card shadow="xs" radius="md" withBorder>
<Text fw={600} mb="sm">健康信息</Text> <Text fw={600} mb="sm">健康信息</Text>
<Stack spacing="xs"> <Stack spacing="xs">
{Object.entries(health).map(([moduleName, data]) => {Object.entries(health).map(([moduleName, data]) => (
renderHealthItem(moduleName, data) <HealthItem key={moduleName} moduleName={moduleName} data={data} />
)} ))}
</Stack> </Stack>
</Card> </Card>
); );

View File

@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
import NodeStatus from "./NodeStatus"; import NodeStatus from "./NodeStatus";
import PaginationControl from "./PaginationControl"; import PaginationControl from "./PaginationControl";
import { apiRequest } from "../config/request"; import { apiRequest } from "../config/request";
import { MASTER_API } from "../config/api"; import { MASTER_API, EXTERNAL_HOST } from "../config/api";
export function NodeTable({ export function NodeTable({
withSearch = false, withSearch = false,
@ -56,13 +56,28 @@ export function NodeTable({
<Table.Td>{node.version}</Table.Td> <Table.Td>{node.version}</Table.Td>
{withActions && ( {withActions && (
<Table.Td> <Table.Td>
<Button <Group spacing="xs">
size="xs" {/* 查看详情 */}
variant="light" <Button
onClick={() => fetchDetail && fetchDetail(node.id)} size="xs"
> variant="light"
查看详情 onClick={() => fetchDetail && fetchDetail(node.id)}
</Button> >
查看详情
</Button>
{/* Grafana Dashboard 链接 */}
<Button
size="xs"
variant="outline"
component="a"
href={`${EXTERNAL_HOST.GRAFANA}/d/${encodeURIComponent(node.name)}?orgId=1`}
target="_blank"
rel="noopener noreferrer"
>
Grafana
</Button>
</Group>
</Table.Td> </Table.Td>
)} )}
</Table.Tr> </Table.Tr>
@ -71,8 +86,6 @@ export function NodeTable({
return ( return (
<Card shadow="sm" radius="md" p="lg"> <Card shadow="sm" radius="md" p="lg">
{/* 标题 + 查看更多 */} {/* 标题 + 查看更多 */}
{(title || viewMoreLink) && ( {(title || viewMoreLink) && (
<Group position="apart" mb="sm"> <Group position="apart" mb="sm">
{title && <Text fw={700} size="lg">{title}</Text>} {title && <Text fw={700} size="lg">{title}</Text>}

View File

@ -12,7 +12,7 @@ export default function Sidebar() {
const location = useLocation(); // Sidebar const location = useLocation(); // Sidebar
const links = [ const links = [
{ to: "/dashboard", label: "概览仪表盘", icon: <IconGauge size={16} /> }, { to: "/dashboard", label: "仪表盘", icon: <IconGauge size={16} /> },
{ to: "/nodeInfo", label: "节点信息", icon: <IconServer size={16} /> }, { to: "/nodeInfo", label: "节点信息", icon: <IconServer size={16} /> },
{ to: "/metrics", label: "指标详情", icon: <IconActivity size={16} /> }, { to: "/metrics", label: "指标详情", icon: <IconActivity size={16} /> },
{ to: "/logs", label: "日志详情", icon: <IconFileText size={16} /> }, { to: "/logs", label: "日志详情", icon: <IconFileText size={16} /> },

View File

@ -17,14 +17,13 @@ export const MASTER_API = {
// 其他外部 API // 其他外部 API
export const EXTERNAL_API = { export const EXTERNAL_API = {
ALERTS_INFOS: "http://localhost:9093/api/v2/alerts", ALERTS_INFOS: "http://alertmanager.alert.argus.com/api/v2/alerts",
}; };
// 外部服务 Host // 外部服务 Host
export const EXTERNAL_HOST = { export const EXTERNAL_HOST = {
ALERTS: "http://localhost:9093", ALERTS: "http://alertmanager.alert.argus.com",
GRAFANA: "http://grafana.metric.argus.com", GRAFANA: "http://grafana.metric.argus.com/d/cluster-dashboard/cluster-dashboard?orgId=1&refresh=5s",
PROMETHEUS: "http://prometheus.metric.argus.com", PROMETHEUS: "http://prometheus.metric.argus.com",
ES: "http://es.log.argus.com", KIBANA: "http://kibana.log.argus.com/app/discover",
KIBANA: "http://kibana.log.argus.com",
}; };

View File

@ -10,7 +10,6 @@ export const metricsEntries = [
]; ];
export const logsEntries = [ export const logsEntries = [
{ label: "Elasticsearch", href: EXTERNAL_HOST.ES, icon: esLogo },
{ label: "Kibana", href: EXTERNAL_HOST.KIBANA, icon: kibanaLogo }, { label: "Kibana", href: EXTERNAL_HOST.KIBANA, icon: kibanaLogo },
]; ];

View File

@ -1,5 +1,5 @@
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
import { Stack, Title, Loader, Center, Group, Button, Badge, ActionIcon } from "@mantine/core"; import { Stack, Title, Loader, Center, Group, Button, Badge, ActionIcon, Switch } from "@mantine/core";
import { IconRefresh } from "@tabler/icons-react"; import { IconRefresh } from "@tabler/icons-react";
import { apiRequest } from "../config/request"; import { apiRequest } from "../config/request";
import { EXTERNAL_API } from "../config/api"; import { EXTERNAL_API } from "../config/api";
@ -17,18 +17,20 @@ export default function Alerts() {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const pageSize = 10; const pageSize = 10;
const [sortConfig, setSortConfig] = useState({ key: "startsAt", direction: "desc" }); const [sortConfig, setSortConfig] = useState({ key: "startsAt", direction: "desc" });
const [autoRefresh, setAutoRefresh] = useState(true); //
async function fetchAlerts() { async function fetchAlerts() {
setLoading(true); setLoading(true);
const data = await apiRequest(EXTERNAL_API.ALERTS_INFOS); const data = await apiRequest(EXTERNAL_API.ALERTS_INFOS);
if (data && Array.isArray(data)) { if (data && Array.isArray(data)) {
setAlerts(data); setAlerts(data);
const counts = { critical: 0, warning: 0, info: 0 }; const counts = { critical: 0, warning: 0, info: 0, total: 0 };
data.forEach((alert) => { data.forEach((alert) => {
const sev = alert.labels?.severity || "info"; const sev = alert.labels?.severity || "info";
if (sev === "critical") counts.critical++; if (sev === "critical") counts.critical++;
else if (sev === "warning") counts.warning++; else if (sev === "warning") counts.warning++;
else counts.info++; else counts.info++;
counts.total++;
}); });
setStats(counts); setStats(counts);
} }
@ -37,9 +39,14 @@ export default function Alerts() {
useEffect(() => { useEffect(() => {
fetchAlerts(); fetchAlerts();
const timer = setInterval(fetchAlerts, 30000);
let timer;
if (autoRefresh) {
timer = setInterval(fetchAlerts, 30000);
}
return () => clearInterval(timer); return () => clearInterval(timer);
}, []); }, [autoRefresh]);
// //
const nodeOptions = useMemo(() => { const nodeOptions = useMemo(() => {
@ -124,7 +131,14 @@ export default function Alerts() {
<Stack spacing="lg" p="md"> <Stack spacing="lg" p="md">
<Group position="apart"> <Group position="apart">
<Title order={2}>告警详情</Title> <Title order={2}>告警详情</Title>
<Button component="a" href="{EXTERNAL_HOST.ALERTS}" target="_blank" variant="outline"> <Switch
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.currentTarget.checked)}
label="自动刷新"
color="green"
size="sm"
/>
<Button component="a" href={EXTERNAL_HOST.ALERTS} target="_blank" variant="outline">
打开 Alertmanager 打开 Alertmanager
</Button> </Button>
<ActionIcon onClick={fetchAlerts} color="blue" variant="filled" size="lg" title="刷新"> <ActionIcon onClick={fetchAlerts} color="blue" variant="filled" size="lg" title="刷新">

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Grid, Text } from "@mantine/core"; import { Grid, Text, Stack, Title } from "@mantine/core";
import { NodeTable } from "../components/NodeTable"; import { NodeTable } from "../components/NodeTable";
import { HealthCard } from "../components/HealthCard"; import { HealthCard } from "../components/HealthCard";
import { AlertStats } from "../components/AlertStats"; import { AlertStats } from "../components/AlertStats";
@ -14,12 +14,13 @@ export default function Dashboard() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const countAlerts = (data) => { const countAlerts = (data) => {
const stats = { critical: 0, warning: 0, info: 0 }; const stats = { critical: 0, warning: 0, info: 0, total: 0 };
data?.forEach((alert) => { data?.forEach((alert) => {
const severity = alert.labels?.severity || "info"; const severity = alert.labels?.severity || "info";
if (severity === "critical") stats.critical++; if (severity === "critical") stats.critical++;
else if (severity === "warning") stats.warning++; else if (severity === "warning") stats.warning++;
else stats.info++; else stats.info++;
stats.total++;
}); });
return stats; return stats;
}; };
@ -41,7 +42,7 @@ export default function Dashboard() {
status_statistics: healthRes?.status_statistics || [], status_statistics: healthRes?.status_statistics || [],
}); });
setAlerts(countAlerts(alertsRes?.data || [])); setAlerts(countAlerts(alertsRes || []));
} catch (err) { } catch (err) {
console.error("获取 Dashboard 数据失败:", err); console.error("获取 Dashboard 数据失败:", err);
} finally { } finally {
@ -61,13 +62,16 @@ export default function Dashboard() {
} }
return ( return (
<Grid> <Stack spacing="lg" p="md">
<Title order={1} mb="md">仪表盘</Title>
<Grid>
<Grid.Col span={6}><HealthCard health={health} /></Grid.Col> <Grid.Col span={6}><HealthCard health={health} /></Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<AlertStats stats={alerts} layout="column" title="告警统计" link="/alerts" /> <AlertStats stats={alerts} layout="column" title="告警统计" link="/alerts" />
</Grid.Col> </Grid.Col>
<Grid.Col span={12}><NodeTable clusterData={cluster} title="集群节点" viewMoreLink="/nodeInfo" /></Grid.Col> <Grid.Col span={12}><NodeTable clusterData={cluster} title="集群节点" viewMoreLink="/nodeInfo" /></Grid.Col>
</Grid> </Grid>
</Stack>
); );
} }

View File

@ -5,7 +5,7 @@ import { metricsEntries } from "../config/entries";
export default function Metrics() { export default function Metrics() {
return ( return (
<Stack spacing="lg" p="md"> <Stack spacing="lg" p="md">
<Title order={2}>指标入口</Title> <Title order={2}>指标详情</Title>
<Grid gutter="lg"> <Grid gutter="lg">
{metricsEntries.map((entry) => ( {metricsEntries.map((entry) => (
<Grid.Col key={entry.href} span={{ base: 12, sm: 4, md: 3 }}> <Grid.Col key={entry.href} span={{ base: 12, sm: 4, md: 3 }}>

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Grid } from "@mantine/core"; import { Grid, Stack, Title } from "@mantine/core";
import { apiRequest } from "../config/request"; import { apiRequest } from "../config/request";
import { MASTER_API } from "../config/api"; import { MASTER_API } from "../config/api";
import { NodeTable } from "../components/NodeTable"; import { NodeTable } from "../components/NodeTable";
@ -23,24 +23,30 @@ export default function NodePage() {
}; };
return ( return (
<Grid gutter="lg"> <Stack spacing="lg" p="md">
{/* 左侧:节点表格 */} <Title order={2} mb="md">
<Grid.Col span={drawerOpen ? 8 : 12}> 节点信息
<NodeTable </Title>
withSearch
withPagination
withActions
fetchDetail={fetchNodeDetail}
/>
</Grid.Col>
{/* 节点详情 Drawer */} <Grid gutter="lg">
<NodeDetailDrawer {/* 左侧:节点表格 */}
opened={drawerOpen} <Grid.Col span={drawerOpen ? 8 : 12}>
onClose={() => setDrawerOpen(false)} <NodeTable
nodeId={selectedNodeId} withSearch
loading={detailLoading} withPagination
/> withActions
</Grid> fetchDetail={fetchNodeDetail}
/>
</Grid.Col>
{/* 节点详情 Drawer */}
<NodeDetailDrawer
opened={drawerOpen}
onClose={() => setDrawerOpen(false)}
nodeId={selectedNodeId}
loading={detailLoading}
/>
</Grid>
</Stack>
); );
} }

View File

@ -1 +0,0 @@
172.18.0.3

View File

@ -1,4 +1,3 @@
version: '3.8'
services: services:
web-frontend: web-frontend:
build: build:
@ -20,7 +19,7 @@ services:
- ${DATA_ROOT:-./data}/web:/private/argus/web/frontend - ${DATA_ROOT:-./data}/web:/private/argus/web/frontend
- ${DATA_ROOT:-./data}/etc:/private/argus/etc - ${DATA_ROOT:-./data}/etc:/private/argus/etc
networks: networks:
- argus-network - argus-debug-net
restart: unless-stopped restart: unless-stopped
logging: logging:
driver: "json-file" driver: "json-file"
@ -45,7 +44,7 @@ services:
volumes: volumes:
- ${DATA_ROOT:-./data}/etc:/private/argus/etc - ${DATA_ROOT:-./data}/etc:/private/argus/etc
networks: networks:
- argus-network - argus-debug-net
restart: unless-stopped restart: unless-stopped
logging: logging:
driver: "json-file" driver: "json-file"
@ -54,9 +53,8 @@ services:
max-file: "3" max-file: "3"
networks: networks:
argus-network: argus-debug-net:
driver: bridge external: true
name: argus-network
volumes: volumes:
web-frontend_data: web-frontend_data: