feat(monitor): F6 日志告警引擎 — AlertEngine 支持 type=log 规则查询Loki/ES+触发/恢复 + 数据源认证升级(none/basic/bearer/custom) + 从查询创建告警真实API对接

This commit is contained in:
attacker committed 2026-06-28 04:00:38 +08:00
1 parent 894cb16a05
commit 6bba6144f3
17 files changed
+657 -74

No files matched your search

@@ -28,9 +28,11 @@ export function LogDatasourceDialog({
const [name, setName] = useState('');
const [url, setUrl] = useState('');
const [defaultIndex, setDefaultIndex] = useState('logstash-*');
const [basicAuth, setBasicAuth] = useState(false);
const [authType, setAuthType] = useState<LogDataSource['auth_type']>('none');
const [user, setUser] = useState('');
const [pass, setPass] = useState('');
const [token, setToken] = useState('');
const [headers, setHeaders] = useState<{ name: string; value: string }[]>([]);
const [skipTLS, setSkipTLS] = useState(false);
const [isDefault, setIsDefault] = useState(false);
const [saving, setSaving] = useState(false);
@@ -45,38 +47,52 @@ export function LogDatasourceDialog({
setName(editing.name);
setUrl(editing.url);
setDefaultIndex(editing.default_index || 'logstash-*');
setBasicAuth(!!editing.basic_auth);
// 兼容旧版:未设置 auth_type 但 basic_auth=true 时回退为 basic // Logan.Li
const initialAuth = editing.auth_type || (editing.basic_auth ? 'basic' : 'none');
setAuthType(initialAuth);
setUser(editing.basic_auth_user || '');
setPass('');
setToken(editing.token || '');
try {
setHeaders(editing.headers ? JSON.parse(editing.headers) : []);
} catch {
setHeaders([]);
}
setSkipTLS(!!editing.skip_tls_verify);
setIsDefault(!!editing.is_default);
// 编辑 ES 源时预加载索引列表
if (editing.type === 'elasticsearch' && editing.url) {
fetchIndices(editing.url, !!editing.basic_auth, editing.basic_auth_user || '', '');
fetchIndices(editing);
}
} else {
setName(engine === 'loki' ? 'Loki' : 'Elasticsearch');
const suffix = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
setName(engine === 'loki' ? `Loki-${suffix}` : `Elasticsearch-${suffix}`);
setUrl(engine === 'loki' ? 'http://localhost:3100' : 'http://localhost:9200');
setDefaultIndex('logstash-*');
setBasicAuth(false);
setAuthType('none');
setUser('');
setPass('');
setToken('');
setHeaders([]);
setSkipTLS(false);
setIsDefault(false);
setIndices([]);
}
}, [open, editing, engine]);
const fetchIndices = async (esUrl: string, auth: boolean, authUser: string, authPass: string) => {
if (!esUrl) return;
const fetchIndices = async (ds: LogDataSource) => {
if (!ds.url || ds.type !== 'elasticsearch') return;
setLoadingIndices(true);
try {
const result = await logCenterApi.listIndices({
url: esUrl,
basic_auth: auth ? 'true' : 'false',
basic_auth_user: authUser,
basic_auth_pass: authPass,
skip_tls_verify: skipTLS ? 'true' : 'false',
url: ds.url,
auth_type: ds.auth_type || 'none',
basic_auth: ds.auth_type === 'basic' ? 'true' : 'false',
basic_auth_user: ds.basic_auth_user || '',
basic_auth_pass: ds.basic_auth_pass || '',
token: ds.token || '',
headers: ds.headers || '',
skip_tls_verify: ds.skip_tls_verify ? 'true' : 'false',
});
setIndices(Array.isArray(result) ? result : []);
} catch {
@@ -91,9 +107,12 @@ export function LogDatasourceDialog({
name: name.trim(),
type: engine,
url: url.trim(),
basic_auth: basicAuth,
basic_auth_user: user,
basic_auth_pass: pass,
auth_type: authType,
basic_auth: authType === 'basic',
basic_auth_user: authType === 'basic' ? user : '',
basic_auth_pass: authType === 'basic' ? pass : '',
token: authType === 'bearer' ? token : '',
headers: authType === 'custom' ? JSON.stringify(headers.filter(h => h.name)) : '[]',
skip_tls_verify: skipTLS,
default_index: engine === 'elasticsearch' ? defaultIndex : undefined,
is_default: isDefault,
@@ -166,7 +185,21 @@ export function LogDatasourceDialog({
variant="ghost"
size="sm"
className="h-6 px-1.5 text-[10px] text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
onClick={() => fetchIndices(url, basicAuth, user, pass)}
onClick={() => fetchIndices({
id: editing?.id || 0,
name: name.trim(),
type: engine,
url: url.trim(),
auth_type: authType,
basic_auth: authType === 'basic',
basic_auth_user: user,
basic_auth_pass: pass,
token,
headers: JSON.stringify(headers.filter(h => h.name)),
skip_tls_verify: skipTLS,
default_index: defaultIndex,
is_default: isDefault,
} as LogDataSource)}
disabled={!url || loadingIndices}
>
{loadingIndices ? (
@@ -177,34 +210,84 @@ export function LogDatasourceDialog({
获取索引
</Button>
</div>
{indices.length > 0 ? (
<Select value={defaultIndex} onValueChange={setDefaultIndex}>
<SelectTrigger className="mt-1 h-9 w-full font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-h-48">
{indices.map((idx) => (
<SelectItem key={idx} value={idx} className="font-mono text-xs">
{idx}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input value={defaultIndex} onChange={e => setDefaultIndex(e.target.value)} placeholder="索引名或通配符,如 logstash-*" className="mt-1 h-9 font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
<Input value={defaultIndex} onChange={e => setDefaultIndex(e.target.value)} placeholder="索引名或通配符,如 .monitoring-es-7-*" className="mt-1 h-9 font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
{/* 已获取索引快捷选择(支持通配) // Logan.Li */}
{indices.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1 max-h-24 overflow-auto">
{indices.slice(0, 30).map((idx) => (
<button
key={idx}
type="button"
onClick={() => setDefaultIndex(idx)}
className="px-1.5 py-0.5 rounded text-[10px] font-mono border transition-colors hover:opacity-80"
style={{ background: 'var(--dg-bg-tertiary)', borderColor: 'var(--dg-border)', color: 'var(--dg-text-secondary)' }}
title={idx}
>
{idx.length > 28 ? `${idx.slice(0, 28)}...` : idx}
</button>
))}
</div>
)}
{/* 常见通配模式 // Logan.Li */}
<div className="mt-2 flex flex-wrap gap-1">
{['.monitoring-es-7-*', 'logstash-*', 'filebeat-*', 'metrics-*', '*-2026.06.*'].map((pattern) => (
<button
key={pattern}
type="button"
onClick={() => setDefaultIndex(pattern)}
className="px-1.5 py-0.5 rounded text-[10px] font-mono border transition-colors hover:opacity-80"
style={{ background: 'rgba(34,197,94,0.08)', borderColor: 'rgba(34,197,94,0.25)', color: '#22c55e' }}
>
{pattern}
</button>
))}
</div>
</div>
)}
<div className="flex items-center justify-between">
<Label className="text-xs text-[var(--dg-text-muted)]">基础认证</Label>
<Switch checked={basicAuth} onCheckedChange={setBasicAuth} />
<div>
<Label className="text-xs text-[var(--dg-text-muted)]">认证方式</Label>
<Select value={authType} onValueChange={(v) => setAuthType(v as LogDataSource['auth_type'])}>
<SelectTrigger className="mt-1 h-9 w-full bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<SelectItem value="none" className="text-xs">无认证</SelectItem>
<SelectItem value="basic" className="text-xs">Basic Auth</SelectItem>
<SelectItem value="bearer" className="text-xs">Bearer Token</SelectItem>
<SelectItem value="custom" className="text-xs">自定义 Headers</SelectItem>
</SelectContent>
</Select>
</div>
{basicAuth && (
{authType === 'basic' && (
<div className="grid grid-cols-2 gap-2">
<Input value={user} onChange={e => setUser(e.target.value)} placeholder="用户名" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
<Input type="password" value={pass} onChange={e => setPass(e.target.value)} placeholder="密码" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
</div>
)}
{authType === 'bearer' && (
<div>
<Input type="password" value={token} onChange={e => setToken(e.target.value)} placeholder="Bearer Token" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
</div>
)}
{authType === 'custom' && (
<div className="space-y-2">
{headers.map((h, i) => (
<div key={i} className="grid grid-cols-2 gap-2">
<Input value={h.name} onChange={e => {
const next = [...headers];
next[i].name = e.target.value;
setHeaders(next);
}} placeholder="Header 名,如 X-Scope-OrgID" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
<Input value={h.value} onChange={e => {
const next = [...headers];
next[i].value = e.target.value;
setHeaders(next);
}} placeholder="Header 值" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
</div>
))}
<Button variant="outline" size="sm" className="h-7 text-xs border-[var(--dg-border)]" onClick={() => setHeaders([...headers, { name: '', value: '' }])}>添加 Header</Button>
</div>
)}
<div className="flex items-center justify-between">
<Label className="text-xs text-[var(--dg-text-muted)]">跳过 TLS 校验</Label>
<Switch checked={skipTLS} onCheckedChange={setSkipTLS} />