Immich 监控实践:基于 Prometheus、Grafana 与结构化日志的性能观测体系
Immich 通过 Prometheus 指标与 OpenTelemetry 埋点提供内置的性能监控能力,并支持将日志切换为结构化 JSON 格式以便接入 Loki、ELK 等日志聚合系统。本文基于 Immich 官方监控文档,结合服务端源码中的遥测实现,完整讲解如何开启指标端点、配置 Prometheus 与 Grafana,以及如何启用 JSON 结构化日志。
Prometheus 采集原理
Prometheus 采用"拉取"(pull)策略:它会周期性地向你配置的各个数据源发起请求来获取指标,数据源在被请求之前不会主动推送任何数据。这意味着被监控的一方——即 Immich 服务端——必须暴露一个 HTTP 端点供 Prometheus 抓取。Immich 的指标默认不暴露,需要显式开启(详见下文配置)。
Immich 的指标采集构建在 OpenTelemetry 之上。从 telemetry.repository.ts 中的 bootstrapTelemetry 可以看到,Immich 使用 NodeSDK 启动遥测,并通过 PrometheusExporter({ port }) 作为指标读取器监听指定端口;同时注册了 HttpInstrumentation、IORedisInstrumentation、NestInstrumentation、PgInstrumentation 四类插桩,分别覆盖 HTTP 请求、Redis 调用、NestJS 路由和 PostgreSQL 查询。由于采用了 OpenTelemetry 插桩,除了导出 Prometheus 指标外,导出分布式追踪(traces)也是可行的。
需要说明的是,监控是选择性开启(opt-in)的功能:Immich 只会采集你明确配置的指标,这些数据不会发送到 Immich 之外的任何地方(除非你自己配置了相应的导出目标)。
指标类型与指标分组
Prometheus 指标有三种基本形式,Immich 文档中对此有明确说明:
- Counter(计数器):只能单调递增。例如某个 API 端点被调用的次数;
- Gauge(仪表盘值):可以在一定范围内上下波动。例如 CPU 使用率;
- Histogram(直方图):每个观测值被归入若干个"桶"(bucket),例如响应时间以毫秒为桶边界。其特殊之处在于桶是累积的:一个观测值不仅落入包含它的最小桶,还会同时计入所有更大的桶。例如直方图有 1ms、5ms、10ms 三个桶时,一次 3ms 的观测会同时计入 5ms 与 10ms 两个桶。
在源码层面,这三类形式对应 telemetry.repository.ts 中 MetricGroupRepository 的三个方法:addToCounter、addToGauge(底层是 OpenTelemetry 的 UpDownCounter)、addToHistogram。所有以毫秒为单位计时的直方图会应用统一的分桶边界,见 aggregationBoundaries:
const aggregationBoundaries = [
0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10_000,
];
Immich 的指标按用途分组为 API(端点调用次数与响应时间)、host(内存与 CPU 使用率)和 IO(内部数据库查询、图像处理等),每组指标可以独立开启或关闭。源码中遥测分组由 ImmichTelemetry 枚举定义,共五个取值:
export enum ImmichTelemetry {
Host = 'host',
Api = 'api',
Io = 'io',
Repo = 'repo',
Job = 'job',
}
repo 对应数据库仓库层的查询指标,job 对应后台任务指标。TelemetryRepository(telemetry.repository.ts)内部按 api、host、jobs、repo 等字段分别持有一个 MetricGroupRepository,在构造时根据环境变量中的遥测配置决定各组是否启用。
开启指标端点
Immich 默认不暴露任何指标端点。要开启它,需要在 .env 文件中添加环境变量:
IMMICH_TELEMETRY_INCLUDE=all
更细粒度的配置方式:
IMMICH_TELEMETRY_INCLUDE:以逗号分隔的列表枚举要采集的分组,例如IMMICH_TELEMETRY_INCLUDE=repo,api;IMMICH_TELEMETRY_EXCLUDE:排除特定分组,例如只排除任务指标IMMICH_TELEMETRY_EXCLUDE=job。
可用的分组取值与默认行为,可在环境变量文档的 Prometheus 一节查到的参数表中确认(两个变量均供 server 容器使用,对应 api、microservices 两个 worker)。
从源码看,解析逻辑位于 config.repository.ts:当 IMMICH_TELEMETRY_INCLUDE 为 all 时展开为全部遥测分组,否则按逗号拆分(空白字符会被忽略);最终生效的分组是 INCLUDE 集合减去 EXCLUDE 集合(setDifference),并会校验每个取值是否属于合法的 ImmichTelemetry,遇到非法取值会直接抛出 Invalid telemetry found 错误使服务启动失败——因此拼写错误会立即暴露,而不是被静默忽略。
关于端点端口:API 指标默认监听 8081,微服务(machine-learning 等)指标默认监听 8082,可分别通过 IMMICH_API_METRICS_PORT 与 IMMICH_MICROSERVICES_METRICS_PORT 覆盖,默认值见 config.repository.ts。如果需要直接查看原始指标数据,可以为 immich_server 容器额外发布 8081:8081 与 8082:8082 端口,访问各自的 /metrics 端点即可看到与 Prometheus 采集一致的原始数据。
将 Prometheus 接入 Docker Compose
下一步是配置一个 Prometheus 实例来抓取上述端点。以下假设你尚无现成的 Prometheus 实例,已有实例的配置步骤类似。
在 Compose 文件中定义 Prometheus 服务(可直接参考仓库中的示例 docker-compose.prod.yml 的组织方式):
immich-prometheus:
container_name: immich_prometheus
ports:
# this exposes the default port for Prometheus so you can interact with it
- 9090:9090
image: prom/prometheus
volumes:
# the Prometheus configuration file - a barebones one is provided to get started
- ./prometheus.yml:/etc/prometheus/prometheus.yml
# a named volume defined in the bottom of the Compose file; it can also be a mounted folder
- prometheus-data:/prometheus
并在全局 volumes 列表中登记命名卷:
volumes:
model-cache:
prometheus-data:
最后一块拼图是 Prometheus 的配置文件。仓库内提供了一个开箱即用的起步版本 docker/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: immich_api
static_configs:
- targets: ['immich-server:8081']
- job_name: immich_microservices
static_configs:
- targets: ['immich-server:8082']
该文件定义了抓取间隔(每 15 秒一次)与两个抓取目标:immich_api 指向服务端容器的 8081 端口(API 指标),immich_microservices 指向 8082 端口(微服务指标)。注意这里使用的是 Docker 内部服务名 immich-server——将这份配置放到与 Compose 文件相同的目录后,无需为 immich 容器额外发布任何新端口,所有通信都在内部 Docker 网络中完成。
执行 docker compose down 与 docker compose up -d 重启栈后,Prometheus 实例即开始采集 Immich 服务端与微服务的指标。仓库提供的配置只是起点,Prometheus 的配置方式非常多,你可以在此基础上自由扩展(如添加告警规则、其他数据源等)。
查看指标
最简单的方式是直接使用 Prometheus 自带的 Web UI:访问 http://<host>:9090 后可以在界面上搜索并可视化指标,也可以查看各数据源(target)的抓取状态。
用 Grafana 做可视化展示
如果需要一个专门的、展示效果更好的工具,可以在 Prometheus 之上接入 Grafana。它连接 Prometheus(以及其他数据源)提供精细的数据可视化。
Grafana 的接入方式与 Prometheus 类似。添加一个服务:
immich-grafana:
container_name: immich_grafana
command: ['./run.sh', '-disable-reporting'] # this is to disable Grafana's telemetry
ports:
- 3000:3000
image: grafana/grafana
volumes:
# stores your pretty dashboards and panels
- grafana-data:/var/lib/grafana
再在 volumes 中登记 grafana-data:
volumes:
model-cache:
prometheus-data:
grafana-data:
-disable-reporting 参数用于关闭 Grafana 自身的遥测上报。重启服务栈后访问 http://<host>:3000,首次登录用户名和密码均为 admin,登录后需要修改密码。随后进入设置(Settings),添加一个数据源,地址填写 http://immich-prometheus:9090,让 Grafana 指向你的 Prometheus 实例。
开始搭建第一个仪表盘吧:新建 Panel 时选择 Prometheus 作为数据源即可。注意频繁保存仪表盘,否则进度会丢失。
结构化 JSON 日志
除 Prometheus 指标外,Immich 还支持结构化 JSON 日志输出,非常适合接入 Grafana Loki、ELK Stack、Datadog、Splunk 等日志聚合系统。
配置
默认情况下 Immich 输出人类可读的彩色控制台日志。启用 JSON 日志只需设置环境变量:
IMMICH_LOG_FORMAT=json
默认的 IMMICH_LOG_FORMAT=console 面向开发场景的人类可读输出;生产部署若配合日志聚合系统,建议改用 json。更多说明可参考环境变量文档中的 General 一节。
JSON 日志格式
启用后,日志以结构化 JSON 逐行输出:
{"level":"log","pid":36,"timestamp":1766533331507,"message":"Initialized websocket server","context":"WebsocketRepository"}
{"level":"warn","pid":48,"timestamp":1766533331629,"message":"Unable to open /build/www/index.html, skipping SSR.","context":"ApiService"}
{"level":"error","pid":36,"timestamp":1766533331690,"message":"Failed to load plugin immich-core:","context":"Error"}
字段含义:
| 字段 | 说明 |
|---|---|
level |
日志级别(log、warn、error 等) |
pid |
进程 ID |
timestamp |
Unix 时间戳(毫秒) |
message |
日志消息 |
context |
产生日志的服务或组件名 |
源码中的日志实现
格式切换的实现位于 logging.repository.ts。Immich 继承 NestJS 的 ConsoleLogger 封装了 MyConsoleLogger,其构造参数中的 json 标志直接来自 IMMICH_LOG_FORMAT:logFormat === LogFormat.Json 时启用 JSON 序列化,并自动关闭 ANSI 彩色输出(isColorEnabled 仅在非 JSON 模式下为真),保证日志聚合器拿到的是干净的结构化文本。日志级别支持 verbose、debug、log、warn、error、fatal 六级,且级别过滤由 LOG_LEVELS 前缀切片控制(例如设为 warn 时,仅 warn 及以上级别会被输出)。
一个值得注意的实现细节:formatContext 方法(logging.repository.ts)会从 nestjs-cls 的 Correlation ID 中读取请求关联 ID 并拼接进上下文前缀。从源码结构看,这意味着开启 JSON 日志后,同一请求链路产生的日志可通过 correlation ID 串联起来,便于在日志聚合系统中做请求级追踪——这与前文提到的 OpenTelemetry 上下文机制(AsyncLocalStorageContextManager)在思路上是一致的。
小结
Immich 的监控体系由三层组成:基于 OpenTelemetry + PrometheusExporter 的指标采集(按 host/api/io/repo/job 分组、可独立开关)、通过 Docker 内部网络抓取的 Prometheus + Grafana 可视化链路,以及可通过 IMMICH_LOG_FORMAT=json 启用的结构化日志。三者均以环境变量驱动、默认关闭,你可以根据运维需求选择开启其中的任意一部分。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00