仅属主可查看失败原因,分享只读场景置空;额度回退改用独立 ctx,避免客户端断开导致回退失败;新增 ImageProcessStatus 与 FailReason 响应字段,并抽取 detachedCtx 复用后台任务上下文。
241 lines
13 KiB
Plaintext
241 lines
13 KiB
Plaintext
// =============================================================================
|
||
// Grafana Alloy 采集配置 —— 只采集 zt-closet-backend 的本地日志文件
|
||
//
|
||
// ⚠️ 本文件有两种「花括号」,语法规则相反,混用会直接启动失败:
|
||
// 1) 块(block) endpoint { } / stage.json { } / logging { }
|
||
// → 参数之间用【换行】分隔,【不能加逗号】
|
||
// 加了会报:Argument definitions must be separated by newlines, not commas
|
||
// 2) 对象字面量(= 号右边的表达式)
|
||
// values = { } / expressions = { } / external_labels = { } / path_targets 里的 { }
|
||
// → 字段之间【必须用逗号】分隔,否则报:missing ',' in field list
|
||
// 判断方法:`名字 {` 后面直接换行 = 块;`名字 = {` 是对象字面量。
|
||
// 改动本文件时务必按此规则,否则 Alloy 起不来。
|
||
//
|
||
// 部署方式:Alloy 以 systemd 服务运行(不是 Docker)
|
||
// 配置路径 /etc/alloy/config.alloy (把本文件拷到这里)
|
||
// 存储路径 --storage.path=/var/lib/alloy/data(见 systemctl cat alloy)
|
||
//
|
||
// 采集对象:lumberjack 滚动写出的 JSON 日志(见 main.go setupSlogLogger)
|
||
// 宿主路径 /opt/zt-closet/storage/logs/app.log
|
||
// 容器/服务内读取路径由你挂载或直接指向该目录
|
||
// 归档 app-*.log.gz(Compress: true,默认不采集,见 loki.source.file 注释)
|
||
// 实测格式 {"time":"2026-09-09T01:52:52.772742663+08:00","level":"INFO",
|
||
// "source":{"function":"...","file":"...","line":44},
|
||
// "msg":"GIN_REQUEST","request_id":"...","method":"GET","path":"/pods",
|
||
// "query":"","status":404,"ip":"...","latency":"4.559µs","user_agent":""}
|
||
// =============================================================================
|
||
|
||
// 若 Alloy 与应用不在同一文件系统视图下(比如 Alloy 装在别的主机/容器),
|
||
// 请把下面 local.file_match 里的 __path__ 改成实际可见的路径。
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// logging —— Alloy 自身的运行日志(不是被采集的业务日志)
|
||
// -----------------------------------------------------------------------------
|
||
logging {
|
||
level = "info" // Alloy 自身日志级别:debug / info / warn / error,排查采集问题时改 debug
|
||
format = "logfmt" // Alloy 自身日志格式,logfmt 便于人读
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// local.file_match —— 「发现」要采集哪些文件,并给它们打上静态标签
|
||
// 只负责找文件、产出 target 列表,不读内容。
|
||
// -----------------------------------------------------------------------------
|
||
local.file_match "app_logs" {
|
||
// ↓ 这是一个「对象字面量」,字段之间必须用逗号分隔
|
||
path_targets = [{
|
||
// ⚠️ 必须限定为 app*.log,不能用 *.log:
|
||
// 同目录下还有 classification.log(Python 服务输出,是
|
||
// "INFO:__main__:..." 纯文本 + tqdm 进度条,不是 JSON)。
|
||
// 用 *.log 会把它一起采走,解析不出任何字段,还会混入 \r 进度条垃圾。
|
||
__path__ = "/opt/zt-closet/storage/logs/app*.log",
|
||
|
||
// 以下三个是自定义标签,会附加到每一条日志上(低基数,可以安全做标签)。
|
||
job = "zt-closet-backend", // 采集任务名,多服务时用于区分
|
||
service_name = "zt-closet-backend", // 服务名,推荐保留(Loki 生态的通用标签名)
|
||
env = "prod", // 环境标识,prod / test / dev
|
||
}]
|
||
|
||
// 每隔多久重新扫描一次目录,发现新建/轮转出来的新文件。
|
||
// 该值就是官方默认值,保留只为显式可见。
|
||
sync_period = "10s"
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// loki.source.file —— 真正「读」文件内容,按行产出日志
|
||
// -----------------------------------------------------------------------------
|
||
loki.source.file "app_logs" {
|
||
// 上游 local.file_match 发现的文件列表。
|
||
targets = local.file_match.app_logs.targets
|
||
|
||
// 下游处理管道;解析 / 打标签 / 降噪都在 loki.process 里做。
|
||
forward_to = [loki.process.app_logs.receiver]
|
||
|
||
// 从文件末尾开始读:首次部署时不重放历史日志,
|
||
// 避免把几十 MB 旧日志一次性灌进 Loki。false 则会从头回放。
|
||
tail_from_end = true
|
||
|
||
// 读取位置(offset)记录在 --storage.path 指定的目录,服务重启后接着读、不重复。
|
||
// 该目录必须持久化(systemd 下通常是 /var/lib/alloy/data,属主为 alloy 用户),
|
||
// 否则 offset 丢失,配合 tail_from_end=true 会直接跳过一段日志。
|
||
|
||
// ↓↓↓ 可选:需要采集 lumberjack 压缩归档的 app-*.log.gz 时启用
|
||
// 注意:官方明确说明 tail_from_end 与 decompression 不可同时使用,
|
||
// 启用下面这段时需要把上面的 tail_from_end 删掉。
|
||
// decompression {
|
||
// enabled = true // 注意:块参数只用换行分隔,不要加逗号
|
||
// initial_delay = "10s" // 文件写入后延迟多久开始解压读取,避免读到半截文件
|
||
// format = "gz" // 合法值只有 gz / z / bz2
|
||
// }
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// loki.process —— 日志处理管道:把 JSON 拆成字段,决定哪些做标签、哪些做元数据
|
||
// 这是整个配置里唯一需要按业务调整的地方。
|
||
// -----------------------------------------------------------------------------
|
||
loki.process "app_logs" {
|
||
// ---- 第一步:解析 JSON:把一行 JSON 的每个 key 提取成可用字段 ----
|
||
// 左侧是管道内的字段名,右侧是 JMESPath 表达式(点号表示取嵌套字段)。
|
||
stage.json {
|
||
// ↓ 对象字面量:字段之间必须用逗号
|
||
expressions = {
|
||
time = "time", // 日志自身时间戳(RFC3339Nano),供 stage.timestamp 使用
|
||
level = "level", // 日志级别:DEBUG/INFO/WARN/ERROR
|
||
msg = "msg", // 日志消息,如 GIN_REQUEST
|
||
trace_id = "trace_id", // 链路 ID,仅 slog.XxxContext(ctx,...) 且 ctx 内有 span 时存在
|
||
span_id = "span_id", // span ID,同上
|
||
request_id = "request_id", // 单请求 ID,见 middleware/logger.go
|
||
method = "method", // HTTP 方法
|
||
path = "path", // ⚠️ 高基数字段:打的是 c.Request.URL.Path 原始路径,
|
||
// 不是 gin 路由模板,含 UUID 等动态段,
|
||
// 所以只能做元数据,绝不能提升为标签
|
||
status = "status", // HTTP 状态码
|
||
ip = "ip", // 客户端 IP
|
||
latency = "latency", // 处理耗时,字符串如 "4.559µs"(非数字,不能直接聚合)
|
||
source_file = "source.file", // 因为 AddSource:true,日志自带 文件:行号
|
||
source_line = "source.line", // ↑ 同上,定位代码位置用
|
||
}
|
||
}
|
||
|
||
// ---- 第二步:用日志自身的时间戳,而不是 Alloy 读到它的时间 ----
|
||
// 不加这个 stage,Loki 会用「采集时刻」当作日志时间:一旦 Alloy 重启、
|
||
// 限流重试或追赶积压,历史日志会被写成"现在",时间范围查询与
|
||
// count_over_time 告警全部错位,且没有任何报错提示。
|
||
// format 用 RFC3339Nano 精确匹配实测的 "2026-09-09T01:52:52.772742663+08:00"。
|
||
// 注意:若配合 decompression 回放超过 14 天的归档,Loki 会以
|
||
// "entry too far behind" 拒收整批(reject_old_samples_max_age 默认 14d)。
|
||
// ⚠️ 注意这是「块」不是对象字面量,参数之间【只能用换行分隔,禁止加逗号】
|
||
stage.timestamp {
|
||
source = "time"
|
||
format = "RFC3339Nano"
|
||
}
|
||
|
||
// ---- 第三步:把 level / status 提升为「索引标签」----
|
||
// 标签参与索引、筛选最快,但每个不同的标签组合都会形成一个独立 stream。
|
||
// 这里 level(4 种) × status(约 8 种) ≈ 32 个 stream,量级很小可以接受;
|
||
// 注意 path 是高基数字段,绝不能加进来。
|
||
stage.labels {
|
||
values = {
|
||
level = "", // 空字符串表示「沿用同名字段」,等价于 level = "level"
|
||
status = "",
|
||
}
|
||
}
|
||
|
||
// ---- 第四步:其余字段降级为「结构化元数据」(structured metadata) ----
|
||
// 元数据不建索引、不加剧 stream 基数,但 LogQL 里照样能用
|
||
// | trace_id="xxx" 过滤,也能点击跳转 Tempo 查链路。
|
||
//
|
||
// ⚠️ 启用前提(缺一不可,否则 distributor 直接返回 400 拒收整批,外部
|
||
// 表现为「Alloy 不停重试、Loki 里一条日志都没有」,很难定位):
|
||
// 1) Loki ≥ 3.0(2.9 起为实验特性)
|
||
// 2) schema 版本 ≥ v13,且索引类型必须为 tsdb(不支持已废弃的 boltdb-shipper)
|
||
// 3) limits_config.allow_structured_metadata: true
|
||
// 确认不了就先整段注释掉:其余配置照常工作,只是查询时少了这些字段。
|
||
// 另外元数据上限为每行 64KB / 128 条,这里 9 个字段远未触及。
|
||
stage.structured_metadata {
|
||
values = {
|
||
trace_id = "",
|
||
span_id = "",
|
||
request_id = "", // 按 request_id 串起一次请求的全部日志
|
||
method = "",
|
||
path = "", // 高基数字段放这里最合适:可过滤,但不产生新 stream
|
||
ip = "",
|
||
latency = "",
|
||
source_file = "", // 出错时直接看是哪个文件哪一行打的
|
||
source_line = "",
|
||
// 注意:status 已在上一 stage 提升为标签,此处不再重复设置,
|
||
// 避免同一字段既是标签又是元数据导致语义混淆
|
||
}
|
||
}
|
||
|
||
// ---- 第五步(当前已启用):丢弃 Docker HEALTHCHECK 产生的探针噪音 ----
|
||
// 若服务以 systemd 直接运行而非 Docker,没有 HEALTHCHECK,这段可删除。
|
||
// 需要保留这层可用性信号时,也把本段整段删除即可。
|
||
//
|
||
// 说明:stage.drop 的 source 只能引用「extracted map」中的字段
|
||
// (官方文档明确限定为 extracted data,且该 stage 没有 source_type 参数,
|
||
// 读不到标签、也读不到 structured metadata)。path 正是第一步
|
||
// stage.json 提取出来的,因此这里可用。多个条件之间是 AND 关系。
|
||
// 丢弃计数见 Alloy UI:loki_process_dropped_lines_total{reason="healthcheck"}。
|
||
// ⚠️ 同样这是「块」,参数之间只用换行,禁止加逗号
|
||
stage.drop {
|
||
source = "path" // 引用 stage.json 提取出的字段
|
||
value = "/ping" // 与 source 值完全相等时丢弃
|
||
drop_counter_reason = "healthcheck"
|
||
}
|
||
|
||
// 处理结果推给出口。
|
||
forward_to = [loki.write.default.receiver]
|
||
}
|
||
|
||
// -----------------------------------------------------------------------------
|
||
// loki.write —— 出口:把日志批量推送到 Loki 的 push 接口
|
||
//
|
||
// 层级注意:顶层只有 external_labels / max_streams;
|
||
// url、batch_*、*_backoff_*、remote_timeout、tenant_id、basic_auth 等
|
||
// 都必须写在 endpoint 块内。写到顶层 Alloy 启动即报 unrecognized attribute。
|
||
// -----------------------------------------------------------------------------
|
||
loki.write "default" {
|
||
endpoint {
|
||
// Loki 写入地址。
|
||
url = "http://119.45.6.212:3100/loki/api/v1/push"
|
||
|
||
// ---- 批量:攒批降低请求数 ----
|
||
batch_wait = "60s" // 最多攒 60s 才发:请求数最省,但日志最多延迟 1 分钟才可见;
|
||
// 真实延迟 = min(batch_wait, 攒满 batch_size)。
|
||
// 需要近实时排障就调到 1s~5s。
|
||
batch_size = "1MiB" // 攒够 1MiB 立即发,与 batch_wait 谁先满足用谁
|
||
|
||
// 请求超时,默认 10s;跨公网写入或 Loki 较慢时可放大。
|
||
remote_timeout = "10s"
|
||
|
||
// ---- 重试:Loki 短暂不可用时退避重试,避免丢日志 ----
|
||
// 必须保证 min < max:退避实现中一旦 nextDelayMin >= nextDelayMax
|
||
// 就直接返回 MinBackoff 且永不增长(max 被静默忽略)。
|
||
// ⚠️ endpoint 是「块」,参数只用换行分隔,禁止加逗号
|
||
min_backoff_period = "500ms" // 退避起点,官方默认 500ms
|
||
max_backoff_period = "5m" // 退避上限,官方默认 5m
|
||
max_backoff_retries = 10 // 重试 10 次后仍失败则丢弃该批
|
||
|
||
// ⚠️ 单位陷阱:Go duration 中 m=分钟、ms=毫秒,
|
||
// "500m" 是 500 分钟(8h20m),"500ms" 才是 500 毫秒。
|
||
//
|
||
// 提示:退避不是固定倍数,而是在 [min, max) 区间取随机抖动值、区间再翻倍。
|
||
// 以 min=500ms / retries=10 为例,实际发生的 9 次等待区间依次为
|
||
// [0.5s,1s) → [1s,2s) → … → [128s,256s),最大也只到 256s,
|
||
// 即使把 max 设成 5m 或 500m 也不会触及上限。真正决定「最多重试多久」
|
||
// 的是 max_backoff_retries。
|
||
|
||
// Loki 开启多租户鉴权时才需要:
|
||
// tenant_id = "zt-closet"
|
||
// basic_auth {
|
||
// username = "zt-closet"
|
||
// password = sys.env("LOKI_PASSWORD") // 密钥只从环境变量读,不要写进本文件
|
||
// }
|
||
}
|
||
|
||
// 顶层参数:附加到所有日志的全局标签,便于区分集群/机房。
|
||
external_labels = {
|
||
cluster = "prod",
|
||
}
|
||
}
|