1. 引言
日志是容器化应用排障与观测的第一手资料。然而很多团队在容器化初期,往往把日志当成"最后才考虑"的事:应用在容器里直接写文件、日志无限增长撑爆磁盘、时区错乱导致排查困难......等到线上出问题时,才发现连一条完整的日志链路都拉不出来。
本文围绕容器日志的收集与管理 展开,先讲清楚日志驱动矩阵与"为什么容器里必须打 stdout/stderr",再给出日志轮转与大小限制的配置方法,最后分别用 Loki + Promtail + Grafana (轻量)和 ELK(重量)两条路线,演示一条日志"从应用到面板"的完整链路。全文以 ValidX demo 为改造对象,带你亲手把日志规范落地。
2. 日志驱动矩阵:json-file / journald / syslog / local
Docker 通过 logging driver(日志驱动) 决定容器日志写到哪里、以什么格式存储。默认是 json-file,但生产环境往往需要按需切换。
| 驱动 | 输出位置 | 典型场景 | 备注 |
|---|---|---|---|
json-file |
宿主机 /var/lib/docker/containers/<id>/ 下的 JSON 文件 |
默认驱动,配合 Filebeat/Promtail 采集 | 每条日志一行 JSON,含 log、stream、time 字段 |
journald |
宿主机 systemd journal | 与 systemd 深度集成的系统 | 可用 journalctl 直接查看 |
syslog |
宿主机 syslog 服务(默认 UDP 514) | 已有 syslog 集中平台的企业 | 兼容传统日志设施 |
local |
宿主机本地文件(自定义格式) | 对性能敏感、无需外部采集 | 无轮转、无结构化,需自行处理 |
如何查看当前驱动?
bash
docker info --format '{{.LoggingDriver}}'
如何为单个容器指定驱动?
bash
docker run -d --log-driver json-file --log-opt max-size=10m --log-opt max-file=3 nginx
如何全局配置? 在 /etc/docker/daemon.json 中设置:
json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
注意:
local驱动虽然性能好,但格式不标准,外部采集器(如 Promtail)解析成本高,一般不建议在需要集中观测的场景使用。
3. 应用日志规范:为什么容器里日志必须打 stdout/stderr
这是容器日志最核心的一条原则:应用只把日志写到标准输出(stdout)和标准错误(stderr),不要自己写文件。
原因有三:
-
日志生命周期交给平台。容器是瞬态的,Pod 随时可能被重建。应用自己写文件,容器一删日志就没了;而 stdout/stderr 由 Docker/容器运行时统一接管,配合日志驱动持久化或外发,日志才不随容器消亡。
-
统一采集入口。Promtail、Filebeat、Fluentd 等采集器都默认从容器 stdout/stderr 对应的文件或 journal 读取。应用写文件的话,采集器还得进容器、找路径、处理权限,链路复杂且脆弱。
-
避免多写一份磁盘。应用写文件 + 运行时再采集,等于日志写了两遍,浪费 IO 和磁盘。
改造前(错误示范) :应用在容器里写 /var/log/app.log。
改造后(正确示范):应用把日志打到 stdout/stderr,由运行时统一收集。
以 ValidX demo 为例,改造前它把日志写进文件,改造后我们让它直接输出到 stdout:
python
# 改造前:写文件
# with open("/var/log/validx.log", "a") as f:
# f.write(f"{time} {level} {msg}\n")
# 改造后:打 stdout
import sys, time
def log(level: str, msg: str):
line = f"{time.strftime('%Y-%m-%dT%H:%M:%S%z')} {level} {msg}"
print(line, flush=True) # stdout
if level == "ERROR":
print(line, file=sys.stderr, flush=True) # stderr
关键点:
flush=True保证日志立即写出,避免因缓冲导致日志延迟或丢失。
4. 日志轮转与大小限制:max-size / max-file
容器日志默认无限增长,这是最常见的坑。一个高频打印的容器,几天就能把宿主机磁盘写满。必须配置轮转。
json-file 驱动的轮转参数:
| 参数 | 作用 | 建议值 |
|---|---|---|
max-size |
单个日志文件达到该大小即轮转 | 10m 或 50m |
max-file |
保留的日志文件个数 | 3 或 5 |
配置示例(daemon.json 全局):
json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Kubernetes 场景:kubelet 负责容器日志轮转,配置在 kubelet 参数中:
yaml
# kubelet 配置
containerLogMaxSize: 10Mi
containerLogMaxFiles: 3
验证轮转是否生效:
bash
# 查看容器日志文件
ls -lh /var/lib/docker/containers/<container-id>/*.log
# 触发大量日志后观察文件数量
docker logs --tail 50 <container>
坑位提醒:
max-size与max-file只对新写入的日志生效,已存在的超大日志文件不会自动切割。配置后建议滚动重启容器。
5. 轻量方案:Loki + Promtail + Grafana 从零搭建
Loki 是 Grafana 团队推出的日志聚合系统,特点是不建全文索引,只索引标签,因此比 ELK 轻量得多,资源占用小,适合中小团队。
5.1 架构
#mermaid-svg-QltHpLTIv7w9ypCV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QltHpLTIv7w9ypCV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QltHpLTIv7w9ypCV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QltHpLTIv7w9ypCV .error-icon{fill:#552222;}#mermaid-svg-QltHpLTIv7w9ypCV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QltHpLTIv7w9ypCV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QltHpLTIv7w9ypCV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QltHpLTIv7w9ypCV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QltHpLTIv7w9ypCV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QltHpLTIv7w9ypCV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QltHpLTIv7w9ypCV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QltHpLTIv7w9ypCV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QltHpLTIv7w9ypCV .marker.cross{stroke:#333333;}#mermaid-svg-QltHpLTIv7w9ypCV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QltHpLTIv7w9ypCV p{margin:0;}#mermaid-svg-QltHpLTIv7w9ypCV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QltHpLTIv7w9ypCV .cluster-label text{fill:#333;}#mermaid-svg-QltHpLTIv7w9ypCV .cluster-label span{color:#333;}#mermaid-svg-QltHpLTIv7w9ypCV .cluster-label span p{background-color:transparent;}#mermaid-svg-QltHpLTIv7w9ypCV .label text,#mermaid-svg-QltHpLTIv7w9ypCV span{fill:#333;color:#333;}#mermaid-svg-QltHpLTIv7w9ypCV .node rect,#mermaid-svg-QltHpLTIv7w9ypCV .node circle,#mermaid-svg-QltHpLTIv7w9ypCV .node ellipse,#mermaid-svg-QltHpLTIv7w9ypCV .node polygon,#mermaid-svg-QltHpLTIv7w9ypCV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QltHpLTIv7w9ypCV .rough-node .label text,#mermaid-svg-QltHpLTIv7w9ypCV .node .label text,#mermaid-svg-QltHpLTIv7w9ypCV .image-shape .label,#mermaid-svg-QltHpLTIv7w9ypCV .icon-shape .label{text-anchor:middle;}#mermaid-svg-QltHpLTIv7w9ypCV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QltHpLTIv7w9ypCV .rough-node .label,#mermaid-svg-QltHpLTIv7w9ypCV .node .label,#mermaid-svg-QltHpLTIv7w9ypCV .image-shape .label,#mermaid-svg-QltHpLTIv7w9ypCV .icon-shape .label{text-align:center;}#mermaid-svg-QltHpLTIv7w9ypCV .node.clickable{cursor:pointer;}#mermaid-svg-QltHpLTIv7w9ypCV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QltHpLTIv7w9ypCV .arrowheadPath{fill:#333333;}#mermaid-svg-QltHpLTIv7w9ypCV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QltHpLTIv7w9ypCV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QltHpLTIv7w9ypCV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QltHpLTIv7w9ypCV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QltHpLTIv7w9ypCV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QltHpLTIv7w9ypCV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QltHpLTIv7w9ypCV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QltHpLTIv7w9ypCV .cluster text{fill:#333;}#mermaid-svg-QltHpLTIv7w9ypCV .cluster span{color:#333;}#mermaid-svg-QltHpLTIv7w9ypCV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QltHpLTIv7w9ypCV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QltHpLTIv7w9ypCV rect.text{fill:none;stroke-width:0;}#mermaid-svg-QltHpLTIv7w9ypCV .icon-shape,#mermaid-svg-QltHpLTIv7w9ypCV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QltHpLTIv7w9ypCV .icon-shape p,#mermaid-svg-QltHpLTIv7w9ypCV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QltHpLTIv7w9ypCV .icon-shape .label rect,#mermaid-svg-QltHpLTIv7w9ypCV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QltHpLTIv7w9ypCV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QltHpLTIv7w9ypCV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QltHpLTIv7w9ypCV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 容器 stdout/stderr
Promtail 采集
Loki 存储与查询
Grafana 展示
- Promtail :采集器,读取容器日志文件,打上标签(如
app、namespace)后推给 Loki。 - Loki:日志存储与查询引擎,提供 LogQL 查询语言。
- Grafana:可视化面板,通过 Loki 数据源展示日志。
5.2 用 docker-compose 从零搭建
yaml
version: "3.8"
services:
loki:
image: grafana/loki:2.9.0
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
promtail:
image: grafana/promtail:2.9.0
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/log:/var/log:ro
- ./promtail-config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
grafana:
image: grafana/grafana:10.1.0
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
5.3 Promtail 配置
yaml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: container-logs
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container'
- source_labels: ['__meta_docker_container_log_stream']
target_label: 'stream'
5.4 在 Grafana 中查看
- 打开
http://localhost:3000,添加 Loki 数据源(URL 填http://loki:3100)。 - 进入 Explore,选择 Loki 数据源。
- 用 LogQL 查询:
logql
{container="validx-demo"} |= "ERROR"
这条查询会返回 ValidX demo 容器中所有包含 ERROR 的日志行。
6. 重量方案:ELK 简介
ELK 是 Elasticsearch + Logstash + Kibana 的组合,功能强大、生态成熟,但资源占用高、运维复杂,适合日志量大、需要全文检索与复杂聚合的大型团队。
6.1 组件职责
| 组件 | 职责 |
|---|---|
| Elasticsearch | 分布式搜索与分析引擎,存储日志并建立全文索引 |
| Logstash | 数据采集与加工管道,支持丰富 filter(解析、脱敏、格式化) |
| Kibana | 可视化与探索界面,提供 Discover、Dashboard、Alerting |
6.2 数据链路
#mermaid-svg-dPCUiU3JcGdxMGBv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dPCUiU3JcGdxMGBv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dPCUiU3JcGdxMGBv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dPCUiU3JcGdxMGBv .error-icon{fill:#552222;}#mermaid-svg-dPCUiU3JcGdxMGBv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dPCUiU3JcGdxMGBv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dPCUiU3JcGdxMGBv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dPCUiU3JcGdxMGBv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dPCUiU3JcGdxMGBv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dPCUiU3JcGdxMGBv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dPCUiU3JcGdxMGBv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dPCUiU3JcGdxMGBv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dPCUiU3JcGdxMGBv .marker.cross{stroke:#333333;}#mermaid-svg-dPCUiU3JcGdxMGBv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dPCUiU3JcGdxMGBv p{margin:0;}#mermaid-svg-dPCUiU3JcGdxMGBv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dPCUiU3JcGdxMGBv .cluster-label text{fill:#333;}#mermaid-svg-dPCUiU3JcGdxMGBv .cluster-label span{color:#333;}#mermaid-svg-dPCUiU3JcGdxMGBv .cluster-label span p{background-color:transparent;}#mermaid-svg-dPCUiU3JcGdxMGBv .label text,#mermaid-svg-dPCUiU3JcGdxMGBv span{fill:#333;color:#333;}#mermaid-svg-dPCUiU3JcGdxMGBv .node rect,#mermaid-svg-dPCUiU3JcGdxMGBv .node circle,#mermaid-svg-dPCUiU3JcGdxMGBv .node ellipse,#mermaid-svg-dPCUiU3JcGdxMGBv .node polygon,#mermaid-svg-dPCUiU3JcGdxMGBv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dPCUiU3JcGdxMGBv .rough-node .label text,#mermaid-svg-dPCUiU3JcGdxMGBv .node .label text,#mermaid-svg-dPCUiU3JcGdxMGBv .image-shape .label,#mermaid-svg-dPCUiU3JcGdxMGBv .icon-shape .label{text-anchor:middle;}#mermaid-svg-dPCUiU3JcGdxMGBv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dPCUiU3JcGdxMGBv .rough-node .label,#mermaid-svg-dPCUiU3JcGdxMGBv .node .label,#mermaid-svg-dPCUiU3JcGdxMGBv .image-shape .label,#mermaid-svg-dPCUiU3JcGdxMGBv .icon-shape .label{text-align:center;}#mermaid-svg-dPCUiU3JcGdxMGBv .node.clickable{cursor:pointer;}#mermaid-svg-dPCUiU3JcGdxMGBv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dPCUiU3JcGdxMGBv .arrowheadPath{fill:#333333;}#mermaid-svg-dPCUiU3JcGdxMGBv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dPCUiU3JcGdxMGBv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dPCUiU3JcGdxMGBv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dPCUiU3JcGdxMGBv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dPCUiU3JcGdxMGBv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dPCUiU3JcGdxMGBv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dPCUiU3JcGdxMGBv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dPCUiU3JcGdxMGBv .cluster text{fill:#333;}#mermaid-svg-dPCUiU3JcGdxMGBv .cluster span{color:#333;}#mermaid-svg-dPCUiU3JcGdxMGBv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dPCUiU3JcGdxMGBv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dPCUiU3JcGdxMGBv rect.text{fill:none;stroke-width:0;}#mermaid-svg-dPCUiU3JcGdxMGBv .icon-shape,#mermaid-svg-dPCUiU3JcGdxMGBv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dPCUiU3JcGdxMGBv .icon-shape p,#mermaid-svg-dPCUiU3JcGdxMGBv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dPCUiU3JcGdxMGBv .icon-shape .label rect,#mermaid-svg-dPCUiU3JcGdxMGBv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dPCUiU3JcGdxMGBv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dPCUiU3JcGdxMGBv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dPCUiU3JcGdxMGBv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 容器 stdout/stderr
Filebeat 采集
Logstash 加工
Elasticsearch 存储
Kibana 展示
6.3 与 Loki 的对比
| 维度 | Loki | ELK |
|---|---|---|
| 索引策略 | 只索引标签,不索引内容 | 全文索引 |
| 资源占用 | 低 | 高(ES 集群吃内存) |
| 查询语言 | LogQL | Lucene / KQL |
| 适合规模 | 中小团队、K8s 原生 | 大团队、海量日志、复杂检索 |
| 部署复杂度 | 低(单二进制) | 高(多组件集群) |
选型建议:日志量在 TB 级以下、以排障和监控为主,选 Loki;需要全文检索、复杂聚合分析、已有 ES 生态,选 ELK。
7. 实战:ValidX demo 一条日志"从应用到面板"的完整链路
下面我们把 ValidX demo 改造后的日志,完整走一遍"应用 → stdout → Promtail → Loki → Grafana"的链路。
7.1 改造应用输出
ValidX demo 改造后,每次请求都会打印一条结构化日志:
python
def handle_request(req):
log("INFO", f"request received path={req.path} method={req.method}")
try:
result = process(req)
log("INFO", f"request processed status=200 latency={result.latency}ms")
except Exception as e:
log("ERROR", f"request failed error={str(e)}")
7.2 启动并观察
bash
docker compose up -d
docker logs -f validx-demo
你会看到类似输出:
2026-09-27T08:30:00+0800 INFO request received path=/api/check method=POST
2026-09-27T08:30:01+0800 INFO request processed status=200 latency=12ms
7.3 在 Grafana 面板中检索
打开 Grafana Explore,输入:
logql
{container="validx-demo"} |= "request processed"
即可看到这条日志从应用到面板的完整呈现。再配一个简单的统计面板:
logql
sum by (level) (count_over_time({container="validx-demo"}[5m]))
就能按日志级别统计 5 分钟内的日志量。
8. 坑位总结
8.1 容器内写日志文件的坏处
- 容器删除后日志丢失,无法追溯。
- 采集器需要进容器找文件,链路复杂。
- 日志写两份(应用文件 + 运行时采集),浪费 IO。
- 无法利用 Docker/K8s 原生的日志轮转与采集能力。
8.2 json-file 默认无限增长
不配置 max-size / max-file,日志文件会无限膨胀,最终写满宿主机磁盘,导致容器甚至整个节点异常。务必在 daemon.json 或 kubelet 中配置轮转。
8.3 时区对日志时间的影响
容器默认使用 UTC 时区,而业务日志往往需要本地时间。若不处理,会出现"日志时间比实际慢 8 小时"的经典问题。
解决方案:
- 应用层统一输出带时区的时间戳(推荐):
python
time.strftime('%Y-%m-%dT%H:%M:%S%z')
- 或通过环境变量注入时区:
bash
docker run -e TZ=Asia/Shanghai ...
- 或在 docker-compose 中设置:
yaml
services:
validx:
environment:
- TZ=Asia/Shanghai
Python 时区处理实战:
下面用 Python 标准库 zoneinfo 演示如何在应用启动时读取 TZ 环境变量,并据此设置日志时间戳的时区:
python
import os
import sys
import time
from datetime import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
DEFAULT_TZ = "UTC"
def resolve_timezone() -> ZoneInfo:
"""读取 TZ 环境变量并解析为 zoneinfo 对象,失败时回退到 UTC。"""
tz_name = os.environ.get("TZ", DEFAULT_TZ)
try:
return ZoneInfo(tz_name)
except ZoneInfoNotFoundError:
print(f"WARN unknown TZ={tz_name!r}, fallback to UTC", file=sys.stderr, flush=True)
return ZoneInfo(DEFAULT_TZ)
TZ = resolve_timezone()
def log(level: str, msg: str):
ts = datetime.now(TZ).strftime("%Y-%m-%dT%H:%M:%S%z")
line = f"{ts} {level} {msg}"
print(line, flush=True)
if level == "ERROR":
print(line, file=sys.stderr, flush=True)
# 启动时打印当前生效时区,便于排查
log("INFO", f"timezone={TZ.key}")
# 模拟一条业务日志
log("INFO", "request processed status=200 latency=12ms")
运行结果对比:
以 TZ=UTC 启动:
bash
TZ=UTC python app.py
输出:
2026-09-27T00:30:00+0000 INFO timezone=UTC
2026-09-27T00:30:00+0000 INFO request processed status=200 latency=12ms
以 TZ=Asia/Shanghai 启动:
bash
TZ=Asia/Shanghai python app.py
输出:
2026-09-27T08:30:00+0800 INFO timezone=Asia/Shanghai
2026-09-27T08:30:00+0800 INFO request processed status=200 latency=12ms
可以看到,同一时刻的日志,UTC 显示 00:30:00+0000,Asia/Shanghai 显示 08:30:00+0800,相差正好 8 小时。%z 后缀(+0000 / +0800)把时区偏移写进了日志,采集端无需再猜时区,直接按偏移解析即可。
提示:
zoneinfo依赖系统时区数据库(Linux 一般自带)。若在精简镜像中报ZoneInfoNotFoundError,可安装tzdata包:pip install tzdata。注意:
TZ环境变量对 Java 的SimpleDateFormat不一定生效,Java 应用建议显式设置-Duser.timezone=Asia/Shanghai。
9. 总结
容器日志管理的关键,是把"日志的产生"与"日志的收集"解耦:应用只负责往 stdout/stderr 打日志,收集与存储交给平台。本文从日志驱动矩阵讲起,明确了 stdout 规范与轮转配置,再分别用 Loki 和 ELK 两条路线演示了完整链路,最后总结了三个高频坑位。
建议落地顺序:
- 先统一应用日志规范,全部走 stdout/stderr。
- 配置日志轮转,避免磁盘被写满。
- 按团队规模选择 Loki 或 ELK,搭建集中观测平台。
- 统一时区处理,保证日志时间可读。
日志链路通了,排障效率会提升一个量级。