Files
zt-closet-backend/scripts/alloy/config.alloy
T
zhengchiliu e9b9754b57 fix(clothes): 保护失败原因并可靠回退额度
仅属主可查看失败原因,分享只读场景置空;额度回退改用独立
ctx,避免客户端断开导致回退失败;新增 ImageProcessStatus 与
FailReason 响应字段,并抽取 detachedCtx 复用后台任务上下文。
2026-09-16 02:01:19 +08:00

241 lines
13 KiB
Plaintext
Raw 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.
// =============================================================================
// 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",
}
}