feat(collector): 采集命名空间多选 + app 标签来源可自定义 + app 白名单
=== 一、采集命名空间改为多选(原来只有文本输入)===
- CollectorEditDialog 重写:勾选式多选面板,从
GET /clusters/:id/namespaces 拉取真实命名空间列表
- "所有命名空间(全集群采集)"一键全选(targetNamespaces 置空即全集群)
- 支持搜索过滤命名空间;已选数量实时显示
- 保存时为空数组 → Promtail 不加 namespaces 白名单
=== 二、app 标签取值来源可配置 ===
新增 Collector.AppLabelSource:
留空(默认)= app.kubernetes.io/name → app → controller 名(去hash) → pod 名 四级回退
可填任意 Pod 标签,如 app.kubernetes.io/name / app / k8s.kuboard.cn/name
也可直接写 __meta_kubernetes_pod_label_xxx
→ 作为最高优先级插到回退链最前,其余仍兜底
模板侧新增:
sanitizePromLabel() 标签名 → Prometheus meta label(. / 等转 _)
promtailAppSourceLabel() 解析用户配置
promtailAppIncludeFilter() 生成白名单 keep 规则
=== 三、app 白名单(控制采集量)===
新增 Collector.AppInclude []string:
填了之后生成
- source_labels: [app]
regex: ^(app1|app2)$
action: keep
只保留匹配的日志流 —— 这是收敛日志量最直接的手段
service.Update 同步支持 appLabelSource / appInclude(兼容 []string 与逗号分隔字符串)。
前端 CollectorEditDialog:
采集命名空间 = 勾选多选 + 全选
app 标签取值来源 = 输入框 + 5 个常用值 datalist 提示
只采集这些 app = 逗号分隔输入
镜像:lghost/lokistack:latest + v0.1 = sha256:43369d06cb28
This commit is contained in:
1 parent
e006fc1a0e
commit
41633f0b38
7 files changed
+271
-24
No files matched your search
@@ -16,6 +16,13 @@ type Collector struct {
|
|||||||
Cluster *Cluster `json:"cluster,omitempty"`
|
Cluster *Cluster `json:"cluster,omitempty"`
|
||||||
TargetDeployment string `json:"targetDeployment"` // sidecar 专用:目标 deployment 名
|
TargetDeployment string `json:"targetDeployment"` // sidecar 专用:目标 deployment 名
|
||||||
Namespace string `json:"namespace"` // 采集器自身部署所在的 k8s 命名空间
|
Namespace string `json:"namespace"` // 采集器自身部署所在的 k8s 命名空间
|
||||||
|
// AppLabelSource 决定 app 标签从哪里取值(可填私有标签,便于按自己的规范匹配)。
|
||||||
|
// 留空 = 默认:app.kubernetes.io/name → app → Pod 的 controller_name(去 hash) → pod 名
|
||||||
|
// 可填:controller_name / app.kubernetes.io/name / app / k8s.kuboard.cn/name 等任意 Pod 标签
|
||||||
|
AppLabelSource string `json:"appLabelSource"`
|
||||||
|
// AppInclude 只采集这些 app 的日志(白名单,按 AppLabelSource 解析出的 app 值匹配)。
|
||||||
|
// 留空 = 不限制。配合 TargetNamespaces 可显著收敛日志量。
|
||||||
|
AppInclude []string `json:"appInclude"`
|
||||||
// TargetNamespaces 只采集这些命名空间的 Pod 日志;为空表示采集全集群。
|
// TargetNamespaces 只采集这些命名空间的 Pod 日志;为空表示采集全集群。
|
||||||
// 对应 promtail 里 kubernetes_sd_configs.namespaces.names
|
// 对应 promtail 里 kubernetes_sd_configs.namespaces.names
|
||||||
TargetNamespaces []string `json:"targetNamespaces"`
|
TargetNamespaces []string `json:"targetNamespaces"`
|
||||||
|
|||||||
@@ -248,6 +248,43 @@ func (s *CollectorService) Update(id uint, updates map[string]interface{}) error
|
|||||||
}
|
}
|
||||||
db.Collectors[i].TargetNamespaces = nss
|
db.Collectors[i].TargetNamespaces = nss
|
||||||
}
|
}
|
||||||
|
// appLabelSource:app 标签取值来源(空=默认 controller_name 回退链)
|
||||||
|
if v, ok := updates["appLabelSource"].(string); ok {
|
||||||
|
if strings.TrimSpace(v) != db.Collectors[i].AppLabelSource {
|
||||||
|
db.Collectors[i].StatusMessage = "app 标签来源已保存,点击部署以应用新配置"
|
||||||
|
}
|
||||||
|
db.Collectors[i].AppLabelSource = strings.TrimSpace(v)
|
||||||
|
}
|
||||||
|
// appInclude:只采集这些 app(白名单,空=不限)
|
||||||
|
if v, ok := updates["appInclude"]; ok {
|
||||||
|
var apps []string
|
||||||
|
switch t := v.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
for _, item := range t {
|
||||||
|
if s, ok := item.(string); ok {
|
||||||
|
if s = strings.TrimSpace(s); s != "" {
|
||||||
|
apps = append(apps, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []string:
|
||||||
|
for _, s := range t {
|
||||||
|
if s = strings.TrimSpace(s); s != "" {
|
||||||
|
apps = append(apps, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
for _, s := range strings.Split(t, ",") {
|
||||||
|
if s = strings.TrimSpace(s); s != "" {
|
||||||
|
apps = append(apps, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(apps) != len(db.Collectors[i].AppInclude) {
|
||||||
|
db.Collectors[i].StatusMessage = "采集 app 白名单已保存,点击部署以应用新配置"
|
||||||
|
}
|
||||||
|
db.Collectors[i].AppInclude = apps
|
||||||
|
}
|
||||||
db.Collectors[i].UpdatedAt = time.Now()
|
db.Collectors[i].UpdatedAt = time.Now()
|
||||||
db.RequestSave()
|
db.RequestSave()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ func (d *CollectorDeployer) installDaemonset(ctx context.Context, endpoint strin
|
|||||||
for _, kind := range []string{"daemonset", "configmap", "serviceaccount"} {
|
for _, kind := range []string{"daemonset", "configmap", "serviceaccount"} {
|
||||||
_ = deleteK8sResource(ctx, cluster, kind, ns, "promtail")
|
_ = deleteK8sResource(ctx, cluster, kind, ns, "promtail")
|
||||||
}
|
}
|
||||||
manifest := PromtailDaemonsetYAML(ns, endpoint, clusterName, d.collector.Labels, d.collector.TargetNamespaces)
|
manifest := PromtailDaemonsetYAML(ns, endpoint, clusterName, d.collector.Labels,
|
||||||
|
d.collector.TargetNamespaces, d.collector.AppLabelSource, d.collector.AppInclude)
|
||||||
if err := d.applyYAML(ctx, nil, manifest, "promtail"); err != nil {
|
if err := d.applyYAML(ctx, nil, manifest, "promtail"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -196,6 +197,55 @@ extraVolumeMounts:
|
|||||||
// PromtailDaemonsetYAML DaemonSet 模式纯 YAML 安装(SA/RBAC/ConfigMap/DaemonSet),
|
// PromtailDaemonsetYAML DaemonSet 模式纯 YAML 安装(SA/RBAC/ConfigMap/DaemonSet),
|
||||||
// 取代 helm 安装 —— 后端无需安装 helm 二进制。
|
// 取代 helm 安装 —— 后端无需安装 helm 二进制。
|
||||||
// relabel 体系与主流 helm chart 对齐:namespace/pod/container/node_name + __path__ 按 pod uid 精确匹配。
|
// relabel 体系与主流 helm chart 对齐:namespace/pod/container/node_name + __path__ 按 pod uid 精确匹配。
|
||||||
|
// sanitizePromLabel 把 K8s 标签名转成 Prometheus/Promtail 的 meta label 片段。
|
||||||
|
//
|
||||||
|
// k8s.kuboard.cn/name → k8s_kuboard_cn_name
|
||||||
|
// app.kubernetes.io/name → app_kubernetes_io_name
|
||||||
|
func sanitizePromLabel(name string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range strings.TrimSpace(name) {
|
||||||
|
switch {
|
||||||
|
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_':
|
||||||
|
b.WriteRune(r)
|
||||||
|
default:
|
||||||
|
b.WriteRune('_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// promtailAppSourceLabel 把用户配置的 AppLabelSource 转成 Promtail 的 meta label 名。
|
||||||
|
// 返回空串表示「用默认的四级回退链」。
|
||||||
|
func promtailAppSourceLabel(src string) string {
|
||||||
|
src = strings.TrimSpace(src)
|
||||||
|
if src == "" || src == "controller_name" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(src, "label:") {
|
||||||
|
src = strings.TrimPrefix(src, "label:")
|
||||||
|
}
|
||||||
|
// 已经是 __meta_ 开头就直接用
|
||||||
|
if strings.HasPrefix(src, "__meta_kubernetes_") {
|
||||||
|
return src
|
||||||
|
}
|
||||||
|
return "__meta_kubernetes_pod_label_" + sanitizePromLabel(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
// promtailAppIncludeFilter 生成 app 白名单的 keep 规则(为空则不限)。
|
||||||
|
func promtailAppIncludeFilter(apps []string) string {
|
||||||
|
clean := make([]string, 0, len(apps))
|
||||||
|
for _, a := range apps {
|
||||||
|
if a = strings.TrimSpace(a); a != "" {
|
||||||
|
clean = append(clean, regexp.QuoteMeta(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(clean) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("\n - source_labels: [app]\n regex: ^(%s)$\n action: keep",
|
||||||
|
strings.Join(clean, "|"))
|
||||||
|
}
|
||||||
|
|
||||||
// promtailNamespaceFilter 生成 kubernetes_sd_configs 的命名空间白名单。
|
// promtailNamespaceFilter 生成 kubernetes_sd_configs 的命名空间白名单。
|
||||||
// namespaces 为空 → 返回空串(采集全集群所有命名空间)。
|
// namespaces 为空 → 返回空串(采集全集群所有命名空间)。
|
||||||
func promtailNamespaceFilter(namespaces []string) string {
|
func promtailNamespaceFilter(namespaces []string) string {
|
||||||
@@ -218,10 +268,16 @@ func promtailNamespaceFilter(namespaces []string) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func PromtailDaemonsetYAML(ns, lokiURL, clusterName, labelsJSON string, targetNamespaces []string) string {
|
func PromtailDaemonsetYAML(ns, lokiURL, clusterName, labelsJSON string, targetNamespaces []string, appLabelSource string, appInclude []string) string {
|
||||||
promtailImage := ResolveImageFor("promtail", "grafana/promtail", "3.4.0")
|
promtailImage := ResolveImageFor("promtail", "grafana/promtail", "3.4.0")
|
||||||
extLabels := promtailExternalLabels(clusterName, labelsJSON)
|
extLabels := promtailExternalLabels(clusterName, labelsJSON)
|
||||||
nsFilter := promtailNamespaceFilter(targetNamespaces)
|
nsFilter := promtailNamespaceFilter(targetNamespaces)
|
||||||
|
// 自定义 app 标签来源(用户可用自己注入的私有标签),插到回退链最前面
|
||||||
|
appSourceLine := ""
|
||||||
|
if lbl := promtailAppSourceLabel(appLabelSource); lbl != "" {
|
||||||
|
appSourceLine = " - " + lbl + "\n"
|
||||||
|
}
|
||||||
|
appIncludeFilter := promtailAppIncludeFilter(appInclude)
|
||||||
return fmt.Sprintf(`apiVersion: v1
|
return fmt.Sprintf(`apiVersion: v1
|
||||||
kind: ServiceAccount
|
kind: ServiceAccount
|
||||||
metadata:
|
metadata:
|
||||||
@@ -310,8 +366,10 @@ data:
|
|||||||
#
|
#
|
||||||
# 不使用 k8s.kuboard.cn/name 之类的平台私有标签:那只是 Kuboard 管理的集群才有,
|
# 不使用 k8s.kuboard.cn/name 之类的平台私有标签:那只是 Kuboard 管理的集群才有,
|
||||||
# 换 Rancher/Lens/Helm/kubectl 部署的集群就不存在,会破坏通用性。
|
# 换 Rancher/Lens/Helm/kubectl 部署的集群就不存在,会破坏通用性。
|
||||||
|
# 如集群确实有私有规范标签,可在「采集器 → 编辑」里配置 AppLabelSource,
|
||||||
|
# 它会作为最高优先级插到下面列表最前(其余仍兜底)。
|
||||||
- source_labels:
|
- source_labels:
|
||||||
- __meta_kubernetes_pod_label_app_kubernetes_io_name
|
%s - __meta_kubernetes_pod_label_app_kubernetes_io_name
|
||||||
- __meta_kubernetes_pod_label_app
|
- __meta_kubernetes_pod_label_app
|
||||||
- __tmp_controller_name
|
- __tmp_controller_name
|
||||||
- __meta_kubernetes_pod_name
|
- __meta_kubernetes_pod_name
|
||||||
@@ -350,7 +408,7 @@ data:
|
|||||||
replacement: /var/log/pods/*$1/*.log
|
replacement: /var/log/pods/*$1/*.log
|
||||||
separator: /
|
separator: /
|
||||||
source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]
|
source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]
|
||||||
target_label: __path__
|
target_label: __path__%s
|
||||||
---
|
---
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: DaemonSet
|
kind: DaemonSet
|
||||||
@@ -424,7 +482,7 @@ spec:
|
|||||||
- name: containers
|
- name: containers
|
||||||
hostPath:
|
hostPath:
|
||||||
path: /var/lib/docker/containers
|
path: /var/lib/docker/containers
|
||||||
`, ns, ns, ns, lokiURL, extLabels, nsFilter, ns, promtailImage)
|
`, ns, ns, ns, lokiURL, extLabels, nsFilter, appSourceLine, promtailImage, appIncludeFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FluentbitConfig Sidecar 模式 Fluent Bit 配置(采集 logPath,推送至 lokiHost:lokiPort)
|
// FluentbitConfig Sidecar 模式 Fluent Bit 配置(采集 logPath,推送至 lokiHost:lokiPort)
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ export const collectorApi = {
|
|||||||
|
|
||||||
create: (data: CreateCollectorRequest) => http.post<Collector>('/collectors', data),
|
create: (data: CreateCollectorRequest) => http.post<Collector>('/collectors', data),
|
||||||
|
|
||||||
update: (id: number, data: { name: string; logPath: string; targetNamespaces?: string[] }) =>
|
update: (id: number, data: {
|
||||||
|
name: string;
|
||||||
|
logPath: string;
|
||||||
|
targetNamespaces?: string[];
|
||||||
|
appLabelSource?: string;
|
||||||
|
appInclude?: string[];
|
||||||
|
}) =>
|
||||||
http.put<{ message: string }>(`/collectors/${id}`, data),
|
http.put<{ message: string }>(`/collectors/${id}`, data),
|
||||||
|
|
||||||
remove: (id: number) => http.delete<{ message: string }>(`/collectors/${id}`),
|
remove: (id: number) => http.delete<{ message: string }>(`/collectors/${id}`),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { memo, useEffect, useState } from 'react'
|
import { memo, useEffect, useMemo, useState } from 'react'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { collectorApi } from '@/api'
|
import { clusterApi, collectorApi } from '@/api'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
@@ -13,11 +13,24 @@ import type { Collector } from '@/types'
|
|||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().trim().min(1, '请输入采集策略名称'),
|
name: z.string().trim().min(1, '请输入采集策略名称'),
|
||||||
logPath: z.string().trim(),
|
logPath: z.string().trim(),
|
||||||
/** 逗号分隔的命名空间白名单;留空 = 采集全集群 */
|
/** 采集的命名空间白名单;空数组 = 全集群 */
|
||||||
targetNamespaces: z.string().trim(),
|
targetNamespaces: z.array(z.string()),
|
||||||
|
/** app 标签取值来源;留空 = 默认 controller_name 四级回退 */
|
||||||
|
appLabelSource: z.string().trim(),
|
||||||
|
/** 只采集这些 app(逗号分隔);留空 = 不限 */
|
||||||
|
appIncludeRaw: z.string().trim(),
|
||||||
})
|
})
|
||||||
type Values = z.infer<typeof schema>
|
type Values = z.infer<typeof schema>
|
||||||
|
|
||||||
|
/** app 标签来源的常用取值(可自由填写其它 Pod 标签名) */
|
||||||
|
const APP_SOURCE_PRESETS = [
|
||||||
|
{ value: '', label: '默认:app.kubernetes.io/name → app → controller 名 → pod 名' },
|
||||||
|
{ value: 'controller_name', label: 'controller_name(Pod 的 ReplicaSet 名,去 hash = Deployment 名)' },
|
||||||
|
{ value: 'app.kubernetes.io/name', label: 'app.kubernetes.io/name(K8s 标准标签)' },
|
||||||
|
{ value: 'app', label: 'app(常见简写)' },
|
||||||
|
{ value: 'k8s.kuboard.cn/name', label: 'k8s.kuboard.cn/name(Kuboard 注入)' },
|
||||||
|
]
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collector: Collector | null
|
collector: Collector | null
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
@@ -26,21 +39,63 @@ interface Props {
|
|||||||
|
|
||||||
export const CollectorEditDialog = memo(function CollectorEditDialog({ collector, onOpenChange, onSaved }: Props) {
|
export const CollectorEditDialog = memo(function CollectorEditDialog({ collector, onOpenChange, onSaved }: Props) {
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [namespaces, setNamespaces] = useState<string[]>([])
|
||||||
|
const [nsLoading, setNsLoading] = useState(false)
|
||||||
|
const [nsError, setNsError] = useState('')
|
||||||
|
const [nsFilter, setNsFilter] = useState('')
|
||||||
|
|
||||||
const form = useForm<Values>({
|
const form = useForm<Values>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: { name: '', logPath: '', targetNamespaces: '' },
|
defaultValues: { name: '', logPath: '', targetNamespaces: [], appLabelSource: '', appIncludeRaw: '' },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isDaemonSet = collector?.type === 'k8s-daemonset'
|
||||||
|
const supportsPath = !isDaemonSet
|
||||||
|
const selectedNs = form.watch('targetNamespaces')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (collector) {
|
if (!collector) return
|
||||||
form.reset({
|
form.reset({
|
||||||
name: collector.name,
|
name: collector.name,
|
||||||
logPath: collector.logPath || '',
|
logPath: collector.logPath || '',
|
||||||
targetNamespaces: (collector.targetNamespaces ?? []).join(', '),
|
targetNamespaces: collector.targetNamespaces ?? [],
|
||||||
|
appLabelSource: collector.appLabelSource ?? '',
|
||||||
|
appIncludeRaw: (collector.appInclude ?? []).join(', '),
|
||||||
})
|
})
|
||||||
}
|
setNsFilter('')
|
||||||
}, [collector, form])
|
}, [collector, form])
|
||||||
|
|
||||||
|
// 拉集群命名空间列表(供多选)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!collector || !isDaemonSet) return
|
||||||
|
const clusterID = collector.clusterID
|
||||||
|
if (!clusterID) return
|
||||||
|
let active = true
|
||||||
|
setNsLoading(true)
|
||||||
|
setNsError('')
|
||||||
|
clusterApi.namespaces(clusterID)
|
||||||
|
.then((res) => { if (active) setNamespaces(res?.namespaces ?? []) })
|
||||||
|
.catch((err) => { if (active) setNsError(err instanceof Error ? err.message : '加载命名空间失败') })
|
||||||
|
.finally(() => { if (active) setNsLoading(false) })
|
||||||
|
return () => { active = false }
|
||||||
|
}, [collector, isDaemonSet])
|
||||||
|
|
||||||
|
const filteredNs = useMemo(
|
||||||
|
() => namespaces.filter((n) => n.toLowerCase().includes(nsFilter.toLowerCase())),
|
||||||
|
[namespaces, nsFilter],
|
||||||
|
)
|
||||||
|
|
||||||
|
const allSelected = selectedNs.length === 0 // 空 = 全集群
|
||||||
|
|
||||||
|
function toggleNs(ns: string) {
|
||||||
|
const next = selectedNs.includes(ns) ? selectedNs.filter((n) => n !== ns) : [...selectedNs, ns]
|
||||||
|
form.setValue('targetNamespaces', next, { shouldDirty: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAll() {
|
||||||
|
form.setValue('targetNamespaces', [], { shouldDirty: true })
|
||||||
|
}
|
||||||
|
|
||||||
const submit = form.handleSubmit(async (values) => {
|
const submit = form.handleSubmit(async (values) => {
|
||||||
if (!collector) return
|
if (!collector) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -48,8 +103,9 @@ export const CollectorEditDialog = memo(function CollectorEditDialog({ collector
|
|||||||
await collectorApi.update(collector.id, {
|
await collectorApi.update(collector.id, {
|
||||||
name: values.name,
|
name: values.name,
|
||||||
logPath: values.logPath,
|
logPath: values.logPath,
|
||||||
// 逗号/空格分隔 → 字符串数组;留空数组表示采集全集群
|
targetNamespaces: values.targetNamespaces,
|
||||||
targetNamespaces: values.targetNamespaces
|
appLabelSource: values.appLabelSource,
|
||||||
|
appInclude: values.appIncludeRaw
|
||||||
.split(/[,\s]+/)
|
.split(/[,\s]+/)
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
@@ -63,27 +119,104 @@ export const CollectorEditDialog = memo(function CollectorEditDialog({ collector
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const supportsPath = collector?.type !== 'k8s-daemonset'
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={collector !== null} onOpenChange={onOpenChange}>
|
<Dialog open={collector !== null} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader><DialogTitle>编辑采集策略</DialogTitle></DialogHeader>
|
<DialogHeader><DialogTitle>编辑采集策略</DialogTitle></DialogHeader>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={submit} className="space-y-4">
|
<form onSubmit={submit} className="space-y-4">
|
||||||
<FormField control={form.control} name="name" render={({ field }) => (
|
<FormField control={form.control} name="name" render={({ field }) => (
|
||||||
<FormItem><FormLabel>名称</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
<FormItem><FormLabel>名称</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
||||||
)} />
|
)} />
|
||||||
{collector?.type === 'k8s-daemonset' && <FormField control={form.control} name="targetNamespaces" render={({ field }) => (
|
|
||||||
<FormItem>
|
{isDaemonSet && (
|
||||||
|
<>
|
||||||
|
{/* ── 采集命名空间:多选 ── */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<FormLabel>采集命名空间</FormLabel>
|
<FormLabel>采集命名空间</FormLabel>
|
||||||
<FormControl><Input placeholder="astralx, log(留空 = 全集群)" {...field} /></FormControl>
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{allSelected ? '全集群所有命名空间' : `已选 ${selectedNs.length} 个`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{nsError && <p className="text-xs text-destructive">{nsError}</p>}
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<label className="flex items-center gap-2 border-b px-3 py-2 text-sm font-medium cursor-pointer hover:bg-muted/50">
|
||||||
|
<input type="checkbox" checked={allSelected} onChange={setAll} />
|
||||||
|
所有命名空间(全集群采集)
|
||||||
|
</label>
|
||||||
|
<div className="p-2">
|
||||||
|
<Input
|
||||||
|
className="mb-2 h-8"
|
||||||
|
placeholder="搜索命名空间…"
|
||||||
|
value={nsFilter}
|
||||||
|
onChange={(e) => setNsFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="max-h-44 overflow-auto grid gap-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{nsLoading && <p className="p-2 text-xs text-muted-foreground">加载中…</p>}
|
||||||
|
{!nsLoading && filteredNs.length === 0 && (
|
||||||
|
<p className="p-2 text-xs text-muted-foreground">
|
||||||
|
{namespaces.length === 0 ? '未取到命名空间列表(可手动输入)' : '无匹配'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{filteredNs.map((ns) => (
|
||||||
|
<label key={ns} className="flex items-center gap-2 rounded px-2 py-1 text-sm hover:bg-muted/50 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
disabled={allSelected}
|
||||||
|
checked={selectedNs.includes(ns)}
|
||||||
|
onChange={() => toggleNs(ns)}
|
||||||
|
/>
|
||||||
|
<span className="truncate">{ns}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
勾「所有命名空间」= 采集全集群;否则只采集勾选的命名空间。
|
||||||
|
生成 Promtail 的 <code className="font-mono">kubernetes_sd_configs.namespaces</code> 白名单。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── app 标签来源 ── */}
|
||||||
|
<FormField control={form.control} name="appLabelSource" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>app 标签取值来源</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input list="app-source-presets" placeholder="留空 = 默认(controller 名回退链)" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<datalist id="app-source-presets">
|
||||||
|
{APP_SOURCE_PRESETS.filter((p) => p.value).map((p) => (
|
||||||
|
<option key={p.value} value={p.value}>{p.label}</option>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
多个命名空间用逗号分隔;留空则采集全集群所有命名空间。保存后点击「部署」应用新配置,
|
默认按 <code className="font-mono">app.kubernetes.io/name → app → controller 名 → pod 名</code> 四级回退。
|
||||||
Promtail 会按此生成 kubernetes_sd_configs.namespaces 白名单。
|
若集群有自己的私有规范标签(如 <code className="font-mono">k8s.kuboard.cn/name</code>),
|
||||||
|
填在这里会作为最高优先级,其余仍兜底。
|
||||||
</p>
|
</p>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)} />}
|
)} />
|
||||||
|
|
||||||
|
{/* ── app 白名单 ── */}
|
||||||
|
<FormField control={form.control} name="appIncludeRaw" render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>只采集这些 app(可选)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="留空 = 不限;多个用逗号分隔,如 cex-spot-api, cex-asset-api" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
填了之后 Promtail 只保留 app 命中这些值的日志(<code className="font-mono">action: keep</code>),
|
||||||
|
是控制日志量最直接的手段。值需与上面「app 标签取值来源」解析出的 app 一致。
|
||||||
|
</p>
|
||||||
|
</FormItem>
|
||||||
|
)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{supportsPath && <FormField control={form.control} name="logPath" render={({ field }) => (
|
{supportsPath && <FormField control={form.control} name="logPath" render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>采集日志路径</FormLabel>
|
<FormLabel>采集日志路径</FormLabel>
|
||||||
@@ -92,6 +225,7 @@ export const CollectorEditDialog = memo(function CollectorEditDialog({ collector
|
|||||||
<p className="text-xs text-muted-foreground">Sidecar 需使用业务容器的共享日志卷,或专用 logs 目录。保存后点击「部署」应用新配置。</p>
|
<p className="text-xs text-muted-foreground">Sidecar 需使用业务容器的共享日志卷,或专用 logs 目录。保存后点击「部署」应用新配置。</p>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)} />}
|
)} />}
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
||||||
<Button type="submit" disabled={saving}>{saving ? '保存中…' : '保存'}</Button>
|
<Button type="submit" disabled={saving}>{saving ? '保存中…' : '保存'}</Button>
|
||||||
|
|||||||
@@ -199,6 +199,10 @@ export interface Collector {
|
|||||||
namespace: string;
|
namespace: string;
|
||||||
/** 只采集这些命名空间;为空表示采集全集群 */
|
/** 只采集这些命名空间;为空表示采集全集群 */
|
||||||
targetNamespaces?: string[];
|
targetNamespaces?: string[];
|
||||||
|
/** app 标签取值来源;留空=默认 controller_name 四级回退 */
|
||||||
|
appLabelSource?: string;
|
||||||
|
/** 只采集这些 app 的日志(白名单);留空=不限 */
|
||||||
|
appInclude?: string[];
|
||||||
logPath: string;
|
logPath: string;
|
||||||
labels: string;
|
labels: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
Reference in new issue
Block a user