package services import ( "encoding/json" "fmt" "sort" "strings" ) // PromtailConfig 主机 Promtail 配置(推送至 lokiEndpoint,采集 logPath) // 生产级配置:持久化位置文件、批处理、重试、CRI日志格式解析 // promtailExternalLabels 组并 cluster 标识与用户自定义标签(JSON)为 external_labels YAML 行 func promtailExternalLabels(cluster string, labelsJSON string) string { lines := []string{fmt.Sprintf(" cluster: %q", cluster)} if labelsJSON != "" { var m map[string]string if err := json.Unmarshal([]byte(labelsJSON), &m); err == nil { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { lines = append(lines, fmt.Sprintf(" %s: %q", k, m[k])) } } } return strings.Join(lines, "\n") } func PromtailConfig(lokiEndpoint, logPath, cluster, labelsJSON string) string { conf := fmt.Sprintf(`server: http_listen_port: 9080 grpc_listen_port: 0 log_level: info log_format: logfmt positions: filename: /var/lib/promtail/positions.yaml sync_period: 10s clients: - url: %s/loki/api/v1/push batchwait: 1s batchsize: 1048576 timeout: 10s backoff_config: min_period: 500ms max_period: 5m max_retries: 10 external_labels: job: host-logs __EXT_LABELS__ limits_config: readline_rate_enabled: false readline_rate: 1000 readline_burst: 10000 max_streams: 0 scrape_configs: - job_name: app static_configs: - targets: [localhost] labels: job: app host: ${HOSTNAME} __path__: %s pipeline_stages: - cri: {} - match: selector: '{job="app"}' stages: - timestamp: source: time format: RFC3339Nano - limit: rate: 1000 burst: 10000 drop: true `, lokiEndpoint, logPath) return strings.Replace(conf, "__EXT_LABELS__", promtailExternalLabels(cluster, labelsJSON), 1) } // PromtailHelmValues DaemonSet 模式 Promtail Helm values(生产级) // 包含:cri解析、kubernetes标签、批处理配置、资源限制 func PromtailHelmValues(lokiEndpoint, cluster, labelsJSON string) string { values := fmt.Sprintf(`config: clients: - url: %s/loki/api/v1/push batchwait: 1s batchsize: 1048576 timeout: 10s backoff_config: min_period: 500ms max_period: 5m max_retries: 10 external_labels: __EXT_LABELS__ positions: filename: /run/promtail/positions.yaml scrape_configs: - job_name: kubernetes-pods kubernetes_sd_configs: - role: pod pipeline_stages: - cri: {} - match: selector: '{app!=""}' stages: - docker: {} - match: selector: '{app!=""}' stages: - labels: app: namespace: pod: container: - limit: rate: 1000 burst: 10000 drop: true relabel_configs: - source_labels: [__meta_kubernetes_pod_controller_name] regex: ([0-9a-z-.]+?)(-[0-9a-f]{8,10})? action: replace target_label: __tmp_controller_name - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name] regex: .+ action: replace target_label: app - source_labels: [__meta_kubernetes_pod_label_app] regex: .+ action: replace target_label: app - source_labels: [__meta_kubernetes_pod_annotation_kubernetes_io_config_hash] regex: .+ action: replace target_label: config_hash - action: replace source_labels: ['__meta_kubernetes__pod_name'] target_label: 'pod' - action: replace source_labels: ['__meta_kubernetes_namespace'] target_label: 'namespace' - action: replace source_labels: ['__meta_kubernetes_pod_container_name'] target_label: 'container' - action: replace source_labels: ['__meta_kubernetes_pod_node_name'] target_label: 'node_name' - replacement: /var/log/pods/*$1/*.log separator: / source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name] target_label: __path__ daemonset: enabled: true resources: limits: cpu: 200m memory: 256Mi requests: cpu: 100m memory: 128Mi extraVolumes: - name: varlog hostPath: path: /var/log - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers - name: promtail-run hostPath: path: /run/promtail extraVolumeMounts: - name: varlog mountPath: /var/log readOnly: true - name: varlibdockercontainers mountPath: /var/lib/docker/containers readOnly: true - name: promtail-run mountPath: /run/promtail `, lokiEndpoint) return strings.Replace(values, "__EXT_LABELS__", promtailExternalLabels(cluster, labelsJSON), 1) } // PromtailDaemonsetYAML DaemonSet 模式纯 YAML 安装(SA/RBAC/ConfigMap/DaemonSet), // 取代 helm 安装 —— 后端无需安装 helm 二进制。 // relabel 体系与主流 helm chart 对齐:namespace/pod/container/node_name + __path__ 按 pod uid 精确匹配。 func PromtailDaemonsetYAML(ns, lokiURL, clusterName, labelsJSON string) string { promtailImage := ResolveImage("docker.io/grafana/promtail:3.4.0") extLabels := promtailExternalLabels(clusterName, labelsJSON) return fmt.Sprintf(`apiVersion: v1 kind: ServiceAccount metadata: name: promtail namespace: %s --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: opsxloki-promtail rules: - apiGroups: [""] resources: ["pods", "nodes"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: opsxloki-promtail roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: opsxloki-promtail subjects: - kind: ServiceAccount name: promtail namespace: %s --- apiVersion: v1 kind: ConfigMap metadata: name: promtail namespace: %s data: promtail.yaml: | server: http_listen_port: 3101 grpc_listen_port: 0 positions: filename: /run/promtail/positions.yaml clients: - url: %s/loki/api/v1/push batchwait: 1s batchsize: 1048576 backoff_config: min_period: 500ms max_period: 5m max_retries: 10 external_labels: job: promtail %s scrape_configs: - job_name: kubernetes-pods kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_namespace] target_label: namespace - source_labels: [__meta_kubernetes_pod_name] target_label: pod - source_labels: [__meta_kubernetes_pod_container_name] target_label: container - source_labels: [__meta_kubernetes_pod_node_name] target_label: node_name - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name, __meta_kubernetes_pod_label_app] regex: ^;*([^;]+)(;.*)?$ target_label: app - separator: / source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_pod_label_app_kubernetes_io_name] target_label: job - source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name] separator: / target_label: __path__ replacement: /var/log/pods/*$1/*.log --- apiVersion: apps/v1 kind: DaemonSet metadata: name: promtail namespace: %s labels: app: promtail spec: selector: matchLabels: app: promtail template: metadata: labels: app: promtail spec: serviceAccountName: promtail tolerations: - effect: NoSchedule operator: Exists containers: - name: promtail image: %s imagePullPolicy: IfNotPresent args: ["-config.file=/etc/promtail/promtail.yaml"] env: - name: HOSTNAME valueFrom: fieldRef: fieldPath: spec.nodeName ports: - name: http-metrics containerPort: 3101 readinessProbe: httpGet: path: /ready port: http-metrics initialDelaySeconds: 10 periodSeconds: 10 resources: requests: cpu: 50m memory: 64Mi limits: cpu: 200m memory: 256Mi volumeMounts: - name: config mountPath: /etc/promtail - name: run mountPath: /run/promtail - name: pods mountPath: /var/log/pods readOnly: true - name: containers mountPath: /var/lib/docker/containers readOnly: true volumes: - name: config configMap: name: promtail - name: run hostPath: path: /run/promtail - name: pods hostPath: path: /var/log/pods - name: containers hostPath: path: /var/lib/docker/containers `, ns, ns, ns, lokiURL, extLabels, ns, promtailImage) } // FluentbitConfig Sidecar 模式 Fluent Bit 配置(采集 logPath,推送至 lokiHost:lokiPort) // Sidecar 已明确知道应用与命名空间,无需 Kubernetes 元数据过滤器。 func FluentbitConfig(lokiHost, lokiPort, logPath, app, namespace string) string { return fmt.Sprintf(`[SERVICE] Flush 1 Log_Level info HTTP_Server On HTTP_Listen 0.0.0.0 HTTP_Port 2020 storage.path /tmp/flb-storage/ storage.sync normal storage.checksum off storage.backlog.mem_limit 5M Health_Check On HC_Errors_Count 5 HC_Retry_Failure_Count 5 HC_Period 60 [INPUT] Name tail Path %s Tag app.%s Refresh_Interval 5 Mem_Buf_Limit 10MB Skip_Long_Lines On Read_from_Head True DB /tmp/flb.db DB.Sync Normal Buffer_Max_Size 1MB storage.type filesystem [FILTER] Name modify Match app.* Add namespace %s Add app %s Add source fluent-bit [OUTPUT] Name loki Match * Host %s Port %s Labels job=fluent-bit, app=%s, namespace=%s Line_Format json Retry_Limit 10 `, logPath, app, namespace, app, lokiHost, lokiPort, app, namespace) } // FluentbitConfigMapYAML 为每个采集策略生成独立 ConfigMap,避免同命名空间的 Sidecar 相互覆盖。 func FluentbitConfigMapYAML(conf, namespace, name string) string { return fmt.Sprintf(`apiVersion: v1 kind: ConfigMap metadata: name: %s namespace: %s data: fluent-bit.conf: | %s `, name, namespace, indentBlock(conf, " ")) } // SidecarPatchYAML 将业务容器与 Fluent Bit 挂到同一日志卷和目录。 func SidecarPatchYAML(configMapName string, mount sidecarLogMount) string { volume := "" if mount.createVolume { volume = fmt.Sprintf(" - name: %s\n emptyDir: {}\n", mount.volumeName) } appMount := "" if mount.mountApplication { appMount = fmt.Sprintf(" - name: %s\n volumeMounts:\n - name: %s\n mountPath: %q\n", mount.containerName, mount.volumeName, mount.mountPath) } subPath := "" if mount.subPath != "" { subPath = fmt.Sprintf(" subPath: %q\n", mount.subPath) } return fmt.Sprintf(`spec: template: spec: volumes: %s - name: fluent-bit-config configMap: name: %s - name: fluent-bit-state emptyDir: {} containers: %s - name: fluent-bit image: %s imagePullPolicy: IfNotPresent ports: - name: fluent-bit-http containerPort: 2020 protocol: TCP volumeMounts: - name: %s mountPath: %q %s readOnly: true - name: fluent-bit-config mountPath: /fluent-bit/etc/ - name: fluent-bit-state mountPath: /tmp resources: limits: cpu: 200m memory: 256Mi requests: cpu: 100m memory: 128Mi securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL livenessProbe: httpGet: path: / port: fluent-bit-http initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: / port: fluent-bit-http initialDelaySeconds: 5 periodSeconds: 5 `, volume, configMapName, appMount, ResolveImage("docker.io/fluent/fluent-bit:5.1.5"), mount.volumeName, mount.mountPath, subPath) } // indentBlock 给每行加前缀(ConfigMap 嵌入配置需缩进) func indentBlock(s, pad string) string { lines := strings.Split(s, "\n") for i, l := range lines { if l != "" { lines[i] = pad + l } } return strings.Join(lines, "\n") } // parseLokiURL 从 http://host:port 提取 host 与 port(Fluent Bit OUTPUT 需要) func parseLokiURL(endpoint string) (host, port string) { u := strings.TrimSpace(endpoint) u = strings.TrimPrefix(u, "http://") u = strings.TrimPrefix(u, "https://") if i := strings.Index(u, "/"); i >= 0 { u = u[:i] } if i := strings.LastIndex(u, ":"); i >= 0 { return u[:i], u[i+1:] } return u, "3100" }