Add Prometheus and Grafana monitoring instructions
Build and Deploy Gateway / build-and-push (push) Successful in 13s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-08 01:28:53 +03:00
parent c26d4cb43b
commit 10888ac012
12 changed files with 906 additions and 9 deletions
+42 -1
View File
@@ -174,6 +174,22 @@ function InstructionStep({ step }) {
}
function InstructionBlock({ block, open, onToggle }) {
const [copyFeedback, setCopyFeedback] = useState(null);
const copyTimer = useRef(null);
useEffect(() => () => clearTimeout(copyTimer.current), []);
async function copyInstruction(action) {
clearTimeout(copyTimer.current);
try {
await copyText(action.text);
setCopyFeedback({ id: action.id, failed: false });
} catch {
setCopyFeedback({ id: action.id, failed: true });
}
copyTimer.current = setTimeout(() => setCopyFeedback(null), 800);
}
return (
<section
className={`client-instruction-block${open ? ' is-open' : ''}`}
@@ -202,7 +218,30 @@ function InstructionBlock({ block, open, onToggle }) {
))}
</ol>
)}
{block.code && <code>{block.code}</code>}
{block.code && (block.multilineCode
? <pre className="client-instruction-code"><code>{block.code}</code></pre>
: <code>{block.code}</code>)}
{block.copies && <div className="client-instruction-copies">
{block.copies.map((action) => {
const feedback = copyFeedback?.id === action.id ? copyFeedback : null;
return <div className="client-instruction-copy" key={action.id}>
<span>{action.label}</span>
<button
className={`client-copy-button client-instruction-copy-button${feedback ? feedback.failed ? ' is-copy-error' : ' is-copied' : ''}`}
type="button"
onClick={() => copyInstruction(action)}
>
<span className="client-copy-label">Скопировать</span>
{feedback && <span className="client-copy-feedback" aria-hidden="true">
{feedback.failed ? 'Ошибка' : 'Скопировано'}
</span>}
</button>
</div>;
})}
<span className="client-live-region" role="status" aria-live="polite">
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'Скопировано' : ''}
</span>
</div>}
{block.note && <p className="client-instruction-note">{block.note}</p>}
</div>
</div>
@@ -685,6 +724,7 @@ export function ClientOverviewPage({
const previousHasSubscriptionRef = useRef(hasSubscription);
confirmingDeleteRef.current = confirmingDelete;
const gatewayAddress = isGateway ? window.location.hostname : '127.0.0.1';
const controlHost = window.location.host || `${gatewayAddress}:3456`;
const proxyUrls = localProxyUrls(state?.proxyPort, gatewayAddress);
const usage = subscriptionUsage(state?.userInfo);
const duration = connectionDurationParts(state?.singboxStartedAt, now);
@@ -704,6 +744,7 @@ export function ClientOverviewPage({
isGateway,
host: gatewayAddress,
port: state?.proxyPort || (isGateway ? 8080 : 8082),
controlHost,
});
const [instructionsIntro, ...instructionGuides] = instructions;
const openInstruction = instructionGuides.find((block) => block.id === openInstructionId);
+25 -1
View File
@@ -1,4 +1,6 @@
export function instructionBlocks({ isGateway, host, port }) {
import { grafanaDashboardJson, prometheusScrapeConfig } from './prometheus.js';
export function instructionBlocks({ isGateway, host, port, controlHost }) {
const httpProxy = `http://${host}:${port}`;
const socksProxy = `socks5://${host}:${port}`;
@@ -82,6 +84,28 @@ export function instructionBlocks({ isGateway, host, port }) {
`Для отката верните в это поле локальный адрес самого роутера вместо ${host}.`,
],
note: 'Gateway и устройства должны находиться в одной локальной сети. Сначала проверьте настройку на одном устройстве вручную.',
}, {
id: 'prometheus',
label: 'Мониторинг',
title: 'Prometheus и Grafana',
summary: 'Готовые traffic metrics и dashboard для Gateway и отдельных устройств.',
paragraphs: [
`Prometheus забирает накопленные Harbor counters с http://${controlHost}/metrics. Ручка читает готовый snapshot и не запускает новый сбор трафика.`,
'Harbor обновляет traffic snapshot раз в 15 секунд, поэтому начальный scrape interval и refresh dashboard в 30 секунд не создают лишних одинаковых выборок.',
'Dashboard показывает общий объём и скорость, источники Gateway/Proxy, список устройств и выбранных клиентов по имени. В статистику входит только трафик, учтённый Harbor.',
],
steps: [
'Добавьте блок ниже в prometheus.yml и перезагрузите Prometheus.',
'В Grafana добавьте этот Prometheus как data source.',
'Скопируйте dashboard JSON, откройте Dashboards → New → Import и вставьте его.',
],
code: prometheusScrapeConfig(controlHost),
multilineCode: true,
copies: [
{ id: 'prometheus-config', label: 'prometheus.yml', text: prometheusScrapeConfig(controlHost) },
{ id: 'grafana-dashboard', label: 'Grafana dashboard', text: grafanaDashboardJson },
],
note: 'Имя устройства берётся из заданного вами названия, затем из hostname или IP. Переименование не сбрасывает traffic series: счётчик привязан к стабильному device ID.',
}] : []),
];
}
+13
View File
@@ -0,0 +1,13 @@
import dashboard from '../../monitoring/grafana/harbor-gateway.json' with { type: 'json' };
export const grafanaDashboardJson = JSON.stringify(dashboard, null, 2);
export function prometheusScrapeConfig(controlHost) {
return `scrape_configs:
- job_name: harbor_gateway
scrape_interval: 30s
scrape_timeout: 3s
metrics_path: /metrics
static_configs:
- targets: ["${controlHost}"]`;
}
+50
View File
@@ -2577,6 +2577,56 @@ p {
white-space: nowrap;
}
.client-instruction-code {
max-width: 100%;
margin: 0;
overflow-x: auto;
padding: 12px 14px;
border-radius: 12px;
background: color-mix(in oklch, var(--client-control) 84%, transparent);
}
.client-instruction-code code {
overflow: visible;
padding: 0;
border-radius: 0;
background: transparent;
line-height: 1.55;
white-space: pre;
}
.client-instruction-copies {
position: relative;
display: grid;
gap: 7px;
}
.client-instruction-copy {
min-height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.client-instruction-copy > span {
min-width: 0;
overflow: hidden;
color: var(--client-text);
font-size: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.client-instruction-copy-button {
width: 104px;
min-width: 104px;
min-height: 34px;
padding: 0;
color: var(--client-accent);
opacity: 1;
}
.client-instruction-body .client-instruction-note {
padding: 11px 13px;
border-radius: 12px;