日志体系
本文范围
做日志体系,先不要一上来就问“装 ELK 还是 Loki”。生产环境里最怕的不是没有酷炫平台,而是出了问题以后只剩一句话:“我去机器上翻翻日志。”
日志真正要解决的是三件事:
- 平时能看清服务在干什么。
- 出故障时能快速定位是哪一层出问题。
- 日志自己不能把磁盘打满,也不能把密码、token、手机号这些敏感信息打出去。
所以这篇文章不按“概念百科”来写。我们按真实落地顺序走:先规划目录,再写应用日志,再看 Nginx、Docker、systemd,然后做切割、采集、检索、保留,最后专门讲日志爆盘怎么排查。
本文不展开 SkyWalking 链路追踪、Prometheus 指标告警、K8S 日志采集 Operator、完整 SIEM 平台和业务审计系统设计。它们会在边界里出现,但这篇只讲部署运维视角下“日志怎么落盘、怎么查、怎么保留、怎么不把生产拖垮”。
前置条件
这里先约定一下环境。命令以 Linux 为主,示例用 Ubuntu / RHEL 系都能理解的写法。应用以 Java / Spring Boot 为例,入口层用 Nginx,容器用 Docker 或 Docker Compose。K8S、SkyWalking、Prometheus 这类东西不在这里展开,后面单独讲。
还有一个版本提醒。截至 2026-07-09,Filebeat 老的 log input 已经废弃并在 9.0 中禁用,新配置优先用 filestream。Grafana Promtail 已在 2026-03-02 EOL,新接入 Loki 优先看 Grafana Alloy 或 Fluent Bit。旧教程里的配置能看思路,但不要直接复制到新生产环境。
正式动手前,至少准备好这些条件:
- 有只读排障账号和明确的提权流程,不能让采集器用 root 到处读。
- 已约定日志根目录、服务名、环境名、traceId 字段和敏感字段脱敏规则。
- 已确认本地磁盘、集中日志平台和归档存储的保留周期。
- 已准备内网软件源、离线包或企业制品库,避免生产现场临时访问外网。
- 已明确日志属于排障证据,删除、截断、归档都要能留下操作记录。
先把日志分清楚
很多团队一开始日志混乱,不是工具没选好,而是没有分清“这条日志到底给谁看”。应用排障、入口访问、系统启动、容器 stdout、审计留痕,它们不是一类东西。
一般生产环境至少有这些日志:
| 日志类型 | 常见位置 | 主要用途 |
|---|---|---|
| 应用运行日志 | /data/logs/order-service/app.log | 看业务流程、外部调用、普通异常 |
| 应用错误日志 | /data/logs/order-service/error.log | 快速聚焦 ERROR 和异常栈 |
| Nginx access log | /var/log/nginx/access.log | 看请求、状态码、耗时、客户端 IP |
| Nginx error log | /var/log/nginx/error.log | 查 502、504、证书、权限、上游连接 |
| Docker stdout 日志 | /var/lib/docker/containers/... | docker logs 能看到的内容 |
| systemd journal | journalctl | 查服务启动失败、OOM、daemon 异常 |
| syslog/messages | /var/log/syslog 或 /var/log/messages | 查系统级事件和传统服务日志 |
| 审计/操作日志 | 数据库或独立审计文件 | 查谁在什么时候做了什么 |
这里要注意,docker logs 看到的只是容器标准输出和标准错误。如果你的 Java 应用只写 /logs/app.log,不输出到控制台,那么 docker logs 可能是空的。反过来,如果应用只往控制台疯狂输出,而 Docker 日志驱动没做限制,宿主机的 *-json.log 就可能把磁盘吃完。
日志从产生到检索的数据流图
日志体系要先画清楚数据怎么走。否则你看到“平台里没有日志”时,很容易在 Kibana 或 Grafana 里反复刷新,却没有检查日志是不是根本没落盘、没轮转、没被采集,或者已经被采集器丢掉了。
图里每一段都是一个排障断点:应用没有打印 traceId,后面就很难串链路;本地文件没有切割,磁盘会先爆;采集器没有 multiline,Java 异常栈会散成几十条;输出端标签设计不对,检索成本会急剧升高。
验证时不要只看最终平台,先从源头往后走:
grep 'demo-trace-001' /data/logs/order-service/app.log
logrotate -d /etc/logrotate.d/order-service
journalctl --disk-usage
docker inspect --format '{{.HostConfig.LogConfig.Type}} {{.LogPath}}' order-service
filebeat test config -c filebeat.yml
filebeat test output -c filebeat.yml常见坑有四类。第一,应用、Nginx、Docker、journal 被重复采集,导致同一条日志进平台两三次。第二,把 traceId、userId、orderNo 这类高基数字段放成 Loki 标签,查询看似方便,存储和索引压力会很快失控。第三,继续沿用 Filebeat 旧 log input,新版本已经要求迁移到 filestream。第四,Promtail 已进入生命周期尾声,新建 Loki 链路要优先评估 Alloy 或其他仍在维护的采集器。
生产约束也要写在图旁边:敏感字段尽量在应用侧脱敏;采集器必须有背压和磁盘缓冲预案;日志保留周期要区分在线检索、冷归档和审计留存;任何删除、截断、归档动作都要留下操作记录。
先建一个像样的日志目录
别让每个服务自己随便找地方写日志。目录一乱,后面 logrotate、采集器、排障脚本都会跟着乱。
一般可以这样规划:
sudo mkdir -p /data/logs/order-service
sudo mkdir -p /data/logs/payment-service
sudo mkdir -p /data/logs/nginx
sudo mkdir -p /data/logs/archive这几条命令解决的是目录标准化问题。以后你找业务日志,就去 /data/logs/服务名/;找入口日志,就去 /data/logs/nginx/ 或 /var/log/nginx/。
如果应用用普通用户启动,还要把权限给对:
sudo chown -R app:app /data/logs/order-service
sudo chmod 750 /data/logs/order-service验证一下:
sudo -u app test -w /data/logs/order-service && echo "日志目录可写"看到 日志目录可写,说明应用用户能写这个目录。如果没有输出,先别改应用,先查权限:
namei -l /data/logs/order-servicenamei -l 会把路径上的每一级目录权限都列出来。很多时候不是最后一级目录错了,而是上层目录没有执行权限。
如果服务跑在 Docker 里,一般把容器里的 /logs 挂到宿主机:
services:
order-service:
image: registry.example.com/order-service:1.0.0
volumes:
- /data/logs/order-service:/logs这样应用写 /logs/app.log,宿主机就能看到 /data/logs/order-service/app.log。容器删除重建,日志不会跟着丢;采集器也不用进入容器读文件。
应用日志先按能排障来写
应用日志不是越多越好。真正有用的日志,应该能回答这几个问题:
- 谁发起的请求。
- 请求走到了哪个服务、哪个方法。
- 当前处理到哪一步。
- 调用了哪个外部依赖。
- 耗时是多少。
- 失败时异常栈是什么。
- 能不能用一个
traceId把这些日志串起来。
日志级别别乱用
先把级别用对:
| 级别 | 生产里怎么用 |
|---|---|
TRACE | 默认关闭,只在非常细的问题追踪时短期开 |
DEBUG | 默认关闭,排查某个包时短期开 |
INFO | 正常生产主级别,记录关键流程 |
WARN | 有风险但还能继续,比如重试、降级、慢调用 |
ERROR | 已经失败或影响用户,要带异常栈 |
这里要注意,用户密码输错、库存不足、参数校验不通过,这些如果是业务预期结果,不要动不动打 ERROR。否则告警系统会被正常业务噪声淹没。
Spring Boot 先用配置跑起来
如果日志需求还不复杂,可以先用 Spring Boot 配置文件:
spring:
application:
name: order-service
logging:
level:
root: INFO
com.example.order: INFO
file:
name: /data/logs/order-service/app.log
pattern:
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{traceId:-}] %logger{36} - %msg%n"
console: "%d{HH:mm:ss.SSS} %-5level [%thread] [%X{traceId:-}] %logger{36} - %msg%n"
logback:
rollingpolicy:
file-name-pattern: /data/logs/order-service/app.%d{yyyy-MM-dd}.%i.log.gz
max-file-size: 100MB
max-history: 14
total-size-cap: 5GB这个配置做了几件事:
- 根日志级别是
INFO,生产不会默认打DEBUG。 - 日志写到
/data/logs/order-service/app.log。 - 日志格式里带线程名、
traceId、logger 名称。 - 单个文件最大 100MB。
- 最多保留 14 个周期。
- 总容量最多 5GB。
改完以后重启服务:
sudo systemctl restart order-service看日志有没有写出来:
tail -f /data/logs/order-service/app.log正常应该能看到类似这样的内容:
2026-07-09 10:15:30.123 INFO [http-nio-8080-exec-1] [8f31c...] com.example.OrderController - create order success如果文件没生成,按这个顺序查:
systemctl status order-service --no-pager
journalctl -u order-service -n 200 --no-pager
namei -l /data/logs/order-service第一条看服务有没有起来;第二条看 systemd 里有没有启动错误;第三条看目录权限。
复杂一点就用 logback-spring.xml
生产里一般会把普通日志和错误日志分开。普通日志用于排流程,错误日志用于告警和快速定位。
可以放一个 logback-spring.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="APP_NAME" value="order-service"/>
<property name="LOG_PATH" value="/data/logs/${APP_NAME}"/>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{traceId:-}] %logger{36} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="APP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/app.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>14</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="APP_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>这里有几个地方要特别注意。
第一,totalSizeCap 一定要配。只配 maxHistory,遇到单日日志暴涨时还是可能爆盘。
第二,ERROR_FILE 不要只保留一两天。线上复盘经常隔天才开始,错误日志保留 30 天是比较常见的起点。
第三,容器里用文件日志时,日志目录要挂出来。否则日志写在容器可写层里,容器删了日志没了,磁盘占用还不好查。
traceId 要从入口一路带下去
没有 traceId 的时候,排查一次用户问题很痛苦。你可能先拿用户 ID 查订单服务,结果查出几万条历史日志;再拿订单号查支付服务;再猜 MQ 消息在哪个时间段消费。这个过程非常靠运气。
有 traceId 后,排查方式应该变成这样:
grep 'demo-trace-001' /data/logs/*/*.log或者在日志平台里查:
traceId: "demo-trace-001"最简单的做法是在入口过滤器里生成或接收 X-Trace-Id,然后放到 MDC:
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
@Component
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_ID = "traceId";
private static final String TRACE_HEADER = "X-Trace-Id";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String traceId = request.getHeader(TRACE_HEADER);
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put(TRACE_ID, traceId);
response.setHeader(TRACE_HEADER, traceId);
try {
filterChain.doFilter(request, response);
} finally {
MDC.remove(TRACE_ID);
}
}
}验证一下:
curl -H 'X-Trace-Id: demo-trace-001' http://127.0.0.1:8080/actuator/health
grep 'demo-trace-001' /data/logs/order-service/app.log如果 grep 能搜到,说明入口到日志这条链路通了。
这里要注意,线程池、异步任务、MQ 消费会丢 MDC。比如主线程里有 traceId,丢给线程池以后,新线程不一定有。MQ 也一样,生产者要把 traceId 放到消息 Header,消费者取出来以后再放回 MDC,并且消费结束后清理。
日志脱敏要在应用侧就做
有些人觉得脱敏可以交给日志平台处理,这个思路不稳。日志一旦落盘、被采集、被索引、被备份,后面想删干净非常麻烦。
这些内容不要原样打日志:
- 密码、验证码、token、session、cookie。
- 身份证、手机号、银行卡、邮箱。
Authorization请求头。- 第三方接口签名、支付回调密钥。
- 大请求体、大响应体、文件内容。
不要这样写:
log.info("login request={}", request);
log.info("Authorization={}", authorization);
log.error("pay callback body={}", body);可以这样写:
log.info("login userId={}, mobile={}", userId, maskMobile(mobile));
log.info("pay callback orderNo={}, amount={}, channel={}", orderNo, amount, channel);
log.warn("third api failed traceId={}, code={}, costMs={}", traceId, code, costMs);如果确实要临时查看请求体,也要短期开、按接口开、打完马上关,不要把全站请求体长期打进生产日志。
Nginx 日志先看 access,再看 error
Nginx 是入口层,很多问题应用根本不知道。比如静态资源 404、证书错误、上游连接不上、请求还没转到后端就被拒绝,这些都要看 Nginx。
access_log 建议用 JSON
在 http 块里加:
map $http_x_trace_id $trace_id {
default $http_x_trace_id;
"" $request_id;
}
log_format access_json escape=json
'{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"x_forwarded_for":"$http_x_forwarded_for",'
'"request_id":"$request_id",'
'"trace_id":"$trace_id",'
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_response_time":"$upstream_response_time",'
'"referer":"$http_referer",'
'"user_agent":"$http_user_agent"'
'}';然后开启日志:
access_log /var/log/nginx/access.log access_json buffer=64k flush=5s;
error_log /var/log/nginx/error.log warn;反向代理时把 traceId 往后传:
location /api/ {
proxy_set_header X-Trace-Id $trace_id;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://backend;
}改完先检查配置:
sudo nginx -t看到 syntax is ok 和 test is successful,再 reload:
sudo systemctl reload nginx访问一下:
curl -H 'X-Trace-Id: demo-nginx-001' http://127.0.0.1/api/health
tail -n 5 /var/log/nginx/access.log
grep 'demo-nginx-001' /var/log/nginx/access.log如果日志里能看到 demo-nginx-001,说明入口层已经能记录并透传 traceId。
access_log 怎么看
看慢请求,不要只看应用日志。先看:
tail -n 100 /var/log/nginx/access.log重点看三个字段:
status:是不是 4xx / 5xx。request_time:整个请求在 Nginx 侧耗时。upstream_response_time:后端服务响应耗时。
如果 request_time 很高,upstream_response_time 也很高,通常是后端慢。如果 upstream_response_time 是空的或 -,说明可能没打到后端,继续查路由、静态资源、权限或 Nginx 自己的问题。
查 5xx:
grep '"status":5' /var/log/nginx/access.log | tail -50查某个 traceId:
grep 'demo-nginx-001' /var/log/nginx/access.logerror_log 查入口层问题
常用查法:
grep -E "connect\\(\\) failed|upstream timed out|permission denied|no live upstreams|SSL" /var/log/nginx/error.log常见现象可以这样判断:
connect() failed (111: Connection refused):后端端口没起来,或服务正在重启。upstream timed out:后端处理太慢,继续查应用日志和数据库。permission denied:静态文件权限、SELinux 或目录权限问题。no live upstreams:upstream 都不可用了。SSL_do_handshake:证书链、协议或客户端兼容性问题。
这里要注意,502/504 不要一上来就改 Nginx 参数。先看 error log。很多时候真正原因是后端服务挂了、端口不通、线程池满了、数据库慢了。
Nginx 日志切割别只 mv 文件
Nginx 日志越跑越大,肯定要切割。这里有个坑:不能只执行 mv access.log access.log.1。因为 Nginx worker 还拿着旧文件句柄,移动以后它还是继续往旧文件写。
正确做法是切割后让 Nginx 重新打开日志文件。推荐用 logrotate:
/var/log/nginx/*.log {
daily
rotate 14
missingok
notifempty
compress
delaycompress
sharedscripts
postrotate
if [ -s /run/nginx.pid ]; then
kill -USR1 `cat /run/nginx.pid`
fi
endscript
}这里的关键是 postrotate 里的 kill -USR1。它告诉 Nginx 重新打开日志文件。
先确认 pid 文件在哪里:
sudo nginx -T 2>/dev/null | grep -E 'pid|error_log|access_log'
ps -ef | grep '[n]ginx: master'测试 logrotate:
sudo logrotate -d /etc/logrotate.d/nginx-d 是调试模式,只演练,不真的切。看起来没问题后再强制跑一次:
sudo logrotate -f /etc/logrotate.d/nginx
ls -lh /var/log/nginx如果切割后新 access.log 不写,优先查三件事:
- pid 路径是不是错了。
postrotate有没有执行。- 新日志文件权限是不是 Nginx 用户能写。
Docker 日志要分清 stdout 和文件日志
启动容器后,很多人第一反应是:
docker logs app-a这条命令看的是容器主进程的标准输出和标准错误。它适合查容器启动失败、应用启动日志、短期实时日志。
常用写法:
docker logs --tail 200 -t app-a
docker logs -f --since "30m" app-a
docker logs -f app-a 2>&1 | grep -iE "error|exception|timeout"参数解释一下:
--tail 200:只看最后 200 行,避免全量刷屏。-t:带 Docker 记录的时间戳。-f:持续跟踪。--since "30m":只看最近 30 分钟。
如果你的应用日志写在容器文件里,就这样看:
docker exec app-a tail -n 200 /logs/app.log
docker exec app-a tail -f /logs/error.log
docker cp app-a:/logs/error.log ./error.log这里要注意,docker exec 只是排障手段,不建议让采集器天天进容器读文件。更好的方式是把 /logs 挂到宿主机,然后采集宿主机目录。
Docker 日志轮转一定要配
Docker 默认日志驱动经常是 json-file。如果不限制大小,容器 stdout 日志可能无限增长。
先看当前日志驱动:
docker info --format '{{.LoggingDriver}}'再看某个容器的日志文件:
docker inspect --format '{{.HostConfig.LogConfig.Type}}' app-a
docker inspect --format '{{.LogPath}}' app-a
sudo ls -lh "$(docker inspect --format '{{.LogPath}}' app-a)"如果看到 json-file,而且日志文件已经几个 G,就要治理。
编辑 /etc/docker/daemon.json,推荐普通 Docker 主机用 local:
{
"log-driver": "local",
"log-opts": {
"max-size": "100m",
"max-file": "5"
}
}如果团队必须继续用 json-file,也要加轮转:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5"
}
}注意,log-opts 里的值要写字符串,不要写数字。
重启 Docker:
sudo systemctl restart docker
docker info --format '{{.LoggingDriver}}'这里有一个非常容易踩的坑:daemon 级配置一般只影响新建容器。老容器要重建,不是简单 restart。
Docker Compose 可以这样写:
services:
order-service:
image: registry.example.com/order-service:1.0.0
logging:
driver: local
options:
max-size: "100m"
max-file: "5"重建:
docker compose down
docker compose up -d验证:
docker inspect --format '{{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}' order-service看到 local 或者看到 max-size/max-file,说明容器日志策略已经生效。
Docker 日志爆了怎么止血
先定位,不要上来就删:
df -h
sudo du -xhd1 /var/lib/docker | sort -h
docker system df
docker inspect --format '{{.Name}} {{.LogPath}}' $(docker ps -q)如果确定是某个容器 stdout 日志太大,可以应急清空:
LOG_FILE=$(docker inspect --format '{{.LogPath}}' app-a)
sudo truncate -s 0 "$LOG_FILE"这条命令解决的是“先把空间腾出来”。它不会删除文件,只是把文件截断为 0,所以比直接 rm 安全。
不要直接删 *-json.log。进程还持有文件句柄时,你删了文件,磁盘空间可能不释放,Docker 读日志也可能异常。
止血以后要回头查根因:
docker logs --tail 200 -t app-a
grep -E "DEBUG|TRACE" /data/logs/order-service/app.log | head
grep -iE "exception|timeout|retry|failed" /data/logs/order-service/app.log | head常见原因就是生产开了 DEBUG、异常在循环里疯狂打印、依赖挂了导致重试风暴、或者接口把大请求体完整打出来了。
systemd 服务日志先看 journalctl
如果你用 systemctl start order-service 启动服务,服务没起来,应用日志里又没内容,这时候不要急着怀疑 logback。应用可能还没启动到写日志那一步。
先看 systemd:
systemctl status order-service --no-pager
journalctl -u order-service -n 200 --no-pager实时跟踪:
journalctl -u order-service -f按时间窗口查:
journalctl -u order-service --since "2026-07-09 10:00:00" --until "2026-07-09 10:30:00"查本次启动以来的错误:
journalctl -p err..alert -b --no-pager这里一般能看到这些问题:
- 配置文件路径错。
- 端口被占用。
- Java 路径不存在。
- 用户没有权限。
- systemd unit 写错。
- 服务启动后马上退出。
journal 自己也要限容量
看 journal 占用:
journalctl --disk-usage如果要持久化 journal:
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald配置 /etc/systemd/journald.conf:
[Journal]
Storage=persistent
SystemMaxUse=2G
SystemKeepFree=5G
MaxRetentionSec=14day重启:
sudo systemctl restart systemd-journald
journalctl --disk-usage临时清理:
sudo journalctl --vacuum-time=14d
sudo journalctl --vacuum-size=2G如果机器没有容量限制,journal 也可能成为爆盘源头。
syslog 和 messages 怎么看
传统 Linux 服务还会写 /var/log/syslog 或 /var/log/messages。Ubuntu / Debian 常见是 /var/log/syslog,RHEL 系常见是 /var/log/messages。
实时看:
sudo tail -f /var/log/syslog
sudo tail -f /var/log/messages查错误:
sudo grep -iE "error|failed|denied|oom|killed" /var/log/messages如果启用了 rsyslog,要转发到集中日志服务器,常见写法是:
*.* @@loghost.example.internal:514一个 @ 通常是 UDP,两个 @@ 是 TCP。生产环境一般优先 TCP,并且要考虑网络中断、队列、TLS、接收端容量,不要只写一行转发就算完事。
logrotate 适合管普通文件日志
不是所有日志都由应用自己滚动。比如脚本日志、旧服务日志、catalina.out、Nginx access log,都经常用 logrotate 管。
新建一个配置,比如 /etc/logrotate.d/order-service:
/data/logs/order-service/*.log {
daily
rotate 14
size 200M
missingok
notifempty
compress
delaycompress
copytruncate
create 0640 app app
}解释一下:
daily:每天检查。rotate 14:保留 14 份。size 200M:达到 200MB 也切。missingok:文件不存在不报错。notifempty:空文件不切。compress:压缩旧日志。delaycompress:延后一轮压缩,避免进程还在写旧文件。copytruncate:复制后截断原文件,适合不会重开日志的进程。
这里要注意,copytruncate 有小概率丢日志。因为复制和截断之间,进程可能还在写。能让进程重新打开日志的场景,优先用 postrotate 发信号;不能重开的旧服务,再用 copytruncate。
测试配置:
sudo logrotate -d /etc/logrotate.d/order-service真正强制执行:
sudo logrotate -f /etc/logrotate.d/order-service
ls -lh /data/logs/order-service如果空间没有释放,查是不是有 deleted 文件还被进程占着:
sudo lsof +L1看到某个 deleted 日志还被 Java 或 Nginx 占着,就需要 reload 或重启对应进程。
日志采集先从简单链路做起
刚开始别急着做复杂平台。先让一台机器的一份日志稳定进入检索系统。
最小链路可以是:
应用文件日志 -> Filebeat / Fluent Bit / Alloy -> Elasticsearch 或 Loki -> Kibana / GrafanaELK、EFK、Loki 怎么选
简单说:
- 要强全文检索、复杂过滤、审计分析,选 Elasticsearch / OpenSearch 体系。
- 容器日志多、希望采集端轻一点,常见是 EFK,也就是 Elasticsearch + Fluent Bit/Fluentd + Kibana。
- 已经有 Grafana 体系,查询主要按服务、环境、容器、主机标签定位,日志量又大,可以考虑 Loki。
这里要注意,Loki 不是便宜版 Elasticsearch。Loki 适合按标签定位日志流,然后在日志内容里过滤。标签要低基数,比如 service、env、host、container。不要把 userId、orderNo、traceId 这种高基数字段随手放进标签。
Filebeat 用 filestream 采应用日志
新配置建议用 filestream。示例:
filebeat.inputs:
- type: filestream
id: order-service
enabled: true
paths:
- /data/logs/order-service/app.log
- /data/logs/order-service/error.log
fields_under_root: true
fields:
service: order-service
env: prod
parsers:
- multiline:
type: pattern
pattern: '^\d{4}-\d{2}-\d{2} '
negate: true
match: after
output.logstash:
hosts: ["logstash01.example.internal:5044"]这个配置做了几件事:
- 采集
order-service的应用日志和错误日志。 - 给每条日志补上
service和env字段。 - 把 Java 异常栈合并成一条日志。
- 输出到 Logstash 的 5044 端口。
测试配置:
filebeat test config -c filebeat.yml
filebeat test output -c filebeat.yml
sudo systemctl restart filebeat
sudo journalctl -u filebeat -f如果 test output 失败,先不要看 Filebeat 采集规则,先查网络:
nc -vz logstash01.example.internal 5044如果 Filebeat 运行正常但日志平台没有数据,继续查:
namei -l /data/logs/order-service/app.log
sudo journalctl -u filebeat -n 200 --no-pager常见问题是采集器用户没有读权限,或者路径写错。
Logstash 先做最小接收
Logstash 配置不要一上来写复杂 grok。先把数据接住、打印出来。
最小配置:
input {
beats {
port => 5044
}
}
output {
stdout {
codec => rubydebug
}
}测试配置:
bin/logstash -f config/filebeat-console.conf --config.test_and_exit看到 Config Validation Result: OK,说明配置语法没问题。
再启动:
bin/logstash -f config/filebeat-console.conf等控制台能看到 Filebeat 发来的日志,再输出到 Elasticsearch:
input {
beats {
port => 5044
}
}
filter {
if [message] =~ "^\{" {
json {
source => "message"
skip_on_invalid_json => true
}
}
}
output {
elasticsearch {
hosts => ["http://es01.example.internal:9200"]
index => "logs-%{[env]}-%{[service]}-%{+YYYY.MM.dd}"
}
}这里先只做 JSON 解析。普通文本日志可以后面再加 grok。不要第一天就把所有日志格式都塞进一个复杂 pipeline,出了问题很难定位。
检索时要围绕 traceId 和时间窗口
排障不要上来就搜 error。生产日志量一大,error 会搜出一堆无关内容。
推荐顺序:
- 先确定时间窗口。
- 再确定服务。
- 如果有
traceId,优先用traceId。 - 没有
traceId,再用订单号、用户 ID、接口路径、状态码组合查。
常用条件类似:
service: "order-service"
env: "prod"
level: "ERROR"
traceId: "demo-trace-001"
message: "TimeoutException"Nginx 入口问题可以查:
status >= 500
upstream_response_time > 3
uri: "/api/orders"这里要注意,traceId 不是只能给链路追踪系统用。哪怕没有 SkyWalking,只要日志里能带 traceId,排障效率也会明显提升。
中国大陆、内网、离线环境怎么落地
日志组件很多都依赖国外站点、Docker 镜像仓库、插件仓库。生产部署时不要默认服务器能直接访问 Docker Hub、GitHub、海外 apt/yum 源。
中国大陆公网
一般做法:
- 官方源可达时,用官方源和官方镜像。
- 官方源不可达时,用云厂商仓库或企业缓存仓库。
- 不把某个第三方镜像地址写死成唯一方案。
- 关键镜像拉到企业私有仓库,再由服务器从私有仓库拉取。
验证网络:
curl -I https://example.com
docker pull registry.example.com/observability/filebeat:9.0.0
dnf repolist
apt-cache policy filebeat这里的重点不是“哪个镜像源最快”,而是你的方案能不能长期复现。镜像源会变,私有仓库和版本清单才是稳定交付的基础。
企业内网
内网推荐这样走:
外网下载机
-> 下载包和镜像
-> 校验 checksum 或签名
-> 推送到 Harbor / 内部制品库 / 内部 yum 或 apt 仓库
-> 内网服务器安装日志系统至少准备这些东西:
- Filebeat / Fluent Bit / Grafana Alloy 安装包或镜像。
- Logstash、Elasticsearch、Kibana 或 Loki、Grafana 镜像。
- 配置模板。
- systemd unit 或 Compose 文件。
- 证书、账号、密码。
- 保留策略。
- 回滚版本。
完全离线
外网机准备镜像:
docker pull docker.elastic.co/beats/filebeat:9.0.0
docker pull docker.elastic.co/logstash/logstash:9.0.0
docker pull docker.elastic.co/elasticsearch/elasticsearch:9.0.0
docker pull grafana/loki:3.5.0
docker pull grafana/alloy:latest
docker save -o logging-images.tar \
docker.elastic.co/beats/filebeat:9.0.0 \
docker.elastic.co/logstash/logstash:9.0.0 \
docker.elastic.co/elasticsearch/elasticsearch:9.0.0 \
grafana/loki:3.5.0 \
grafana/alloy:latest
sha256sum logging-images.tar > logging-images.tar.sha256离线机导入:
sha256sum -c logging-images.tar.sha256
docker load -i logging-images.tar
docker images | grep -E 'filebeat|logstash|elasticsearch|loki|alloy'看到对应镜像,说明镜像包导入成功。
离线交付不要只带镜像包。还要带版本清单、配置文件、启动脚本、校验文件、验证命令和回滚包。否则现场一旦起不来,只能靠猜。
日志保留周期怎么定
日志保留不是越久越好。保留越久,磁盘、索引、备份、权限治理成本越高。
一般可以先按这个基线:
| 日志 | 本地保留 | 集中检索保留 | 说明 |
|---|---|---|---|
| 应用 all 日志 | 7-14 天 | 15-30 天 | 日常排障够用 |
| 应用 error 日志 | 14-30 天 | 30-90 天 | 复盘和告警常用 |
| Nginx access | 7-30 天 | 15-90 天 | 看业务和审计要求 |
| Nginx error | 14-30 天 | 30-90 天 | 入口层故障复盘 |
| Docker stdout | 3-7 天 | 采集后可短保留 | 不建议长期留本地 |
| journal/syslog | 7-14 天 | 15-30 天 | 系统排障 |
| 审计日志 | 按制度 | 按制度 | 通常更久 |
粗略估算容量:
每日日志量 GB = 平均单行字节数 * 每秒行数 * 86400 / 1024 / 1024 / 1024比如平均 500 字节一行,高峰折算 2000 行每秒:
500 * 2000 * 86400 / 1024^3 = 约 80.5 GB/天保留 30 天,就是 2.4TB 级别,还没算副本、索引、压缩和增长。看到这个数字,你就知道为什么生产不能随便打 DEBUG 了。
控制日志量的办法:
- 生产默认
INFO。 - 高频接口不打印完整请求体和响应体。
- 大对象只打印 ID、数量、摘要、耗时。
- 循环里不要打大量日志。
- 慢调用单独打,设置阈值。
- 健康检查日志可以单独处理或降低保留。
日志爆盘排查从 df 开始
日志爆盘不一定直接报“日志满了”。它可能表现为数据库写入失败、容器无法启动、服务启动失败、Nginx 502、应用报锁等待。
第一步,先看分区:
df -h
df -ihdf -h 看磁盘空间,df -ih 看 inode。小文件太多时,空间还在,inode 可能先用完。
第二步,找大目录:
sudo du -xhd1 / | sort -h
sudo du -xhd1 /var | sort -h
sudo du -xhd1 /data | sort -h
sudo du -xhd1 /var/lib/docker | sort -h这里用 -x,表示不跨文件系统,避免扫到其它挂载盘导致判断混乱。
第三步,找大日志文件:
sudo find /var/log /data/logs /var/lib/docker/containers \
-type f -size +500M -printf '%s %p\n' 2>/dev/null \
| sort -n | tail -20如果怀疑 Docker:
docker system df
docker inspect --format '{{.Name}} {{.LogPath}}' $(docker ps -q)第四步,查 deleted 文件:
sudo lsof +L1如果看到某个很大的 deleted 文件还被进程占着,说明你删了文件但空间没释放。需要 reload 或重启对应进程。
爆盘时怎么止血
按风险从低到高处理:
- 删除或压缩已经轮转的旧日志。
- 强制执行 logrotate。
- 调低应用日志级别。
- 清空单个 Docker 容器 stdout 日志。
- 清理 journal。
- 扩容磁盘或迁移日志目录。
- 重启持有 deleted 文件的进程。
常用命令:
sudo logrotate -f /etc/logrotate.conf
LOG_FILE=$(docker inspect --format '{{.LogPath}}' app-a)
sudo truncate -s 0 "$LOG_FILE"
sudo journalctl --vacuum-size=2G这里要注意,不要一上来就:
rm -rf /var/lib/docker
rm -rf /var/log这不是排障,这是扩大事故。
止血以后一定要做根因治理:
- Docker logging driver 是否配置了
max-size/max-file。 - 应用日志是否配置了
maxHistory/totalSizeCap。 - Nginx logrotate 是否真的生效。
- journal 是否设置了
SystemMaxUse。 - 采集器是否重复采集归档目录。
- 是否有接口在打印大请求体。
- 是否有依赖故障造成异常风暴。
常见问题现场排查
docker logs 没有日志
先看日志驱动:
docker inspect --format '{{.HostConfig.LogConfig.Type}}' app-a再看容器里有没有文件日志:
docker exec app-a ls -lh /logs
docker exec app-a tail -n 100 /logs/app.log如果 /logs/app.log 有内容,docker logs 没内容,说明应用只写文件,没有输出到 stdout。这不是 Docker 坏了。
Nginx 切割后不写新日志
先看 pid 和文件句柄:
ps -ef | grep '[n]ginx: master'
sudo lsof | grep access.log | grep nginx
sudo logrotate -d /etc/logrotate.d/nginx一般是 pid 路径错、USR1 没发出去,或者新文件权限不对。
应用日志突然暴涨
先看是不是 DEBUG:
grep -E "DEBUG|TRACE" /data/logs/order-service/app.log | head再看异常是不是重复:
grep -iE "exception|timeout|retry|failed" /data/logs/order-service/app.log | head按分钟统计一下日志量:
awk '{print $1" "$2}' /data/logs/order-service/app.log | cut -c1-16 | sort | uniq -c | sort -nr | head如果某一分钟突然暴涨,结合发布时间、流量、依赖状态查。
Filebeat 不采集
先测配置:
filebeat test config -c /etc/filebeat/filebeat.yml
filebeat test output -c /etc/filebeat/filebeat.yml看服务日志:
sudo systemctl status filebeat --no-pager
sudo journalctl -u filebeat -n 200 --no-pager看权限:
namei -l /data/logs/order-service/app.log常见原因就几个:路径错、权限不够、输出端不通、multiline 写错、采集器 registry 记录了旧 offset。
traceId 搜不到完整链路
按链路查:
- Nginx 有没有记录
trace_id。 - Nginx 有没有把
X-Trace-Id传给后端。 - 应用入口有没有放 MDC。
- 日志 pattern 有没有
%X{traceId}。 - 下游 HTTP 调用有没有继续传 Header。
- MQ 生产和消费有没有传递 traceId。
- 异步线程有没有复制上下文。
本机先 grep:
grep 'demo-trace-001' /var/log/nginx/access.log
grep 'demo-trace-001' /data/logs/*/*.log如果 Nginx 有、应用没有,问题在入口到应用之间。如果应用入口有、下游没有,问题在 HTTP 客户端、RPC 或 MQ 传递。
落地工程深水区
日志体系真正难的地方,不是把日志写出来,而是长期跑下去不失控。
- 容量上限要提前算。先用
du -sh /data/logs/*看每个服务一天写多少,再按“本地保留天数 + 峰值放大倍数 + 压缩前空间”算磁盘。日志盘不要和数据库数据盘、Docker data-root 混在一起。 - 权限不要图省事。应用只写自己的目录,采集器只读必要路径,Nginx、Docker、journal 的读取权限要通过用户组或 ACL 明确授权。临时给
chmod 777,后面很难收回来。 - 保留周期要分层。热日志用于排障,集中索引用于检索,冷归档用于审计。不要把所有日志都长期放在昂贵热存储里,也不要把合规日志只留在本机。
- 脱敏要在源头做。密码、token、cookie、身份证、手机号一旦落盘,再靠后端平台清洗已经晚了。上线前用
grep -iE "password|token|cookie|authorization"抽查真实文件。 - 采集链路要有自监控。采集延迟、输出失败、队列积压、日志平台写入拒绝都要告警。只监控业务服务,不监控采集器,等于证据链随时会断。
- 切割和采集要一起演练。
logrotate -d只能看计划,正式上线前还要验证切割后采集器不会漏文件、重复读或把异常栈拆成多条。 - 删除动作要留证据。生产止血可以压缩、转移、截断非关键日志,但要先记录
df -h、du -xhd1、lsof +L1和处理时间线,避免复盘时只剩“已经清过了”。 - 团队要统一字段口径。
traceId、requestId、userId、orderId、env、service字段混乱时,平台再强也查不准。字段规范要进开发模板和发布检查清单。
上线前检查清单
上线前别只看服务能不能启动。日志至少要检查这些:
- 应用日志目录存在,应用用户可写。
- 应用日志有滚动策略和总容量限制。
- 错误日志和普通日志能分开查。
- 日志格式里有时间、级别、线程、服务名、traceId。
- Nginx access log 有状态码、耗时、上游耗时、traceId。
- Nginx error log 级别不是随便开 debug。
- Docker logging driver 有轮转限制。
- journal 有容量限制。
- logrotate 已经
-d演练过。 - 采集器能读到日志,输出端可达。
- Java 异常栈能合并成一条日志。
- 日志平台能按
service/env/level/traceId查询。 - 没有明文密码、token、cookie、身份证、手机号。
- 本地保留、集中保留、归档保留都有周期。
常用命令速查
应用日志:
tail -f /data/logs/order-service/app.log
grep -iE "error|exception|timeout" /data/logs/order-service/app.log
grep 'demo-trace-001' /data/logs/order-service/app.logNginx:
nginx -t
systemctl reload nginx
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.logDocker:
docker logs --tail 200 -t app-a
docker inspect --format '{{.HostConfig.LogConfig.Type}} {{.LogPath}}' app-a
docker system dfjournal:
journalctl -u order-service -f
journalctl -p err..alert -b --no-pager
journalctl --disk-usagelogrotate:
logrotate -d /etc/logrotate.conf
logrotate -f /etc/logrotate.d/nginx
cat /var/lib/logrotate/status | tail磁盘:
df -h
df -ih
du -xhd1 /var | sort -h
find /var/log /data/logs -type f -size +500M -print
lsof +L1官方文档入口
- Docker logging drivers: https://docs.docker.com/engine/logging/configure/
- Nginx access log module: https://nginx.org/en/docs/http/ngx_http_log_module.html
- Nginx control signals: https://nginx.org/en/docs/control.html
- systemd-journald: https://man7.org/linux/man-pages/man8/systemd-journald.service.8.html
- journalctl: https://man.archlinux.org/man/journalctl.1.en
- logrotate: https://man7.org/linux/man-pages/man8/logrotate.8.html
- Spring Boot logging: https://docs.spring.io/spring-boot/reference/features/logging.html
- Filebeat Logstash output: https://www.elastic.co/docs/reference/beats/filebeat/logstash-output
- Filebeat log input deprecation: https://www.elastic.co/docs/reference/beats/filebeat/filebeat-input-log
- Grafana Loki and Promtail status: https://grafana.com/docs/loki/latest/send-data/promtail/
- Grafana Alloy to Loki: https://grafana.com/docs/loki/latest/send-data/alloy/
- OWASP Logging Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html
