一切看起来都只是日常:早上打开电脑,git pull拉取最新代码,在终端执行了几条命令,启动本地服务,用curl调了一个接口,然后在页面上点了一个按钮,晚上提交了一次数据库变更。你以为这些动作做完就结束了,但实际上,每一个动作都已经被某个系统悄悄记录下来。终端会话有 history,Git 有 reflog,Web 请求有访问日志,接口调用有业务日志,数据库有一条 binlog,前端埋点会告诉你用户在哪个页面停留了多久。
“Everything You Do Is Being Recorded”——这句话放在信息安全语境里,听起来像是某种警告;但放到工程视角看,它其实只是数字化系统运行的基本事实。任何一个运行良好的系统,都离不开对自身行为的记录。真正的区别从来不是“有没有被记录”,而是:记录发生在哪一层、由谁负责、存多久、谁能看、用来做什么,以及这些记录有没有做必要的保护和脱敏。
这篇文章想做的,是把“被记录”这件事拆开讲清楚。你会看到开发者日常工作中留下的技术痕迹有哪些,一套完整的记录体系由哪些层构成,以及如何用 Spring Boot、前端埋点、Nginx、Shell 和 MySQL 这些常见技术,在一个最小工程里跑通一条“从浏览器点击到后端审计日志”的完整记录链。最后还会讨论一个更重要的问题:记录系统的隐私边界在哪里,以及工程上应如何平衡“全量记录”和“最小化采集”。
1. 记录是技术系统的“呼吸”,不是阴谋
如果把一个软件系统比作人体,那么日志、埋点、审计记录就是它的呼吸和脉搏。健康的时候你不会感知到它的存在,可一旦系统出现问题——线上数据被误删、接口突然变慢、某个用户反馈订单状态不对——你才会发现,所有排查手段几乎都建立在“当时留下了什么记录”这个前提上。
反过来看,一个完全没有记录的系统是什么状态?
假设线上出现了一次严重的数据不一致:用户 A 的余额被重复扣减,数据库里多了两条扣款记录。没有业务日志,你不知道是哪一次调用扣的;没有审计日志,你不知道操作者是谁;没有链路追踪,你不知道这次扣款是从前端、定时任务还是第三方回调发起的;没有 binlog,你甚至不知道数据是什么时候、被哪条 SQL 改的。你能做的只有打开代码一行行猜,然后在测试环境拼命复现。这种排障方式,运气好需要半天,运气不好可能要一周。
所以,“Everything You Do Is Being Recorded”这句话,真正值得警惕的不是“系统在监视你”,而是“很多系统根本没有能力回答‘刚刚发生了什么’”。从这个角度看,设计一套完整、可追溯、可治理的记录体系,是工程师的基本功,而不是什么锦上添花的事。
对开发者来说,记录还有一个双重身份。一方面,你是“被记录者”:你在终端敲的命令、在 IDE 里的操作、提交的代码、访问的内部系统,都有日志;另一方面,你又是“记录者”:你写的每一个接口、每一条日志、每一个埋点,都在决定这套系统以后能回溯到什么程度。理解这个双重身份,是理解整篇文章的关键。
2. 记录到底发生在哪一层:行为记录、业务记录与基础设施记录
同一个用户点击一次“提交订单”按钮,表面上只是一次操作,但在技术系统里,它会留下至少三到四条记录,分布在完全不同的层面。
2.1 行为记录层:用户做了什么
这一层回答的是“用户是谁、在什么时间、做了什么动作”。最典型的是前端埋点。用户点击了哪个按钮、浏览了哪个页面、从哪个渠道进入、在页面上停留了多久,这些事件通过前端 SDK 采集后上报到后端。
行为记录层的价值偏向业务分析,比如转化漏斗、留存分析、推荐策略。它也是隐私合规最敏感的区域,因为采集的是用户个人行为数据,必须谨慎处理。
2.2 业务记录层:系统做了什么
这一层回答的是“系统针对这次操作执行了什么逻辑、结果如何”。典型实现是后端业务日志和审计日志。业务日志关心请求参数、处理耗时、异常堆栈;审计日志更关心操作者和操作对象,比如“用户 zhangsan 在 2025-01-01 10:00:00 修改了订单 1001 的收货地址”。
业务记录是故障排查的核心依据。一次接口调用为什么失败、哪个环节耗时最长、异常发生在哪个方法里,都依赖这一层的记录。
2.3 基础设施记录层:网络和系统层面留下了什么
这一层回答的是“流量和资源层面的客观事实”。包括 Nginx 访问日志、数据库 binlog、服务器 CPU 和内存指标、容器日志等。它不关心业务语义,但能够还原出“谁在什么 IP 访问了什么地址”“数据库哪条语句变更了哪一行”。
一张表格能更直观地展示这个分层:
| 层级 | 核心问题 | 典型记录内容 | 常见实现 |
|---|---|---|---|
| 行为记录层 | 用户做了什么 | 点击、浏览、停留、转化 | 前端埋点、事件采集 SDK |
| 业务记录层 | 系统做了什么 | 请求参数、业务状态、异常、操作者 | 业务日志、审计日志、AOP 切面 |
| 基础设施层 | 网络和系统发生了什么 | 访问日志、SQL 变更、服务器指标 | Nginx、MySQL binlog、监控系统 |
理解这个分层很重要。很多关于“被记录”的焦虑,本质上是把三个层面混在一起谈。真正需要管理的,不是“不要记录”,而是“记录之后怎么办”:哪些数据明文存、哪些脱敏存、哪些保留三天、哪些保留三年、谁能访问、出了问题能不能在合法的范围内被审计。
3. 为什么系统必须全量记录:从排障、审计到合规
3.1 故障排障需要“案发现场”
没有现场日志的线上事故,排查成本会成倍上升。记录的价值首先体现在“它可以告诉你问题发生时,系统内部实际发生了什么”。这也是可观测性(Observability)理念的基础:系统必须能够被外部数据完整描述,而不是靠开发者用大脑推断。
3.2 安全审计需要“操作留痕”
账号被盗、内部人员删库、用户被薅羊毛,这类事件发生后,第一诉求是溯源。谁在什么时间、通过什么接口、做了哪个操作、影响了哪些数据,这些信息必须从记录中还原出来。没有审计日志,安全事件就变成了一桩无头悬案。
3.3 业务分析需要“行为数据”
行为记录层的数据可以直接支撑业务决策。按钮曝光率和点击率差多少、哪个步骤流失最严重、新用户首次转化路径是什么,这些问题的答案来自埋点记录。没有行为数据,产品优化只能靠直觉。
3.4 合规要求需要“留痕可查”
近年来的数据保护法规对个人信息处理提出了严格要求,很多行业监管也要求企业留存核心操作日志。这类要求本质上是在倒逼企业建立一套“可审计”的记录体系。只要涉及用户个人信息,记录前就要先想清楚授权范围、脱敏方案、访问权限和保留期限。
这里需要给一个清醒的判断:不是所有记录都有价值。无结构、无关联、无治理的记录只是数据垃圾,占着磁盘空间,检索一次要等半天,真正出问题时反而难以定位。记录体系的核心不是“数据量有多大”,而是“能不能在需要时快速找到真相”。
4. 记录体系的总体架构与技术选型
4.1 一条数据从产生到被检索,要经过哪些环节
一个完整的记录体系,通常包含四个阶段:采集、传输、存储、分析。
采集端的行为各不相同。前端通过埋点 SDK 上报事件,后端通过 AOP 切面或日志框架输出业务日志,基础设施层通过 Agent 采集指标和访问日志。这些数据汇聚到传输层,小项目可以直接写文件或走消息队列,中大型项目通常会引入 Kafka 或 RocketMQ 这类消息中间件,目的是削峰填谷,避免高并发写入直接压垮存储。
存储层的选型决定数据的可检索性。最经典的组合是 ELK(Elasticsearch + Logstash + Kibana),Elasticsearch 负责索引和全文检索,Kibana 负责可视化;轻量级方案可以用 Loki;大数据量场景也会用 ClickHouse 做日志分析。分析层则根据需求拆分:实时监控用 Prometheus + Grafana,链路追踪用 SkyWalking 或 OpenTelemetry。
下面是一张选型对比表:
| 场景 | 常用工具 | 适用规模 | 一句话评价 |
|---|---|---|---|
| 日志采集 | Filebeat / Logstash / Fluentd | 小到大 | Filebeat 轻量,Logstash 功能重 |
| 消息缓冲 | Kafka / RocketMQ | 中到大 | 高吞吐场景必备 |
| 日志存储检索 | Elasticsearch / OpenSearch | 中到大 | 全文检索能力强,重资源 |
| 轻量日志方案 | Loki | 小到中 | 只索引标签,成本更低 |
| 监控指标 | Prometheus + Grafana | 中到大 | 指标类事实标准 |
| 链路追踪 | SkyWalking / OpenTelemetry | 中到大 | 跨服务链路串联 |
4.2 小项目不要盲目堆组件
很多团队一上来就要搭一套完整的 ELK + Kafka,结果发现日志量一天才几个 GB,反而被组件自身的运维成本拖垮。合理的做法是:先用文件日志 +grep解决 80% 的问题,等日志量真正上来,再逐步引入消息队列和搜索引擎。记录体系是演进出来的,不是一步到位设计出来的。
这一原则贯穿本文:下面的最小演示环境不使用任何重组件,只用一个 Spring Boot 工程、一个前端 HTML、可选的一个 Nginx,就能把“记录链”跑通。
5. 最小演示环境搭建
本文示例的目标是跑通一条“前端点击 → 后端接口 → AOP 审计日志 → 日志文件”的完整记录链。为了降低复现门槛,尽量使用常见技术栈。
演示环境清单:
- 操作系统:Linux / macOS / Windows WSL 均可
- JDK 17,若项目是 Spring Boot 2.x,可使用 JDK 8+,代码中注意
jakarta.servlet与javax.servlet的区别 - Maven 3.x
- Nginx 1.20+,可选,用于演示访问日志
- 一个现代浏览器
说明一下,下面示例以 JDK 17 + Spring Boot 3.2 为例编写。如果你用的是 Spring Boot 2.7,AOP 代码里把jakarta.servlet.http.HttpServletRequest换成javax.servlet.http.HttpServletRequest即可,其余逻辑完全一致。
项目结构规划如下:
record-demo/ ├── pom.xml └── src/main/ ├── java/com/example/ │ ├── RecordDemoApplication.java │ ├── audit/ │ │ ├── AuditLog.java │ │ └── AuditLogAspect.java │ └── controller/ │ ├── UserController.java │ └── TrackController.java └── resources/ └── application.yml6. 完整示例:从浏览器到数据库的“记录链”实现
6.1 后端审计日志:Spring Boot AOP 注解实现
后端审计日志是整个记录链的核心。实现思路是自定义一个@AuditLog注解,通过 AOP 切面在业务方法执行前后自动记录操作者、模块、动作、参数、耗时和结果。这样做的好处是业务代码不侵入,加一个注解就能完成记录。
先看pom.xml,需要引入 Spring Web 和 AOP 依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>record-demo</artifactId> <version>1.0.0</version> <name>record-demo</name> <description>Everything You Do Is Being Recorded - Demo</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>定义注解:
// 文件路径:src/main/java/com/example/audit/AuditLog.java package com.example.audit; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AuditLog { String module() default ""; String action() default ""; }定义切面:
// 文件路径:src/main/java/com/example/audit/AuditLogAspect.java package com.example.audit; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.LinkedHashMap; import java.util.Map; @Aspect @Component public class AuditLogAspect { private static final Logger AUDIT = LoggerFactory.getLogger("AUDIT_LOG"); private final ObjectMapper objectMapper = new ObjectMapper(); @Around("@annotation(auditLog)") public Object around(ProceedingJoinPoint joinPoint, AuditLog auditLog) throws Throwable { long start = System.currentTimeMillis(); Throwable error = null; try { return joinPoint.proceed(); } catch (Throwable t) { error = t; throw t; } finally { writeAuditLog(joinPoint, auditLog, start, error); } } private void writeAuditLog(ProceedingJoinPoint joinPoint, AuditLog auditLog, long start, Throwable error) { try { Map<String, Object> record = new LinkedHashMap<>(); record.put("logType", "audit"); record.put("time", LocalDateTime.now() .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"))); record.put("module", auditLog.module()); record.put("action", auditLog.action()); record.put("costMs", System.currentTimeMillis() - start); record.put("operator", resolveUser()); record.put("method", joinPoint.getSignature().toShortString()); record.put("args", maskSensitiveArgs(joinPoint.getArgs())); if (error != null) { record.put("success", false); record.put("error", error.getMessage()); } else { record.put("success", true); } AUDIT.info(objectMapper.writeValueAsString(record)); } catch (Exception e) { AUDIT.error("write audit log error", e); } } private String resolveUser() { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs != null) { HttpServletRequest request = attrs.getRequest(); String user = request.getHeader("X-User-Name"); return user != null ? user : "anonymous"; } return "anonymous"; } private String maskSensitiveArgs(Object[] args) { String[] sensitiveKeys = {"password", "token", "secret", "idCard", "phone"}; StringBuilder sb = new StringBuilder("["); for (Object arg : args) { if (arg == null) { sb.append("null, "); continue; } String json; try { json = objectMapper.writeValueAsString(arg); } catch (Exception e) { json = arg.toString(); } for (String key : sensitiveKeys) { json = json.replaceAll("\"" + key + "\"\\s*:\\s*\"[^\"]*\"", "\"" + key + "\":\"******\""); } sb.append(json).append(", "); } return sb.length() > 1 ? sb.substring(0, sb.length() - 2) + "]" : "[]"; } }关键点有三个:
第一,@Around注解拦截所有标注了@AuditLog的方法,joinPoint.proceed()执行原逻辑,无论成功失败,finally块都会写审计日志——这保证了异常路径也不会漏记。
第二,maskSensitiveArgs在写日志前对敏感字段做脱敏。这里用的是字符串替换,属于演示级实现;生产环境建议用 Jackson 的自定义Serializer或专门脱敏框架,否则嵌套对象、数组结构会覆盖不全。
第三,resolveUser从请求头读取用户,这同样是为了演示方便。生产环境绝对不能信任前端传入的 Header,应该从登录态、Token 或网关解析后的用户上下文里获取。
再写一个业务接口演示用法:
// 文件路径:src/main/java/com/example/controller/UserController.java package com.example.controller; import com.example.audit.AuditLog; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController @RequestMapping("/api/user") public class UserController { @PostMapping("/update") @AuditLog(module = "user", action = "update") public Map<String, Object> updateUser(@RequestBody Map<String, Object> body) { String userId = String.valueOf(body.get("userId")); String phone = String.valueOf(body.get("phone")); // 真实项目中这里会调用 Service 执行业务更新 return Map.of( "code", 0, "message", "update success, userId=" + userId + ", phone=" + phone ); } }再准备一个接收前端埋点的接口:
// 文件路径:src/main/java/com/example/controller/TrackController.java package com.example.controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController @RequestMapping("/api/log") public class TrackController { @PostMapping("/track") public void track(@RequestBody Map<String, Object> body) { // 真实项目中这里可以写入消息队列或专用事件日志 System.out.println("track event: " + body); } }application.yml配置日志文件输出:
server: port: 8080 logging: level: root: info file: name: logs/app.log启动类和其他 Spring Boot 工程一致,这里不再重复贴代码。
6.2 前端用户行为埋点:SendBeacon 上报
前端的记录需求通常有两类:一类是用户主动操作,比如点击按钮、提交表单;另一类是页面生命周期事件,比如页面关闭、跳走。后者有一个经典的误区和坑:用传统的fetch或XMLHttpRequest在pagehide或beforeunload里发请求,经常因为页面正在销毁而请求被取消。
推荐的方案是navigator.sendBeacon。它的特点是浏览器会在后台可靠地发送数据,不阻塞页面卸载,适合埋点场景。
<!-- 文件路径:src/main/resources/static/index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>用户行为记录示例</title> </head> <body> <button id="submitBtn">提交订单</button> <script> function getUserId() { return localStorage.getItem('userId') || 'u_10001'; } function reportEvent(eventType, extra) { const data = { event: eventType, url: window.location.href, userId: getUserId(), timestamp: Date.now(), ...extra }; navigator.sendBeacon('/api/log/track', new Blob([JSON.stringify(data)], { type: 'application/json' })); } document.getElementById('submitBtn').addEventListener('click', function () { reportEvent('click_submit_order', { orderId: 'SO20250101001' }); }); window.__startTime = Date.now(); window.addEventListener('pagehide', function () { reportEvent('page_leave', { stayMs: Date.now() - window.__startTime }); }); </script> </body> </html>这里把页面放在 Spring Boot 的static目录下,启动后访问http://localhost:8080/index.html即可。sendBeacon的第一个参数是上报接口,第二个参数是 Blob 类型的数据体,这样后端TrackController的@RequestBody Map<String, Object>可以直接解析。
6.3 访问日志:Nginx 输出结构化 JSON
接入层访问日志是另一条重要记录。Nginx 默认的 combined 格式虽然可读,但在采集和解析时不够方便。更推荐把它改造成 JSON 格式,这样后续无论接 Logstash、Filebeat 还是 ClickHouse,都能直接解析字段。
# 文件路径:nginx.conf 中的 http 块或 server 块 log_format json_combined escape=json '{"time_local":"$time_local",' '"remote_addr":"$remote_addr",' '"request_method":"$request_method",' '"request_uri":"$request_uri",' '"status":$status,' '"body_bytes_sent":$body_bytes_sent,' '"request_time":$request_time,' '"http_user_agent":"$http_user_agent",' '"http_referer":"$http_referer"}'; access_log /var/log/nginx/access.log json_combined;配置完成后执行nginx -t校验语法,再执行nginx -s reload让配置生效。之后访问一次页面,/var/log/nginx/access.log里就会多出一条 JSON 格式的访问记录。
这里要把 Nginx 和 Spring Boot 的链路关系说清楚:前端请求先到达 Nginx,Nginx 记录访问日志,然后反向代理到 Spring Boot,Spring Boot 再记录业务和审计日志。所以同一次点击,至少会在两层各留下一条记录。
6.4 命令行与数据库操作留痕
系统记录的不只是线上业务流量,开发者和运维人员的操作同样会被记录。这一点很容易被忽略。
终端命令的常规记录由 Shell 的 history 机制完成。默认的 history 不带时间戳,也不容易追溯当前目录,可以做一点增强:
# 编辑 ~/.bashrc export HISTTIMEFORMAT="%F %T " export HISTSIZE=100000 export HISTFILESIZE=100000 export HISTCONTROL=ignoredups shopt -s histappend function record_command() { local cmd cmd=$(history 1 | sed 's/^[ ]*[0-9]*[ ]*//') echo "$(date '+%Y-%m-%d %H:%M:%S') $(whoami) pwd=$(pwd) cmd=${cmd}" >> "$HOME/.command_history.log" } export PROMPT_COMMAND="record_command; history -a"保存后执行source ~/.bashrc。从现在开始,每执行一条命令,都会追加一行到~/.command_history.log,内容包含时间、用户、当前目录和命令本身。
数据库层面的记录,最底层的是 MySQL 的 binlog。它记录的是所有可能变更数据的 SQL 操作。可以通过下面命令确认当前实例是否开启了 binlog:
SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_format'; SHOW MASTER STATUS;如果log_bin为ON,说明实例已经开启了 binlog。出于审计场景,生产环境可以使用 MySQL 官方的审计插件,或者采用专门的数据库审计方案。这里要特别提醒:数据库账号应遵循最小权限原则,不要所有应用程序共用一个可写的root账号,否则审计日志无法定位到具体操作人。
7. 运行验证:一次用户操作如何变成一条完整记录链
前面给了这么多配置和代码,现在把它们串起来跑一遍,看看一次页面点击到底会产生哪些记录。
第一步,启动 Spring Boot 应用:
mvn spring-boot:run第二步,用curl模拟一次用户修改信息的操作:
curl -X POST http://localhost:8080/api/user/update \ -H "Content-Type: application/json" \ -H "X-User-Name: zhangsan" \ -d '{"userId":"1001","phone":"13812345678"}'第三步,查看审计日志。Spring Boot 配置里指定了日志文件为logs/app.log,通过过滤审计标记查看:
tail -f logs/app.log | grep --line-buffered '"logType":"audit"'预期会看到类似下面这样的一行 JSON:
{"logType":"audit","time":"2025-01-01 10:00:00.123","module":"user","action":"update","costMs":12,"operator":"zhangsan","method":"UserController.updateUser(Map)","args":[{"userId":"1001","phone":"******"}],"success":true}判断成功的标准是:
- 出现了
"logType":"audit"字段,说明 AOP 切面生效。 operator是请求头里设置的zhangsan,说明用户身份被记录。args中的phone被替换成了******,说明脱敏逻辑执行了。success为true,说明业务方法正常完成。
第四步,在浏览器中打开http://localhost:8080/index.html,点击“提交订单”按钮,然后关闭页面。此时后端控制台会打印track event: {event=click_submit_order, ...},代表前端行为埋点已经上报。
如果配置了 Nginx 并且前端请求走了 Nginx 代理,此时/var/log/nginx/access.log里也会新增一条 JSON 访问记录。对比时间戳会发现,一次点击操作,在 Nginx 访问日志、Spring Boot 审计日志、前端埋点事件里各留下了一条记录。这就是“Everything You Do Is Being Recorded”在工程世界的真实样貌。
如果哪一步没有出现预期输出,优先从前端页面有没有真正发起请求、后端接口有没有被调用、日志文件路径是否正确这三个方向排查,具体方法见下一节。
8. 常见问题与排查方法
记录体系本身也是一个系统,它也会出问题。下面列几个高频场景:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 审计日志没有输出 | AOP 切面未生效,或目标方法通过this调用,绕过代理 | 检查切面类是否被 Spring 扫描;在方法里打印getClass()确认是否代理对象 | 添加@Component/@Aspect;通过注入的 Bean 调用自身方法,或使用AopContext.currentProxy() |
| 日志时间与本地时间相差 8 小时 | 容器或 JVM 默认时区不是Asia/Shanghai | 终端执行date对比本地时间;查看容器时区配置 | 在启动参数加-Duser.timezone=Asia/Shanghai,并统一宿主机时区 |
前端pagehide埋点经常丢失 | 用了 XHR 或 fetch,页面销毁中断了请求 | 打开浏览器 Network 面板,观察卸载页面时的请求状态 | 改用navigator.sendBeacon |
| Nginx JSON 日志包含未转义字符,解析失败 | log_format未启用escape=json | 查看 access.log 原始内容,观察引号和反斜杠 | 在log_format中开启escape=json |
| 日志文件无限增长,磁盘被写满 | 未配置轮转和保留策略 | 执行df -h查看磁盘使用率,du -sh logs查看目录大小 | 配置 logrotate,按天或按大小分割,定期清理 |
| 同一个方法有时记录有时不记录 | 方法同时被多个代理处理,比如@Transactional和@AuditLog组合,代理顺序不确定 | 调整切面@Order,观察代理调用栈 | 给 AOP 切面显式设置@Order(1) |
| 请求参数里有密码,日志明文输出 | 写日志时未做敏感字段过滤 | 查看原始日志确认泄露范围 | 使用统一脱敏工具,禁止明文记录密码、密钥、token |
先消灭“无日志可用”的问题,再谈“记录得多漂亮”。
9. 记录系统的隐私边界与工程最佳实践
9.1 隐私合规:谁能看、看什么、存多久
回到标题。“Everything You Do Is Being Recorded”之所以让人不安,核心不是记录本身,而是记录的数据可能被滥用。工程上必须用机制来约束记录系统的权力。
最小化采集原则要放在第一位。记录不是把所有信息一股脑存下来,而是只存“完成业务目标所必需”的字段。比如订单审计需要知道“谁下了单、下了什么单”,但不需要知道“用户当时在页面上滚动了几次”。能少存就少存,能不存就不存。
敏感信息必须脱敏或哈希。手机号、身份证号、密码、Token 这类信息,在日志、埋点和数据库中尽量避免明文存储。下面给出一个简单的脱敏