Files
LokiStack/backend/services/deployment_config_test.go
opsx a32f93f533 feat(deployments,analysis): 保留弹框重设计 + 分析页命名空间筛选 + 过期 chunk 真正删除
=== 一、保留策略弹框重设计(RetentionDialog)===
旧版问题:
1. 主输入是 YAML textarea,用户必须会写 `limits_config.retention_period: 30d`
2. 三个按钮:取消 / 仅保存 / 保存并应用 —— 「仅保存」只改部署记录不重载 Loki,
   今天已经造成一次真实故障:DB 记录 7 天、运行时 ConfigMap 是 24h,配置漂移
3. 「查看完整配置」是独立按钮 + 独立状态,多一次点击
4. 硬编码提示 `retention_delete_delay: 2h`,而实际已改成 10m,属于误导

新版:
- 主输入改为**保留天数数字框**(1–3650),YAML 由前端拼,用户不用写
- 快捷预设 1/7/14/30/90,实时显示「= 720 小时」
- >7 天给出存储成本提示
- 完整运行 config 改成 `<details>` 折叠,展开即自动加载(去掉独立按钮)
- **按钮只剩 2 个:取消 / 保存并应用**
- 删掉硬编码的 compactor 提示块

=== 二、日志分析页加命名空间筛选 ===
根因:analysis 页写死 namespace: '',等价 `{namespace=~".+"}` —— 一次分析扫全集群
(实测约 370 GB/天),5 条 count_over_time 跑 30s 都出不来,页面永远转圈。

- 新增命名空间下拉(从 Loki label values 拉取,localStorage 记忆)
- 默认取业务命名空间 astralx(其次列表首个),不再默认全集群
- 「全部命名空间(较慢)」降级为显式选项
- 命名空间未就绪时先显示 loading,避免先发一次全集群查询
- 5 条查询改**分批(每批 2 条)**执行 + 429 自动重试一次

=== 三、后端并发/超时调整 ===
- 重查询并发上限 2 → 4,且拿不到令牌时**排队最多 10s**再拒绝
  (原来立即 429,导致分析页 5 条并发里有 3 条直接失败)
- maxLogQueryLimit 1000 → 5000,与前端下拉完全对齐(原来前端选 5000 会被静默截断)

=== 四、过期 chunk 真正删除(关键修复)===
实测发现:索引表已从 COS 删除,但 chunk 对象仍残留 21,354 个 / 24.8 GB,
最老的 45 小时前 —— 因为 Loki 默认 `retention_delete_delay` = **2h**,
标记后要等 2 小时才真正删 chunk。

- 配置显式设置 `compactor.retention_delete_delay: 10m`
- 已用 `loki -verify-config` 校验(config is valid)
- 删除前后对比:
    对象数 46,536 → 23,934;占用 53.61 GB → 27.25 GB
    超期对象 21,354 个 / 24.8 GB → **0 个**
    桶内最老对象 45.2h → 24.4h(= 保留期 1 天)
- deployment_config_test 同步调整:retention_delete_delay 从"禁止出现"移入"必须存在"

镜像:lghost/lokistack:latest + v0.1 = sha256:b4d98e016bca
2026-10-28 17:05:57 +00:00

