diff --git a/backend/controllers/collector.go b/backend/controllers/collector.go index ce60047..cd54745 100644 --- a/backend/controllers/collector.go +++ b/backend/controllers/collector.go @@ -28,7 +28,7 @@ func (ctrl *CollectorController) Create(c *gin.Context) { Name: req.Name, Type: req.Type, LokiDeploymentID: req.LokiDeploymentID, HostID: req.HostID, ClusterID: req.ClusterID, TargetDeployment: req.TargetDeployment, Namespace: req.Namespace, LogPath: req.LogPath, Labels: req.Labels, - ScrapeJobs: req.ScrapeJobs, + ScrapeJobs: req.ScrapeJobs, TargetNamespaces: req.TargetNamespaces, }) if err != nil { utils.Error(c, 400, err.Error()) diff --git a/backend/dto/collector.go b/backend/dto/collector.go index 444612a..00f74f2 100644 --- a/backend/dto/collector.go +++ b/backend/dto/collector.go @@ -14,4 +14,7 @@ type CreateCollectorRequest struct { Labels string `json:"labels"` // ScrapeJobs 主机模式的结构化采集项(可选;留空按 LogPath 生成 app + system 两个默认项) ScrapeJobs []models.ScrapeJob `json:"scrapeJobs"` + // TargetNamespaces 仅 k8s-daemonset 使用:只采集这些命名空间的 Pod 日志。 + // 空/不传 = 采集全集群(默认,保持旧行为)。 + TargetNamespaces []string `json:"targetNamespaces"` } diff --git a/backend/services/collector.go b/backend/services/collector.go index 3a9637f..3c3b82d 100644 --- a/backend/services/collector.go +++ b/backend/services/collector.go @@ -57,6 +57,7 @@ type CreateCollectorRequest struct { LogPath string Labels string ScrapeJobs []models.ScrapeJob + TargetNamespaces []string } // fillAssociations 填充关联的 Loki / Host / Cluster(用于配置生成与展示) @@ -286,6 +287,7 @@ func (s *CollectorService) Create(req *CreateCollectorRequest) (*models.Collecto LogPath: req.LogPath, Labels: req.Labels, ScrapeJobs: req.ScrapeJobs, + TargetNamespaces: req.TargetNamespaces, Status: "pending", CreatedAt: time.Now(), UpdatedAt: time.Now(), diff --git a/backend/services/promtail_daemonset_ns_test.go b/backend/services/promtail_daemonset_ns_test.go new file mode 100644 index 0000000..4b3316e --- /dev/null +++ b/backend/services/promtail_daemonset_ns_test.go @@ -0,0 +1,37 @@ +package services + +import ( + "strings" + "testing" +) + +// 回归:DaemonSet 模式的"只采集这些命名空间"必须真正渲染进 promtail 配置, +// 否则前端多选形同虚设(用户选了 astralx,实际还是全集群在采)。 +func TestDaemonsetTargetNamespacesFilter(t *testing.T) { + base := func(ns []string) string { + return PromtailDaemonsetYAML("log", "http://loki.log.svc:3100", "c1", "", ns, "", nil) + } + + // 1) 空 = 全集群:不应出现 namespaces 白名单 + all := base(nil) + if strings.Contains(all, "namespaces:") { + t.Error("targetNamespaces 为空时不应生成命名空间白名单(应为全集群采集)") + } + + // 2) 指定一个命名空间:必须出现白名单 + one := base([]string{"astralx"}) + if !strings.Contains(one, "namespaces:") { + t.Fatal("targetNamespaces=[astralx] 时没有生成 namespaces 白名单") + } + if !strings.Contains(one, "astralx") { + t.Error("白名单里没有 astralx") + } + + // 3) 多个命名空间都要在 + multi := base([]string{"astralx", "default"}) + for _, want := range []string{"astralx", "default"} { + if !strings.Contains(multi, want) { + t.Errorf("多选时白名单缺少 %q", want) + } + } +} diff --git a/frontend/src/i18n/locales/en/collectors.ts b/frontend/src/i18n/locales/en/collectors.ts index d4b6a32..7fa9c6c 100644 --- a/frontend/src/i18n/locales/en/collectors.ts +++ b/frontend/src/i18n/locales/en/collectors.ts @@ -81,6 +81,9 @@ const collectors = { namespaceHintSidecar: 'The namespace where the TARGET Deployment lives. The platform finds that Deployment there and injects a Fluent Bit sidecar; collected logs also carry this namespace label. It is NOT a log filter.', namespaceHintDaemonset: 'The namespace where the Promtail DaemonSet is deployed. The DaemonSet collects container logs from ALL namespaces in the cluster — it does not filter by namespace (usually just the Loki namespace, e.g. log).', namespaceHintHost: 'Not used in host mode.', + targetNamespaces: 'Only collect from these namespaces', + targetNamespacesHint: 'Leave empty to collect container stdout from ALL namespaces. Selecting specific ones cuts log volume significantly.', + targetNamespacesAll: 'All namespaces', nsPickCluster: 'Select a cluster first', nsLoading: 'Loading namespaces…', nsPick: 'Select a namespace', diff --git a/frontend/src/i18n/locales/zh/collectors.ts b/frontend/src/i18n/locales/zh/collectors.ts index 83b740b..672f207 100644 --- a/frontend/src/i18n/locales/zh/collectors.ts +++ b/frontend/src/i18n/locales/zh/collectors.ts @@ -85,6 +85,9 @@ const collectors = { namespaceHintSidecar: '填「目标应用(Deployment)所在的命名空间」。平台会在该命名空间里找到目标 Deployment 并注入 Fluent Bit sidecar,采集到的日志也会打上这个 namespace 标签。它不是日志过滤条件。', namespaceHintDaemonset: '填「Promtail DaemonSet 部署到哪个命名空间」。DaemonSet 会采集整个集群所有命名空间的容器日志,不按命名空间过滤(一般就用 Loki 所在的 log)。', namespaceHintHost: '主机模式不使用该字段。', + targetNamespaces: '只采集这些命名空间', + targetNamespacesHint: '不选 = 采集整个集群所有命名空间的容器标准输出。选了就只采这几个(能显著减少日志量)。', + targetNamespacesAll: '全集群', nsPickCluster: '先选择集群', nsLoading: '加载命名空间…', nsPick: '选择命名空间', diff --git a/frontend/src/pages/collectors/components/CollectorFormDialog.tsx b/frontend/src/pages/collectors/components/CollectorFormDialog.tsx index aa39460..0f2af58 100644 --- a/frontend/src/pages/collectors/components/CollectorFormDialog.tsx +++ b/frontend/src/pages/collectors/components/CollectorFormDialog.tsx @@ -63,6 +63,8 @@ function buildCollectorFormSchema(t: TFunction) { targetDeployment: z.string(), namespace: z.string(), logPath: z.string(), + /** DaemonSet 模式:只采集这些命名空间的 Pod 日志;空数组 = 全集群 */ + targetNamespaces: z.array(z.string()), /** 旧版表单即无录入入口,仅透传(默认空串) */ labels: z.string(), }) @@ -100,6 +102,7 @@ const DEFAULT_VALUES: CollectorFormValues = { targetDeployment: '', namespace: '', logPath: '', + targetNamespaces: [], labels: '', } @@ -145,6 +148,8 @@ export const CollectorFormDialog = memo(function CollectorFormDialog({ const clusterID = form.watch('clusterID') const namespace = form.watch('namespace') const isSidecarType = collectorType === 'k8s-fluentbit-sidecar' + const isDaemonsetType = collectorType === 'k8s-daemonset' + const selectedTargetNs = form.watch('targetNamespaces') // 打开时重置(关闭重置时机明确) useEffect(() => { @@ -156,7 +161,7 @@ export const CollectorFormDialog = memo(function CollectorFormDialog({ }, [open, form]) useEffect(() => { - if (!open || !isSidecarType || !clusterID) return + if (!open || (!isSidecarType && !isDaemonsetType) || !clusterID) return const controller = new AbortController() setNamespacesLoading(true) setNamespacesError('') @@ -208,7 +213,10 @@ export const CollectorFormDialog = memo(function CollectorFormDialog({ targetDeployment: values.type === 'k8s-fluentbit-sidecar' ? values.targetDeployment : undefined, namespace: values.type !== 'host-promtail' ? values.namespace : undefined, - logPath: values.logPath, + // DaemonSet 的日志路径由平台固定为 /var/log/pods//*.log,不需要用户填; + // 真要限制范围用 targetNamespaces(空 = 全集群) + logPath: values.type === 'k8s-daemonset' ? '' : values.logPath, + targetNamespaces: values.type === 'k8s-daemonset' ? values.targetNamespaces : undefined, labels: values.labels, }) onOpenChange(false) @@ -447,7 +455,48 @@ export const CollectorFormDialog = memo(function CollectorFormDialog({ )} - /*.log + (容器标准输出)。这里改成"只采集哪些命名空间",空 = 全集群。 */} + {isDaemonsetType && ( +
+

{t('form.targetNamespaces')}

+

{t('form.targetNamespacesHint')}

+
+ {namespacesLoading ? ( +

{t('form.nsLoading')}

+ ) : !clusterID ? ( +

{t('form.nsPickCluster')}

+ ) : namespaces.length === 0 ? ( +

{t('form.nsEmpty')}

+ ) : ( +
+ + {namespaces.map(ns => { + const active = selectedTargetNs.includes(ns) + return ( + + ) + })} +
+ )} +
+
+ )} + + {!isDaemonsetType && ( @@ -468,7 +517,7 @@ export const CollectorFormDialog = memo(function CollectorFormDialog({ {isSidecarType &&

{t('form.logPathSidecarHint')}

} )} - /> + />}