feat(ops): upload node logs and task traffic [CI]
This commit is contained in:
1 parent
e82b165eb0
commit
6cf6b23585
25 files changed
+714
-13
No files matched your search
@@ -0,0 +1,46 @@
|
||||
import type { NodeRuntimeLogsDto } from '@shared/api/types'
|
||||
import { fmtTime } from '@shared/format'
|
||||
import { useQuery } from '@shared/hooks'
|
||||
import { Chip, Empty, ErrorNotice, Spinner } from '@shared/ui'
|
||||
import { api } from '@shared/api/client'
|
||||
|
||||
export function RuntimeLogs({ nodeId }: { nodeId: string }) {
|
||||
const logs = useQuery(
|
||||
() => api.get<NodeRuntimeLogsDto>(`/api/admin/nodes/${encodeURIComponent(nodeId)}/logs`, { limit: 100 }),
|
||||
[nodeId],
|
||||
5000,
|
||||
)
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card-title row">
|
||||
运行日志
|
||||
<span className="right muted">最近 {logs.data?.items.length ?? 0} 条</span>
|
||||
</div>
|
||||
<ErrorNotice error={logs.error} />
|
||||
{logs.data?.dropped ? <p className="faint">节点或后端缓冲区累计丢弃 {logs.data.dropped} 条较早日志。</p> : null}
|
||||
{logs.loading && !logs.data ? <Spinner /> : null}
|
||||
{!logs.loading && (logs.data?.items.length ?? 0) === 0 ? <Empty>还没有收到运行日志</Empty> : null}
|
||||
{(logs.data?.items.length ?? 0) > 0 ? (
|
||||
<div className="runtime-logs">
|
||||
{logs.data?.items.map((entry) => (
|
||||
<div className="runtime-log" key={entry.id}>
|
||||
<time className="muted num">{fmtTime(entry.at)}</time>
|
||||
<Chip tone={entry.level === 'error' ? 'bad' : entry.level === 'warn' ? 'warn' : ''}>{entry.level}</Chip>
|
||||
<div className="runtime-log-message">
|
||||
<div>{entry.message}</div>
|
||||
{Object.keys(entry.fields).length ? (
|
||||
<div className="runtime-log-fields">
|
||||
{Object.entries(entry.fields).map(([key, value]) => (
|
||||
<span key={key}><b>{key}</b>={value}</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,54 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.runtime-logs {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
border-radius: var(--r-card);
|
||||
background: var(--app-outline-soft);
|
||||
}
|
||||
|
||||
.runtime-log {
|
||||
display: grid;
|
||||
grid-template-columns: 150px auto minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
padding: 9px 10px;
|
||||
background: var(--app-card);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.runtime-log-message {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.runtime-log-fields {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 10px;
|
||||
margin-top: 4px;
|
||||
color: var(--app-ink-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.runtime-log-fields b {
|
||||
color: var(--app-ink-faint);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.runtime-log {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.runtime-log-message {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.side .brand {
|
||||
padding: 8px 14px 16px;
|
||||
font-size: 18px;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useQuery } from '@shared/hooks'
|
||||
import { Chip, Confirm, Dialog, Empty, ErrorNotice, Field, Spinner, useAction } from '@shared/ui'
|
||||
import { AgentStatus } from './Agents'
|
||||
import { IPV6_STATES } from '@shared/ipVersions'
|
||||
import { RuntimeLogs } from '../RuntimeLogs'
|
||||
|
||||
function EditDialog({ agent, onClose, onSaved }: { agent: AgentDto; onClose: () => void; onSaved: () => void }) {
|
||||
const { busy, run } = useAction()
|
||||
@@ -358,6 +359,7 @@ export function AgentDetail() {
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
<RuntimeLogs nodeId={a.id} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useQuery } from '@shared/hooks'
|
||||
import { NodeTag, RuntimeStatusChip, useNodeInfos } from '@shared/nodes'
|
||||
import { IPV6_STATES } from '@shared/ipVersions'
|
||||
import { Chip, Dot, Empty, ErrorNotice, Spinner } from '@shared/ui'
|
||||
import { RuntimeLogs } from '../RuntimeLogs'
|
||||
|
||||
export function ProbeDetail() {
|
||||
const { id = '' } = useParams()
|
||||
@@ -111,6 +112,7 @@ export function ProbeDetail() {
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
<RuntimeLogs nodeId={p.id} />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -50,9 +50,13 @@ export function Host() {
|
||||
<section className="hero">
|
||||
<h1>{node?.name ?? id}</h1>
|
||||
<div className="row wrap" style={{ marginTop: 12 }}>
|
||||
{info?.country ? <Flag code={info.country} /> : null}
|
||||
{info?.isp_key ? <IspIcon ispKey={info.isp_key} name={info.isp_name} /> : null}
|
||||
{info?.os_name ? <span className="row"><OsIcon os={info.os} name={info.os_name} />{[info.os_name, info.arch].filter(Boolean).join(' / ')}</span> : null}
|
||||
{info?.country || info?.isp_key || info?.os_name ? (
|
||||
<span className="node-tag">
|
||||
{info.country ? <Flag code={info.country} /> : null}
|
||||
{info.isp_key ? <IspIcon ispKey={info.isp_key} name={info.isp_name} /> : null}
|
||||
{info.os_name ? <span className="plat"><OsIcon os={info.os} name={info.os_name} />{[info.os_name, info.arch].filter(Boolean).join(' / ')}</span> : null}
|
||||
</span>
|
||||
) : null}
|
||||
{node ? <><Chip>{node.kind === 'agent' ? 'Agent' : 'Probe'}</Chip><Dot tone={node.online ? 'ok' : 'bad'} /><span className="muted">{node.online ? (node.stale ? '状态数据暂未更新' : '在线') : '离线'}</span></> : <span className="muted">节点信息加载中…</span>}
|
||||
{node?.last_seen_at && !node.online ? <span className="faint">最后在线 {fmtAgo(node.last_seen_at)}</span> : null}
|
||||
<span className={`live-dot ${live ? 'on' : ''}`} title={live ? '实时更新中' : '实时连接中断,正在重连'} />
|
||||
|
||||
@@ -501,6 +501,23 @@ export interface paths {
|
||||
patch: operations["agents_update"];
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/nodes/{id}/logs": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Recent Agent or Probe runtime logs */
|
||||
get: operations["node_runtime_logs"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/admin/agents/{id}/approve": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1830,6 +1847,22 @@ export interface components {
|
||||
passkey: null | string;
|
||||
account: null | string;
|
||||
};
|
||||
NodeRuntimeLogDto: {
|
||||
/** Format: int64 */
|
||||
id: number | string;
|
||||
/** Format: date-time */
|
||||
at: unknown;
|
||||
level: string;
|
||||
message: string;
|
||||
fields: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
NodeRuntimeLogsDto: {
|
||||
items: components["schemas"]["NodeRuntimeLogDto"][];
|
||||
/** Format: int64 */
|
||||
dropped: number | string;
|
||||
};
|
||||
/** @enum {unknown} */
|
||||
NodeRuntimeStatus: "initializing" | "running" | "probe_paused" | "maintenance" | "stopping" | "offline" | "updating";
|
||||
NodeUpdateDto: {
|
||||
@@ -3843,6 +3876,48 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
node_runtime_logs: {
|
||||
parameters: {
|
||||
query?: {
|
||||
limit?: number | string;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["NodeRuntimeLogsDto"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["ProblemDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
agents_approve: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -7,6 +7,7 @@ type NarrowValue<V> = unknown extends V ? string : number extends V ? Exclude<V,
|
||||
type Narrow<T> = { [K in keyof T]: NarrowValue<T[K]> }
|
||||
|
||||
export type AgentDto = Narrow<S['AgentDto']>
|
||||
export type NodeRuntimeLogsDto = Narrow<S['NodeRuntimeLogsDto']>
|
||||
export type NodeRuntimeStatus = S['NodeRuntimeStatus']
|
||||
export type AgentIpv6Dto = Narrow<S['AgentIpv6Dto']>
|
||||
export type ConnectedProbeDto = Narrow<S['ConnectedProbeDto']>
|
||||
|
||||
Reference in new issue
Block a user