- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂 - i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏 - 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条 - 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器 - UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正 - 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简 - 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
396 lines
10 KiB
Go
396 lines
10 KiB
Go
// LokiDeployer — loki.yaml 配置生成:模板 + 对象存储/规则存储/ring 拓扑。
|
|
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"text/template"
|
|
|
|
"lokistack/pkg/crypto"
|
|
)
|
|
|
|
const lokiConfigTemplate = `auth_enabled: {{ .AuthEnabled }}
|
|
|
|
common:
|
|
path_prefix: /var/lib/loki
|
|
{{- if .FilesystemStorage }}
|
|
storage:
|
|
filesystem:
|
|
chunks_directory: /var/lib/loki/chunks
|
|
rules_directory: /var/lib/loki/rules
|
|
{{- end }}
|
|
replication_factor: {{ .ReplicationFactor }}
|
|
ring:
|
|
instance_addr: {{ .RingInstanceAddr }}
|
|
kvstore:
|
|
store: {{ .RingKVStore }}
|
|
|
|
{{- if .StorageConfigBlock }}
|
|
|
|
storage_config:
|
|
{{ .StorageConfigBlock }}
|
|
{{- end }}
|
|
{{- if .MemberlistBlock }}
|
|
|
|
{{ .MemberlistBlock }}
|
|
{{- end }}
|
|
|
|
ingester:
|
|
wal:
|
|
dir: /var/lib/loki/wal
|
|
flush_on_shutdown: true
|
|
replay_memory_ceiling: 1GB
|
|
lifecycler:
|
|
ring:
|
|
kvstore:
|
|
store: {{ .RingKVStore }}
|
|
replication_factor: {{ .ReplicationFactor }}
|
|
{{- if .MultiReplica }}
|
|
autoforget_unhealthy: true
|
|
{{- end }}
|
|
|
|
compactor:
|
|
working_directory: /var/lib/loki/compactor
|
|
retention_enabled: true
|
|
delete_request_store: {{ .CompactorStoreType }}
|
|
|
|
limits_config:
|
|
retention_period: {{ .RetentionPeriod }}h
|
|
|
|
schema_config:
|
|
configs:
|
|
- from: "2024-01-01"
|
|
store: tsdb
|
|
object_store: {{ .ObjectStore }}
|
|
schema: v13
|
|
index:
|
|
prefix: tsdb_index_
|
|
period: 24h
|
|
|
|
ruler:
|
|
storage:
|
|
type: {{ .RulerStoreType }}
|
|
{{- if .RulerStorageBlock }}
|
|
{{ .RulerStorageBlock }}
|
|
{{- end }}
|
|
{{- if eq .RulerStoreType "local" }}
|
|
local:
|
|
directory: /var/lib/loki/rules
|
|
{{- end }}
|
|
rule_path: /var/lib/loki/rules-temp
|
|
ring:
|
|
kvstore:
|
|
store: {{ .RingKVStore }}
|
|
|
|
analytics:
|
|
reporting_enabled: false
|
|
`
|
|
|
|
type lokiConfigData struct {
|
|
AuthEnabled bool
|
|
ReplicationFactor int
|
|
RetentionPeriod int
|
|
StorageConfigBlock string
|
|
ObjectStore string
|
|
RulerStoreType string
|
|
CompactorStoreType string
|
|
RingInstanceAddr string // 单副本 127.0.0.1;多副本 0.0.0.0(须可被对端寻址)
|
|
RingKVStore string // 单副本 inmemory;多副本 memberlist
|
|
MemberlistBlock string // 多副本时非空,注入 memberlist 顶级配置
|
|
RulerStorageBlock string // 对象存储时非空,注入 ruler.storage.s3/gcs/azure 嵌套配置
|
|
MultiReplica bool
|
|
FilesystemStorage bool
|
|
}
|
|
|
|
func (d *LokiDeployer) generateLokiConfig() string {
|
|
retentionHours := d.deployment.RetentionDays * 24
|
|
if retentionHours <= 0 {
|
|
retentionHours = DefaultRetentionDays * 24
|
|
}
|
|
|
|
storageType := d.effectiveStorageType()
|
|
storageBlock := d.buildStorageConfigBlock()
|
|
objectStore := "filesystem"
|
|
rulerStore := "local"
|
|
compactorStore := "filesystem"
|
|
|
|
switch storageType {
|
|
case "s3", "oss", "minio", "cos", "obs":
|
|
objectStore = "s3"
|
|
rulerStore = "s3"
|
|
// 删除请求存本地盘,避免各云 S3 兼容实现差异导致 compactor 启动失败
|
|
compactorStore = "filesystem"
|
|
case "gcs":
|
|
objectStore = "gcs"
|
|
rulerStore = "gcs"
|
|
compactorStore = "gcs"
|
|
case "azure":
|
|
objectStore = "azure"
|
|
rulerStore = "azure"
|
|
compactorStore = "azure"
|
|
}
|
|
|
|
// Ring 拓扑:单副本用 inmemory + 127.0.0.1(无需跨实例通信);
|
|
// 多副本必须切 memberlist + 0.0.0.0,否则 ring 无法收敛、写入失败。
|
|
ringAddr := "127.0.0.1"
|
|
ringStore := "inmemory"
|
|
memberlistBlock := ""
|
|
if d.deployment.Replicas >= 2 {
|
|
ringAddr = "0.0.0.0"
|
|
ringStore = "memberlist"
|
|
ns := d.deployment.Namespace
|
|
if ns == "" {
|
|
ns = "log"
|
|
}
|
|
memberlistBlock = fmt.Sprintf(`memberlist:
|
|
join_members:
|
|
- loki-headless.%s.svc.cluster.local
|
|
bind_port: 7946
|
|
dead_node_reclaim_time: 30s
|
|
gossip_to_dead_nodes_time: 2m
|
|
left_ingesters_timeout: 5m`, ns)
|
|
}
|
|
|
|
data := lokiConfigData{
|
|
AuthEnabled: d.deployment.AuthEnabled,
|
|
ReplicationFactor: d.replicationFactor(),
|
|
RetentionPeriod: retentionHours,
|
|
StorageConfigBlock: storageBlock,
|
|
ObjectStore: objectStore,
|
|
RulerStoreType: rulerStore,
|
|
CompactorStoreType: compactorStore,
|
|
RingInstanceAddr: ringAddr,
|
|
RingKVStore: ringStore,
|
|
MemberlistBlock: memberlistBlock,
|
|
RulerStorageBlock: d.buildRulerStorageBlock(),
|
|
MultiReplica: d.deployment.Replicas >= 2,
|
|
FilesystemStorage: objectStore == "filesystem",
|
|
}
|
|
|
|
tmpl, err := template.New("loki-config").Parse(lokiConfigTemplate)
|
|
if err != nil {
|
|
return d.generateLokiConfigFallback(retentionHours)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
return d.generateLokiConfigFallback(retentionHours)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func (d *LokiDeployer) buildStorageConfigBlock() string {
|
|
if d.deployment.StorageConfig == nil {
|
|
return ""
|
|
}
|
|
|
|
sc := d.deployment.StorageConfig
|
|
accessKey, secretKey := d.storageCredentialValues()
|
|
switch sc.StorageType {
|
|
case "s3":
|
|
endpoint := sc.Endpoint
|
|
if endpoint == "" {
|
|
endpoint = fmt.Sprintf("s3.%s.amazonaws.com", sc.Region)
|
|
}
|
|
insecure := "false"
|
|
if sc.Insecure {
|
|
insecure = "true"
|
|
}
|
|
return fmt.Sprintf(` aws:
|
|
bucketnames: %s
|
|
endpoint: %s
|
|
region: %s
|
|
access_key_id: %s
|
|
secret_access_key: %s
|
|
insecure: %s
|
|
s3forcepathstyle: true`, sc.Bucket, endpoint, sc.Region, accessKey, secretKey, insecure)
|
|
case "oss", "cos", "obs":
|
|
insecure := "false"
|
|
if sc.Insecure {
|
|
insecure = "true"
|
|
}
|
|
// 云厂商 S3 兼容端点有两种写法:
|
|
// 区域端点 cos.ap-hongkong.myqcloud.com ← 正确
|
|
// 虚拟主机式 mybucket-1250000000.cos.<region>... ← 桶名已在 host 里
|
|
// Loki 的 S3 客户端会自己把 bucketnames 拼到 endpoint 前面(s3forcepathstyle=false
|
|
// 即虚拟主机风格),所以 endpoint 必须是区域端点。若用户在平台里填了带桶名的写法,
|
|
// 这里剥掉桶名前缀,否则会拼出 <bucket>.<bucket>.cos... 导致 TLS 证书校验失败、
|
|
// 日志写入失败(ruler/ingester 持续报 x509 证书不匹配)。
|
|
endpoint := stripBucketPrefix(sc.Endpoint, sc.Bucket)
|
|
return fmt.Sprintf(` aws:
|
|
bucketnames: %s
|
|
endpoint: %s
|
|
region: %s
|
|
access_key_id: %s
|
|
secret_access_key: %s
|
|
insecure: %s
|
|
s3forcepathstyle: false`, sc.Bucket, endpoint, sc.Region, accessKey, secretKey, insecure)
|
|
case "minio":
|
|
insecure := "false"
|
|
if sc.Insecure {
|
|
insecure = "true"
|
|
}
|
|
return fmt.Sprintf(` aws:
|
|
bucketnames: %s
|
|
endpoint: %s
|
|
access_key_id: %s
|
|
secret_access_key: %s
|
|
insecure: %s
|
|
s3forcepathstyle: true`, sc.Bucket, stripBucketPrefix(sc.Endpoint, sc.Bucket), accessKey, secretKey, insecure)
|
|
case "gcs":
|
|
return fmt.Sprintf(` gcs:
|
|
bucket_name: %s`, sc.Bucket)
|
|
case "azure":
|
|
return fmt.Sprintf(` azure:
|
|
container_name: %s
|
|
account_name: %s
|
|
account_key: %s`, sc.Bucket, accessKey, secretKey)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// buildRulerStorageBlock 生成 ruler.storage 下与存储类型同名的嵌套配置
|
|
// (Loki 3.x 的 ruler 存储客户端不回退读取 storage_config,缺少此块会以空 bucket 启动失败)
|
|
func (d *LokiDeployer) buildRulerStorageBlock() string {
|
|
if d.deployment.StorageConfig == nil {
|
|
return ""
|
|
}
|
|
sc := d.deployment.StorageConfig
|
|
accessKey, secretKey := d.storageCredentialValues()
|
|
switch sc.StorageType {
|
|
case "s3":
|
|
endpoint := sc.Endpoint
|
|
if endpoint == "" {
|
|
endpoint = fmt.Sprintf("s3.%s.amazonaws.com", sc.Region)
|
|
}
|
|
insecure := "false"
|
|
if sc.Insecure {
|
|
insecure = "true"
|
|
}
|
|
return fmt.Sprintf(` s3:
|
|
bucketnames: %s
|
|
endpoint: %s
|
|
region: %s
|
|
access_key_id: %s
|
|
secret_access_key: %s
|
|
insecure: %s
|
|
s3forcepathstyle: true`, sc.Bucket, endpoint, sc.Region, accessKey, secretKey, insecure)
|
|
case "oss", "cos", "obs":
|
|
insecure := "false"
|
|
if sc.Insecure {
|
|
insecure = "true"
|
|
}
|
|
return fmt.Sprintf(` s3:
|
|
bucketnames: %s
|
|
endpoint: %s
|
|
region: %s
|
|
access_key_id: %s
|
|
secret_access_key: %s
|
|
insecure: %s
|
|
s3forcepathstyle: false`, sc.Bucket, sc.Endpoint, sc.Region, accessKey, secretKey, insecure)
|
|
case "minio":
|
|
insecure := "false"
|
|
if sc.Insecure {
|
|
insecure = "true"
|
|
}
|
|
return fmt.Sprintf(` s3:
|
|
bucketnames: %s
|
|
endpoint: %s
|
|
access_key_id: %s
|
|
secret_access_key: %s
|
|
insecure: %s
|
|
s3forcepathstyle: true`, sc.Bucket, sc.Endpoint, accessKey, secretKey, insecure)
|
|
case "gcs":
|
|
return fmt.Sprintf(` gcs:
|
|
bucket_name: %s`, sc.Bucket)
|
|
case "azure":
|
|
return fmt.Sprintf(` azure:
|
|
container_name: %s
|
|
account_name: %s
|
|
account_key: %s`, sc.Bucket, accessKey, secretKey)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (d *LokiDeployer) storageCredentialValues() (string, string) {
|
|
if d.deployment.StorageConfig == nil {
|
|
return "", ""
|
|
}
|
|
if d.deployment.Mode == "k8s-yaml" || d.deployment.Mode == "host-docker" {
|
|
return "${LOKI_STORAGE_ACCESS_KEY}", "${LOKI_STORAGE_SECRET_KEY}"
|
|
}
|
|
// host-docker 模式没有 Secret 注入,必须把库里 ENC: 密文解密后再写进配置
|
|
return crypto.DecryptField(d.deployment.StorageConfig.AccessKey),
|
|
crypto.DecryptField(d.deployment.StorageConfig.SecretKey)
|
|
}
|
|
|
|
// generateLokiConfigFallback 当模板解析失败时的降级配置
|
|
func (d *LokiDeployer) generateLokiConfigFallback(retentionHours int) string {
|
|
return fmt.Sprintf(`auth_enabled: %v
|
|
server:
|
|
http_listen_port: 3100
|
|
grpc_listen_port: 9095
|
|
http_server_read_timeout: 3m
|
|
http_server_write_timeout: 3m
|
|
log_level: info
|
|
common:
|
|
path_prefix: /var/lib/loki
|
|
storage:
|
|
filesystem:
|
|
chunks_directory: /var/lib/loki/chunks
|
|
rules_directory: /var/lib/loki/rules
|
|
replication_factor: %d
|
|
ring:
|
|
instance_addr: 127.0.0.1
|
|
kvstore:
|
|
store: inmemory
|
|
ingester:
|
|
wal:
|
|
enabled: true
|
|
dir: /var/lib/loki/wal
|
|
lifecycler:
|
|
ring:
|
|
kvstore:
|
|
store: inmemory
|
|
replication_factor: %d
|
|
final_sleep: 0s
|
|
chunk_idle_period: 30m
|
|
max_chunk_age: 2h
|
|
querier:
|
|
max_concurrent: 10
|
|
compactor:
|
|
working_directory: /var/lib/loki/compactor
|
|
retention_enabled: true
|
|
retention_delete_delay: 2h
|
|
limits_config:
|
|
retention_period: %dh
|
|
ingestion_rate_mb: 64
|
|
ingestion_burst_size_mb: 128
|
|
schema_config:
|
|
configs:
|
|
- from: "2024-01-01"
|
|
store: tsdb
|
|
object_store: filesystem
|
|
schema: v13
|
|
index:
|
|
prefix: tsdb_index_
|
|
period: 24h
|
|
analytics:
|
|
reporting_enabled: false
|
|
`, d.deployment.AuthEnabled, d.replicationFactor(), d.replicationFactor(), retentionHours)
|
|
}
|
|
|
|
// ==================== 辅助函数 ====================
|
|
|
|
func (d *LokiDeployer) replicationFactor() int {
|
|
if d.deployment.Replicas >= 3 {
|
|
return 3
|
|
}
|
|
if d.deployment.Replicas >= 2 {
|
|
return 2
|
|
}
|
|
return 1
|
|
}
|