=== 一、主机「测试连接」永远显示旧错误(真实 bug)===
HostService.Validate 的失败分支也写 status=active,且从不写 statusMessage:
if info, err := client.GetHostInfo(ctx); err == nil { 更新 os/arch }
else { s.Update(id, {status:"active"}) } // ← 失败也标记成功
结果:连不上也显示正常,页面永远停在几分钟前的旧错误文案,
用户改了 IP 再点测试,还以为"改了还是不行"。
改为区分成功/失败并写清原因:
成功 → status=active + os/arch + statusMessage="连接成功"
失败 → status=error + statusMessage=FormatSSHError(err)
=== 二、回环地址在平台里必然连不上 ===
平台跑在容器/Pod 内,127.0.0.1 指向平台自身。
- 新建主机时拦截:hostIP 为 127.0.0.1/localhost/::1 直接报错并解释原因
- 前端表单同步校验 + 文案
- SSH 错误映射补充:连接被拒绝时提示可能是地址填了回环
=== 三、主机模式采集系统日志(两处会导致采集失败)===
1. pipeline 一律套 cri 阶段:cri 只认容器运行时格式,
套在 /var/log/messages 这类 syslog 上会取不到时间戳、整行被丢。
→ appPipelineStages() 按路径判断:容器日志用 cri,普通文本日志只做限流。
2. promtailExternalLabels 写死 8 空格缩进:主机模板 external_labels 只有 4 空格,
一旦给采集器配置了标签就生成非法 YAML,Promtail 装不上。
→ 增加 indent 参数,主机 6 空格 / DaemonSet 8 空格。
3. 新增 system 采集任务:
/var/log/{messages,secure,cron,maillog,syslog,dmesg}
正则解析 syslog 时间戳(Jan _2 15:04:05,Asia/Shanghai)+ 限流
测试:TestHostPromtailConfigValid / CRIPathKeepsCRIStage / PlainFileHasNoCRIStage
TestPromtailManifestParses 全部通过
=== 四、资源用量 CPU 解析(nanocores)===
metrics-server 返回的 CPU 常是纳核(如 "194533645n"=194.5m)。
原解析只处理 "m",导致 194 亿核、页面出现"CPU 已达 limit 的 4863341125%"。
→ parseQuantityMilli 支持 n/u/m/整核
实测:CPU 当前=178m,峰值=3300m,建议"峰值占 limit 82%,可考虑提前扩容"
=== 五、其他 ===
- 添加集群默认改用 Token 认证(最小权限、可随时吊销)
- RBAC 片段补 metrics.k8s.io(资源面板需要,独立 API 组常被漏授)
- 文案去掉 Grafana 需求描述(平台自身不部署 Grafana)
镜像:lghost/lokistack:latest + v0.1 = sha256:7bddd0f4d1f1
253 lines
5.9 KiB
Go
253 lines
5.9 KiB
Go
// LokiDeployer — k8s-yaml 模式 manifest 生成(StatefulSet/PVC/ConfigMap/Service)。
|
||
package services
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/base64"
|
||
"fmt"
|
||
|
||
"lokistack/pkg/crypto"
|
||
)
|
||
|
||
func (d *LokiDeployer) generateManifest(ns string) string {
|
||
version := d.effectiveVersion()
|
||
image := ResolveImageFor("loki", "grafana/loki", version)
|
||
replicas := d.deployment.Replicas
|
||
if replicas < 1 {
|
||
replicas = 1
|
||
}
|
||
storageSize := d.deployment.StorageSize
|
||
if storageSize == "" {
|
||
storageSize = "20Gi"
|
||
}
|
||
|
||
// 资源默认值按实测定:Loki 单实例在 compaction / 大批量查询时
|
||
// 峰值 CPU 约 3.3 核、峰值内存约 4.96 GiB(2 小时窗口实测)。
|
||
// 之前的 1000m/1Gi 默认值会同时触发 CPU 限流和 OOMKilled。
|
||
cpu := d.deployment.ResourcesCPU
|
||
mem := d.deployment.ResourcesMem
|
||
if cpu == "" {
|
||
cpu = "4"
|
||
}
|
||
if mem == "" {
|
||
mem = "8Gi"
|
||
}
|
||
|
||
configData := d.generateLokiConfig()
|
||
secretManifest := ""
|
||
configExpandArg := ""
|
||
storageEnvBlock := ""
|
||
if sc := d.deployment.StorageConfig; sc != nil {
|
||
switch sc.StorageType {
|
||
// 注意:必须包含 cos / obs —— 腾讯云 COS、华为 OBS 同样走 S3 兼容协议,
|
||
// 也需要生成 loki-storage-credentials Secret、注入 LOKI_STORAGE_* 环境变量、
|
||
// 并给 Loki 加 -config.expand-env=true,否则配置里的 ${LOKI_STORAGE_ACCESS_KEY}
|
||
// 无法展开 → AK 为空 → 对象存储 403 InvalidAccessKeyId。
|
||
case "s3", "oss", "cos", "obs", "minio", "azure":
|
||
secretManifest = fmt.Sprintf(`---
|
||
apiVersion: v1
|
||
kind: Secret
|
||
metadata:
|
||
name: loki-storage-credentials
|
||
namespace: %s
|
||
type: Opaque
|
||
data:
|
||
accessKey: %s
|
||
secretKey: %s`, ns,
|
||
base64.StdEncoding.EncodeToString([]byte(crypto.DecryptField(sc.AccessKey))),
|
||
base64.StdEncoding.EncodeToString([]byte(crypto.DecryptField(sc.SecretKey))))
|
||
configExpandArg = "\n - -config.expand-env=true"
|
||
storageEnvBlock = `
|
||
env:
|
||
- name: LOKI_STORAGE_ACCESS_KEY
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: loki-storage-credentials
|
||
key: accessKey
|
||
- name: LOKI_STORAGE_SECRET_KEY
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: loki-storage-credentials
|
||
key: secretKey`
|
||
}
|
||
}
|
||
// ConfigMap/Secret 更新本身不会重建 Pod。把内容摘要写入 Pod 模板以触发滚动更新。
|
||
configHash := sha256.Sum256([]byte(configData + "\n" + secretManifest))
|
||
|
||
manifest := fmt.Sprintf(`apiVersion: v1
|
||
kind: Namespace
|
||
metadata:
|
||
name: %s
|
||
---
|
||
apiVersion: v1
|
||
kind: ConfigMap
|
||
metadata:
|
||
name: loki-config
|
||
namespace: %s
|
||
data:
|
||
loki.yaml: |
|
||
%s
|
||
%s
|
||
---
|
||
apiVersion: v1
|
||
kind: Service
|
||
metadata:
|
||
name: loki
|
||
namespace: %s
|
||
labels:
|
||
app: loki
|
||
spec:
|
||
type: ClusterIP
|
||
ports:
|
||
- name: http
|
||
port: 3100
|
||
targetPort: 3100
|
||
protocol: TCP
|
||
- name: grpc
|
||
port: 9095
|
||
targetPort: 9095
|
||
protocol: TCP
|
||
selector:
|
||
app: loki
|
||
---
|
||
apiVersion: v1
|
||
kind: PersistentVolumeClaim
|
||
metadata:
|
||
name: loki-data
|
||
namespace: %s
|
||
spec:
|
||
accessModes:
|
||
- ReadWriteOnce
|
||
resources:
|
||
requests:
|
||
storage: %s
|
||
---
|
||
apiVersion: apps/v1
|
||
kind: StatefulSet
|
||
metadata:
|
||
name: loki
|
||
namespace: %s
|
||
labels:
|
||
app: loki
|
||
spec:
|
||
serviceName: loki
|
||
replicas: %d
|
||
selector:
|
||
matchLabels:
|
||
app: loki
|
||
template:
|
||
metadata:
|
||
labels:
|
||
app: loki
|
||
annotations:
|
||
checksum/loki-config: "%x"
|
||
spec:
|
||
securityContext:
|
||
runAsUser: 0
|
||
runAsGroup: 0
|
||
fsGroup: 0
|
||
containers:
|
||
- name: loki
|
||
image: %s
|
||
imagePullPolicy: IfNotPresent
|
||
args:
|
||
- -config.file=/etc/loki/loki.yaml%s%s
|
||
ports:
|
||
- name: http
|
||
containerPort: 3100
|
||
protocol: TCP
|
||
- name: grpc
|
||
containerPort: 9095
|
||
protocol: TCP
|
||
resources:
|
||
limits:
|
||
cpu: "%s"
|
||
memory: "%s"
|
||
requests:
|
||
cpu: "1"
|
||
memory: "2Gi"
|
||
securityContext:
|
||
allowPrivilegeEscalation: false
|
||
readOnlyRootFilesystem: false
|
||
capabilities:
|
||
drop:
|
||
- ALL
|
||
livenessProbe:
|
||
httpGet:
|
||
path: /ready
|
||
port: http
|
||
initialDelaySeconds: 30
|
||
periodSeconds: 10
|
||
timeoutSeconds: 5
|
||
failureThreshold: 6
|
||
readinessProbe:
|
||
httpGet:
|
||
path: /ready
|
||
port: http
|
||
initialDelaySeconds: 10
|
||
periodSeconds: 5
|
||
timeoutSeconds: 3
|
||
failureThreshold: 3
|
||
startupProbe:
|
||
httpGet:
|
||
path: /ready
|
||
port: http
|
||
initialDelaySeconds: 10
|
||
periodSeconds: 10
|
||
timeoutSeconds: 5
|
||
failureThreshold: 30
|
||
volumeMounts:
|
||
- name: config
|
||
mountPath: /etc/loki
|
||
readOnly: true
|
||
- name: data
|
||
mountPath: /var/lib/loki
|
||
- name: wal
|
||
mountPath: /var/lib/loki/wal
|
||
volumes:
|
||
- name: config
|
||
configMap:
|
||
name: loki-config
|
||
- name: data
|
||
persistentVolumeClaim:
|
||
claimName: loki-data
|
||
- name: wal
|
||
emptyDir: {}
|
||
`, ns, ns, indentYAML(configData, 4), secretManifest, ns, ns, storageSize, ns, replicas, configHash, image, configExpandArg, storageEnvBlock, cpu, mem)
|
||
|
||
// 多副本:追加 Headless Service(clusterIP: None + selector app:loki),
|
||
// 使 memberlist 的 join_members (loki-headless.<ns>.svc.cluster.local) 能解析到所有 Pod IP。
|
||
if replicas >= 2 {
|
||
manifest += fmt.Sprintf(`
|
||
---
|
||
apiVersion: v1
|
||
kind: Service
|
||
metadata:
|
||
name: loki-headless
|
||
namespace: %s
|
||
labels:
|
||
app: loki
|
||
spec:
|
||
clusterIP: None
|
||
ports:
|
||
- name: http
|
||
port: 3100
|
||
targetPort: 3100
|
||
protocol: TCP
|
||
- name: grpc
|
||
port: 9095
|
||
targetPort: 9095
|
||
protocol: TCP
|
||
- name: memberlist
|
||
port: 7946
|
||
targetPort: 7946
|
||
protocol: TCP
|
||
selector:
|
||
app: loki
|
||
`, ns)
|
||
}
|
||
return manifest
|
||
}
|
||
|
||
// ==================== Loki 本地配置生成 (text/template) ====================
|