146 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"context"
"encoding/base64"
"io"
"regexp"
"strings"
"testing"
"gopkg.in/yaml.v3"
"lokistack/models"
)
func TestRedactLokiYAML(t *testing.T) {
source := "storage_config:\n aws:\n bucketnames: loki\n access_key_id: ENC:private-access\n secret_access_key: private-secret\nlimits_config:\n retention_period: 720h\n"
got, err := redactLokiYAML(source)
if err != nil {
t.Fatal(err)
}
for _, secret := range []string{"private-access", "private-secret", "ENC:"} {
if strings.Contains(got, secret) {
t.Fatalf("sensitive value remains in config: %s", secret)
}
}
for _, want := range []string{"bucketnames: loki", "retention_period: 720h", "access_key_id: '********'"} {
if !strings.Contains(got, want) {
t.Fatalf("missing %q in sanitized config: %s", want, got)
}
}
}
func TestMinIOConfigUsesBucketNameNotPathPrefix(t *testing.T) {
d := NewLokiDeployer(&models.LokiDeployment{StorageConfig: &models.StorageConfig{
StorageType: "minio", Bucket: "logs-bucket", PathPrefix: "wrong-prefix", Endpoint: "http://minio:9000",
AccessKey: "key", SecretKey: "secret", Insecure: true,
}}, nil)
config := d.generateLokiConfig()
if count := strings.Count(config, "bucketnames: logs-bucket"); count != 2 {
t.Fatalf("bucket name should appear in storage and ruler config; got %d", count)
}
if strings.Contains(config, "wrong-prefix") || strings.Contains(config, "s3://") {
t.Fatalf("invalid S3 URL or path prefix in generated config")
}
}
func TestDeployRejectsUndecryptedStorageCredential(t *testing.T) {
d := NewLokiDeployer(&models.LokiDeployment{StorageConfig: &models.StorageConfig{
StorageType: "minio", AccessKey: "ENC:unavailable", SecretKey: "secret",
}}, nil)
if err := d.Deploy(context.Background()); err == nil || !strings.Contains(err.Error(), "存储凭证解密失败") {
t.Fatalf("expected credential failure, got %v", err)
}
}
func TestGeneratedLokiConfigKeepsRequiredSettingsWithoutDefaults(t *testing.T) {
d := NewLokiDeployer(&models.LokiDeployment{RetentionDays: 90, Replicas: 1, StorageConfig: &models.StorageConfig{
StorageType: "minio", Bucket: "logs", Endpoint: "http://minio:9000",
AccessKey: "key", SecretKey: "secret", Insecure: true,
}}, nil)
config := d.generateLokiConfig()
var parsed map[string]interface{}
if err := yaml.Unmarshal([]byte(config), &parsed); err != nil {
t.Fatalf("invalid Loki YAML: %v", err)
}
// 注意 delete_request_store 用的是 filesystem 而不是 s3:
// loki_config.go 里对 s3/oss/minio/cos/obs 刻意把 compactor 的删除请求存到
// 本地盘(见该处注释),避免各云 S3 兼容实现差异导致 compactor 启动失败。
// loki_object_store_test.go 断言的也是 filesystem —— 两处必须一致。
for _, required := range []string{"retention_period: 2160h", "retention_enabled: true", "delete_request_store: filesystem", "retention_delete_delay: 10m", "bucketnames: logs", "store: tsdb", "schema: v13"} {
if !strings.Contains(config, required) {
t.Fatalf("required setting %q missing", required)
}
}
// 注意:retention_delete_delay 已从"不该出现"清单移除 —— 现在**有意**配置为 10m。
// 默认 2h 会让对象存储里多留 2 小时数据(实测桶内超期对象一度占 24.6GB),
// 预发环境用 10m 既保留安全窗口又不白付存储费。
for _, defaultSetting := range []string{"http_listen_port:", "grpc_listen_port:", "chunk_idle_period:", "compaction_interval:", "max_entries_limit_per_query:", "frontend_worker:", "chunk_store_config:", "autoforget_unhealthy:"} {
if strings.Contains(config, defaultSetting) {
t.Fatalf("unnecessary setting %q remained", defaultSetting)
}
}
d.deployment.Replicas = 2
if !strings.Contains(d.generateLokiConfig(), "autoforget_unhealthy: true") {
t.Fatal("multi-replica ring recovery setting missing")
}
}
func TestK8sManifestKeepsStorageCredentialsInSecret(t *testing.T) {
d := NewLokiDeployer(&models.LokiDeployment{Mode: "k8s-yaml", RetentionDays: 60, Replicas: 1, StorageConfig: &models.StorageConfig{
StorageType: "minio", Bucket: "loki", Endpoint: "http://minio:9000",
AccessKey: "test-access", SecretKey: "test-secret", Insecure: true,
}}, nil)
manifest := d.generateManifest("log")
decoder := yaml.NewDecoder(strings.NewReader(manifest))
var configYAML string
var secret map[string]interface{}
for {
var doc map[string]interface{}
if err := decoder.Decode(&doc); err != nil {
if err == io.EOF {
break
}
t.Fatalf("invalid manifest YAML: %v", err)
}
switch doc["kind"] {
case "ConfigMap":
configYAML = doc["data"].(map[string]interface{})["loki.yaml"].(string)
case "Secret":
secret = doc["data"].(map[string]interface{})
}
}
if configYAML == "" || secret == nil {
t.Fatal("ConfigMap or Secret missing")
}
if strings.Contains(configYAML, "test-access") || strings.Contains(configYAML, "test-secret") {
t.Fatal("storage credential leaked into ConfigMap")
}
for _, marker := range []string{"${LOKI_STORAGE_ACCESS_KEY}", "${LOKI_STORAGE_SECRET_KEY}", "retention_period: 1440h", "retention_enabled: true"} {
if !strings.Contains(configYAML, marker) {
t.Fatalf("required Loki setting %q missing", marker)
}
}
if !strings.Contains(manifest, "-config.expand-env=true") || !strings.Contains(manifest, "secretKeyRef:") {
t.Fatal("StatefulSet does not reference storage Secret")
}
for key, want := range map[string]string{"accessKey": "test-access", "secretKey": "test-secret"} {
decoded, err := base64.StdEncoding.DecodeString(secret[key].(string))
if err != nil || string(decoded) != want {
t.Fatalf("Secret %s data mismatch", key)
}
}
}
func TestK8sManifestChangesPodTemplateWhenRetentionChanges(t *testing.T) {
deployment := &models.LokiDeployment{RetentionDays: 30, Replicas: 1}
d := NewLokiDeployer(deployment, nil)
checksum := regexp.MustCompile(`checksum/loki-config: "[a-f0-9]{64}"`)
first := checksum.FindString(d.generateManifest("log"))
deployment.RetentionDays = 70
second := checksum.FindString(d.generateManifest("log"))
if first == "" || second == "" || first == second {
t.Fatalf("Pod config checksum must change with retention: %q -> %q", first, second)
}
}