diff --git a/backend/services/collector.go b/backend/services/collector.go index 91e1ef6..d641413 100644 --- a/backend/services/collector.go +++ b/backend/services/collector.go @@ -240,6 +240,8 @@ func (s *CollectorService) PreviewConfig(id uint, updates map[string]interface{} } func (s *CollectorService) Create(req *CreateCollectorRequest) (*models.Collector, error) { + // 采集器变更可能带来新的 app 标签(主机模式),下拉候选值必须失效 + defer InvalidateLabelCache() switch req.Type { case "host-promtail", "k8s-daemonset", "k8s-fluentbit-sidecar": default: @@ -358,6 +360,7 @@ func (s *CollectorService) List(page, pageSize int) ([]models.Collector, int64, } func (s *CollectorService) Update(id uint, updates map[string]interface{}) error { + defer InvalidateLabelCache() db := repository.GetDB() db.Lock() defer db.Unlock() diff --git a/backend/services/deployment.go b/backend/services/deployment.go index 2aac692..187b707 100644 --- a/backend/services/deployment.go +++ b/backend/services/deployment.go @@ -319,6 +319,8 @@ func (s *DeploymentService) Update(id uint, updates map[string]interface{}) erro } func (s *DeploymentService) Delete(id uint) error { + // 部署删除会连带清理数据源,候选值失效 + defer InvalidateLabelCache() deployment, err := s.Get(id) if err != nil { return err diff --git a/backend/services/log_query.go b/backend/services/log_query.go index a0285b2..ee371ec 100644 --- a/backend/services/log_query.go +++ b/backend/services/log_query.go @@ -376,7 +376,9 @@ const labelValuesDefaultWindow = time.Hour // 而值本身的时效性要求很低(新上线的 app 晚 1 分钟出现完全可接受)。 // 缓存后重复请求从 25~40ms 降到微秒级,体感是"打开就有"。 const ( - labelCacheTTL = 60 * time.Second + // 30 秒:新上线的 app / 新副本要尽快出现在下拉里, + // 但也不必每点一次都打 Loki(重复请求间隔通常远小于 30 秒)。 + labelCacheTTL = 30 * time.Second labelCacheMax = 500 ) @@ -431,6 +433,18 @@ func labelCachePut(key string, values []string) { labelCache[key] = labelCacheEntry{values: values, expiresAt: time.Now().Add(labelCacheTTL)} } +// InvalidateLabelCache 清空 label 候选值缓存。 +// +// 什么时候必须调:平台侧发生了"会改变候选值集合"的变更 —— +// 采集器新增/修改/卸载(主机模式下 app 标签来自采集器配置)、 +// 部署删除、数据源增删改。这些场景下等 30 秒 TTL 自然过期会让用户 +// 觉得"刚配好的 app 在下拉里找不到"。 +func InvalidateLabelCache() { + labelCacheMu.Lock() + labelCache = map[string]labelCacheEntry{} + labelCacheMu.Unlock() +} + func LokiLabelValues(ctx context.Context, src *models.LogSource, name, selector string, start, end int64) ([]string, error) { // 缓存按调用方传入的原始 start/end 计算 key(下方默认窗口不影响 key 的稳定性) cacheKey := labelCacheKey(src, name, selector, start, end) diff --git a/backend/services/log_source.go b/backend/services/log_source.go index 653c56e..fd8ae33 100644 --- a/backend/services/log_source.go +++ b/backend/services/log_source.go @@ -149,6 +149,7 @@ func AuthorizedLogQuery(src *models.LogSource, userID uint, admin bool, query st } func CreateLogSource(s *models.LogSource) (*models.LogSource, error) { + defer InvalidateLabelCache() if err := encryptSourceCredentials(s); err != nil { return nil, err } @@ -212,6 +213,7 @@ func validSourceHeader(name string) bool { } func UpdateLogSource(id uint, patch map[string]interface{}) error { + defer InvalidateLabelCache() db := repository.GetDB() db.Lock() defer db.Unlock() @@ -327,6 +329,7 @@ func ValidateSourceAccess(access []models.LogSourceAccess) error { } func DeleteLogSource(id uint) error { + defer InvalidateLabelCache() db := repository.GetDB() db.Lock() defer db.Unlock() diff --git a/frontend/src/api/logs/index.ts b/frontend/src/api/logs/index.ts index fcb5110..a6989cd 100644 --- a/frontend/src/api/logs/index.ts +++ b/frontend/src/api/logs/index.ts @@ -34,12 +34,18 @@ export interface DailyVolumePoint { * * 为什么前端也要缓存:快捷 LogQL 的下拉在「切数据源 / 切命名空间 / 重开面板」 * 时都会重新走一遍请求链,后端虽然只要 25~40ms,但串起来仍是三四次往返, - * 用户看到的就是转圈。缓存后同一组合在 60 秒内直接命中内存,体感是"打开就有"。 + * 用户看到的就是转圈。缓存后同一组合在 30 秒内直接命中内存,体感是"打开就有"。 + * + * 时效性说明:新增 app / 新副本最长 30 秒后出现在下拉里。平台侧的变更 + * (采集器增删改、部署删除、数据源增删改)会由后端立即清缓存, + * 所以"刚配好的 app 找不到"这类问题不会被缓存放大。 * * 用 Promise 作为缓存值:并发的相同请求共享同一个飞行中的请求(去重), * 失败时立刻从缓存剔除,避免把错误结果缓存住。 */ -const LABEL_CACHE_TTL = 60_000 +// 与后端 TTL 对齐(30 秒):新上线的 app 要在半分钟内可被选中, +// 同时又能挡住"切命名空间/重开面板"造成的重复请求。 +const LABEL_CACHE_TTL = 30_000 const labelValueCache = new Map }>() function cachedRequest(key: string, run: () => Promise): Promise {