eladmin日志系统全链路解析:从AOP审计到Linux排查与调优

📅 发布时间:2026/10/13 15:20:02
eladmin日志系统全链路解析:从AOP审计到Linux排查与调优
做了几年企业级后台管理系统被我同事问得最多的模块就是日志。无论技术栈怎么变日志这块雷打不动的需求始终是“出了事能查、可疑操作能回溯、黑了有告警”。eladmin作为一款很流行的Spring Boot后台管理脚手架它的日志模块设计得相当有代表性既有AOP切面实现的用户操作审计日志又有登录日志、异常日志这类基础安全日志还衔接了Linux日志运维里最常见的进程与性能排查场景。当年第一次接触eladmin时我其实一眼看去觉得日志页面没什么特别的就是列表加筛选加详情。真正动手扩展把日志从“能查”变成“好用、能扛、能定位问题”之后才发现这套系统在日志设计的取舍和扩展点上有不少值得复盘的地方。这篇文章就从eladmin日志系统出发把框架选型、设计思路、落盘策略到Linux进程排查的完整链路都串一遍适合正在用eladmin改业务、又想把日志体系做得更扎实的Java后端。1. 日志系统的整体设计与技术选型1.1 eladmin日志模块的分层结构eladmin的日志体系从形态上可以拆成三层框架日志、应用业务日志、用户审计日志。框架日志管的是Spring、数据库连接池、MyBatis这些底层组件的输出这一层大多线性写入文件出了问题往上查异常栈应用业务日志是业务代码里手动打的INFO、WARN、ERROR比如某某定时任务跑了多少条数据、某个接口上游响应了多久用户审计日志是eladmin最有特色的部分它把每次“谁在什么时间做了什么操作、请求了什么接口、传了什么参数、返回了什么结果、跑了多久、从哪个IP来”完整记录下来落在独立的业务表里。三层日志解决的问题各不相同框架日志解决“系统为什么挂”业务日志解决“业务为什么错”审计日志解决“人究竟做了什么”。把这三层放在同一个系统里管理是eladmin日志设计的核心思路。我之前接过一个外包项目对方只要了审计日志表开发为了偷懒直接把MyBatis打印的SQL当审计日志用结果出了数据问题根本追不到操作人后来查了一个星期才发现是某个运营同学在后台改了配置没有留痕。所以审计日志这块身份信息、操作前后的关键业务数据、时间戳、操作人IP一个都不能少。日志系统整体还有一层不可忽视的底座文件与性能的平衡策略。业务日志全写文件审计日志异步写库。文件可以按天滚动、按大小切割数据库表按月份分表或者定时清理。这些不是锦上添花是日志系统能不能长期稳定运行的前提否则日志文件撑爆磁盘、日志表膨胀到几个G整套系统都会被拖死。1.2 日志框架选型Logback与Log4j2的抉择eladmin默认用的日志框架是Logback因为它和SLF4J天然绑定Spring Boot也默认集成几乎零配置就能跑。但如果你要自己二次开发建议把“默认能用”和“深度定制”区分开。Logback的优势是稳定、生态成熟、文档多配置写法Spring Boot的开发者大部分都看得懂。它支持异步Appender、滚动策略、MDC做链路追踪日常业务系统的需求基本全覆盖了。Log4j2在极端高并发下性能更好支持无锁异步、更灵活的Lookup但是在Spring Boot环境里需要额外排除spring-boot-starter-logging依赖再手动引入适配器稍微折腾。我个人在eladmin这种后台管理框架上还是保留Logback没必要为了纸面上的性能数字增加配置复杂度。除非单机每秒日志条数到了几十万级否则Logback的异步Appender实测已经很够了而且Log4j2的配置文件写复杂之后反而更容易踩“配置不生效、不报错也没输出”的坑。选型时还有一个容易忽略的细节日志框架要能优雅切换且不污染业务代码。代码里统一通过LoggerFactory.getLogger(Class)拿Logger业务代码不直接依赖具体实现的类。eladmin里的工具类、切面类都是用了SLF4J接口这也是我后来扩展日志模块时最省心的原因想换实现改pom和配置文件就行Java代码一行不用动。提示如果日志系统需要往分布式方向扩展我建议把Logback和SLF4J的combination保留下来中间加一层自研的日志管道。因为ELK、SkyWalking这些生态接入时很多是通过改造Appender或者引入Tracer实现的底层越标准后续接入越顺利。1.3 日志级别与业务场景的匹配策略日志级别不只是INFO、DEBUG、ERROR这几个词它在整个日志系统设计里其实是第一道闸门。日志级别定低了线上全是噪声磁盘IO飙升定高了关键时刻一个诊断信息都没有。我在eladmin项目里常用的策略场景建议级别原因系统启动参数、加载配置INFO启动问题排查第一现场不能省定时任务执行情况INFO任务量不大保留关键节点外部接口调用超时WARN还没到系统崩溃但需要警惕数据库操作失败ERROR数据问题必须最高优先级可见请求参数完整出入参DEBUG生产默认关闭压测和联调时打开用户审计记录独立切面不走普通日志级别单独收集这里要特别提醒一个经验WARN和ERROR不要乱用。有的团队为了找问题方便把“业务校验不通过”也打成ERROR结果监控告警天天响真正的故障反而被淹没。正确做法是ERROR只留给“系统无法继续正常提供服务、需要人工介入”的情况业务上的预期失败用INFO或WARN记录即可配合对应的业务状态码。Logback的dynamic级别调整也是一个实用技巧。生产环境碰到疑难杂症时把某个包或某个类的日志级别临时调成DEBUG不需要重启服务通过Spring Boot Actuator的loggers端点动态改。eladmin本身没有集成这个功能扩展起来也很快加个spring-boot-starter-actuator再暴露/actuator/loggers端点就能操作。排查完记得调回INFO不然会有大量额外日志刷盘。2. 用户审计日志与AOP切面的核心设计2.1 审计日志表的数据模型设计eladmin的审计日志表存储了操作时间、操作人、请求方法、请求参数、浏览器、请求IP、请求耗时、请求路径、异常信息等字段。从设计角度说这张表本质上记录的是“HTTP请求维度”的用户行为。我在设计企业级审计日志表时会在这个基础上增加几个实用字段CREATE TABLE sys_log ( log_id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) COMMENT 操作人账号, log_type VARCHAR(16) COMMENT 日志类型 INFO/WARN/ERROR, operation VARCHAR(255) COMMENT 操作描述, request_uri VARCHAR(255) COMMENT 请求路径, http_method VARCHAR(16) COMMENT 请求方式, request_param TEXT COMMENT 请求参数, response_body TEXT COMMENT 响应结果, request_ip VARCHAR(64) COMMENT 操作IP, request_time BIGINT COMMENT 请求耗时 毫秒, status TINYINT COMMENT 执行状态 1成功 0异常, error_msg TEXT COMMENT 异常信息, trace_id VARCHAR(32) COMMENT 链路追踪ID, create_time DATETIME COMMENT 操作时间, KEY idx_username (username), KEY idx_create_time (create_time), KEY idx_operation (operation) ) COMMENT用户操作审计日志表;这个表的关键点是索引设计。大部分查询场景是“查某个用户某段时间的操作”username加create_time联合索引是刚需。其次是operation字段它存的是“操作描述”比如“新增用户”“修改菜单”前端列表展示时用户一眼就懂后端通过注解方式维护这个描述比自己去解析URL来得直观准确。response_body字段我后来加上的。eladmin原版只记录了返回码但有些问题需要对比“用户提交了什么”和“系统最终存了什么”。比如用户提交了一个状态字段前端显示的是启用库里却是禁用这种问题如果日志里有response_body就能快速确认是不是后端转换逻辑出错。代价是每次操作都会多存一段JSON磁盘开销略大但也值得。还有一个容易忽略的字段是trace_id。虽然eladmin自身没有引入全链路追踪但预留这个字段非常有必要。后期接入SkyWalking或者自研MDC方案时每个请求在入口生成一个traceId放进日志上下文业务日志和审计日志都记录这一字段跨服务排查问题时靠它就是一条线串起所有节点。2.2 基于AOP的操作日志切面实现eladmin审计日志之所以好用靠的是注解加AOP的设计。使用者只需在Controller的方法上加上Log(新增用户)注解系统自动记录操作行为。切面的核心思路其实不复杂Aspect Component public class LogAspect { Autowired private SysLogService sysLogService; Pointcut(annotation(com.example.annotation.Log)) public void logPointcut() { } Around(logPointcut()) public Object handleLog(ProceedingJoinPoint joinPoint) throws Throwable { long startTime System.currentTimeMillis(); SysLog sysLog new SysLog(); try { MethodSignature signature (MethodSignature) joinPoint.getSignature(); Log logAnnotation signature.getMethod().getAnnotation(Log.class); if (logAnnotation ! null) { sysLog.setOperation(logAnnotation.value()); } sysLog.setRequestUri(WebUtils.getRequestUri()); sysLog.setHttpMethod(WebUtils.getHttpMethod()); sysLog.setUsername(SecurityUtils.getCurrentUsername()); sysLog.setRequestIp(WebUtils.getClientIp()); Object result joinPoint.proceed(); sysLog.setStatus((byte) 1); sysLog.setResponseBody(JSON.toJSONString(result)); return result; } catch (Throwable e) { sysLog.setStatus((byte) 0); sysLog.setErrorMsg(ExceptionUtils.getStackTrace(e)); throw e; } finally { sysLog.setRequestTime(System.currentTimeMillis() - startTime); sysLogService.asyncSaveLog(sysLog); } } }这段代码里有几个细节要重点讲。第一异常不能吞。catch到异常后先记录日志然后重新抛出throw e切面不能改变业务接口的正常流转。有的开发者图方便在切面里catch了异常就return null结果接口永远返回成功前端拿到的都是空数据排查起来极其折磨。第二finally块里记录耗时逻辑。不管接口成功还是异常耗时都要记录这样统计接口性能时才能把失败请求也覆盖到。耗时单位我用毫秒eladmin原版用的是Instant时间戳差值展示的时候再换算成毫秒也没问题。第三requestParam不是简单地joinPoint.getArgs()然后toString。这个方法看起来方便实际上一旦参数里有密码字段、文件流、HttpServletResponse这类对象要么把敏感信息打进日志要么toString抛异常。我的做法是用一个自定义序列化器过滤敏感字段只序列化可解析的普通对象文件流直接跳过。2.3 异步落库的实现与线程安全细节AOP切面同步记录日志的话接口耗时会被日志写入拖累。业务操作对应的日志表写入是在事务同步范围内的日志表一旦锁竞争或者慢查询用户感知到的接口延迟立刻飙升。所以eladmin这一层设计我强烈建议走异步落库。异步落库最常见的实现方式有两种一种是直接从Spring容器里取一个线程池把保存动作提交到线程池另一种是先用Spring的Async注解标记方法异步执行。两种方式都可以但要注意几点Async(logExecutor) public void asyncSaveLog(SysLog sysLog) { sysLogMapper.insert(sysLog); }线程池必须单独定义不能直接用系统默认的SimpleAsyncTaskExecutor那个每次新建线程的损耗太大。我习惯配置一个有界队列加拒绝策略的线程池Bean(logExecutor) public Executor logExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(4); executor.setQueueCapacity(1000); executor.setThreadNamePrefix(log-async-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); executor.initialize(); return executor; }线程池的参数按日志量调整核心线程不用太多日志任务本身短小多线程反而增加上下文切换。队列容量决定了高峰期的缓冲能力如果队列满了我选择丢弃日志而不是阻塞业务线程这是日志系统设计里一个重要权衡——日志是辅助手段不能反过来影响主业务流程。异步还有一个大坑事务上下文和线程绑定问题。异步方法里如果继续用Transactional操作数据库Spring的TransactionSynchronizationManager是不认识异步线程的事务会失效。一般日志保存不需要事务单条insert自动提交足够了千万别画蛇添足加事务注解。MDC跨线程传递也要注意。如果业务线程里设置了MDC然后异步保存日志时想带traceId直接在异步线程里拿是拿不到的。我是把traceId作为SysLog的一个字段在切面的finally里从MDC取出后塞进去异步线程只负责持久化不依赖MDC上下文。3. 日志落盘配置与Linux运维实战3.1 Logback配置文件的核心参数解析eladmin的配置文件里Logback的配置通常放在logback-spring.xml。这份配置管的是文件路径、滚动策略、日志格式、异步输出这些事。很多开发者的日志配置是“能用就行”等到磁盘满了或者日志丢了一段才回头看那时已经晚了。我整理出一套比较稳的文件输出配置configuration !-- 保留时间与目录 -- property nameLOG_PATH value${LOG_PATH:-./logs} / property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{traceId}] %logger{50} - %msg%n / !-- 控制台输出 -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 按天滚动与大小双重切割 -- appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_PATH}/app.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern${LOG_PATH}/app.%d{yyyy-MM-dd}.%i.log.gz/fileNamePattern maxFileSize100MB/maxFileSize maxHistory30/maxHistory totalSizeCap5GB/totalSizeCap /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 异步输出 -- appender nameASYNC_FILE classch.qos.logback.classic.AsyncAppender appender-ref refFILE / queueSize1024/queueSize discardingThreshold0/discardingThreshold neverBlocktrue/neverBlock /appender root levelINFO appender-ref refCONSOLE / appender-ref refASYNC_FILE / /root /configuration这几个参数逐一说明。maxFileSize控制单个日志文件大小。日志文件过大打开慢、检索慢、切割不规律比如几GB的文件想要grep光加载都要等半天。切到100MB一个文件日常运维用zgrep看历史日志也快。maxHistory控制保留天数。日志不是越久越好磁盘代价和合规要求平衡之后生产环境一般留30天足够了。需要长期审计的另说可以通过定时任务把特定日志导出归档到对象存储。totalSizeCap是总容量上限。这个参数很多配置里都会漏掉导致maxHistory虽然设了30天但如果某天日志暴涨到几个G总磁盘占用还是会失控。totalSizeCap5GB表示所有日志文件加起来的体积超过5GBOlder日志自动删除动态平衡天数和容量。neverBlocktrue是异步Appender里的关键。当队列满时日志线程不会阻塞业务线程而是直接丢弃日志。性能和可靠性二选一业务系统我优先保性能日志丢几条总比接口被日志拖死强。3.2 多环境日志配置的切换方案eladmin通常有dev、test、prod三套环境。日志配置不能一套走天下开发环境要控制台输出带行号生产环境要JSON结构化输出方便采集日志路径和级别也都不一样。用springProfile标签做环境隔离最方便springProfile namedev logger namecom.example levelDEBUG / /springProfile springProfile nameprod logger namecom.example levelINFO / root levelINFO appender-ref refASYNC_FILE / /root /springProfile生产环境的日志格式我的建议是输出JSON。Elasticsearch采集时直接解析不需要额外的正则搬运工。Logback里配一个LogstashEncoder日志格式就是JSON对象encoder classnet.logstash.logback.encoder.LogstashEncoder includeMdctrue/includeMdc /encoder开发环境看JSON反而不直觉所以dev环境继续用明文pattern。我自己常用的策略是生产JSON输出给机器读开发明文输出给人读两套环境的Appender配置拆开用springProfile激活对应环境。多环境还有一个细节日志文件夹的权限。生产环境一般用专用账号启动应用比如eladmin这个用户日志目录要将属主改成这个账号否则应用启动时没有目录写权限日志直接不输出看控制台才发现服务其实已经在跑了。3.3 脚本长时间窗口无日志输出时如何判断进程是否存活这个场景不只是eladmin会遇到几乎每个用了Linux服务器部署Java应用的人都会碰到明明运行了一个sh脚本或者启动了一个Spring Boot应用慢SQL、长任务、远程调用等待的时候日志文件半个多小时没动静心里就开始发毛——脚本到底还活着没有任务会不会已经卡死了经验不丰富的人会根据“没有日志”直接判断程序挂了。其实没有日志输出跟进程死了是完全不同的两回事。日志不输出有几种常见情况应用在等待上游响应、应用在做长耗时计算、日志级别在运行时被调高了、日志缓冲区还没刷新、或者应用真的卡死了。判断存活与否第一步就是查进程状态。Java进程比较典型用jstack和jstat配合先看进程还在不在再看它在干什么。# 查看Java进程是否存在 ps -ef | grep java # 或者用jps列出JVM进程 jps -l # 查看线程快照检查是否有明显的阻塞或死锁 jstack pid | grep -A 30 java.lang.Thread.State # 查看JVM堆使用情况判断是否频繁GC导致停顿 jstat -gcutil pid 1000 10jstack输出里如果看到大量RUNNABLE线程说明应用还在正常工作。如果有一堆BLOCKED或WAITING线程就要进一步看是不是锁等待或数据库连接池等待。jstat看GC情况如果Full GC频繁且堆直线打满说明程序虽然在运行但已经处于性能崩溃边缘。对于普通的sh脚本或者Python脚本没有jstack这些工具可用那就靠进程信号的检测来判断# 查看进程是否存在且主线程在跑 ps -ef | grep myscript.sh | grep -v grep # 查询进程运行时长和CPU/内存占用 ps -o pid,etime,time,stat,cmd -p pid # 给进程发送空信号判断是否有权限且进程存在 kill -0 pid echo 进程仍在运行 || echo 进程已退出kill -0是个经典技巧。它并不给进程发送任何实际信号只检查进程是否存在、当前用户是否有操作权限。如果返回0就说明进程还在整个检查逻辑写进监控脚本里也是很方便的工具if kill -0 $PID 2/dev/null; then echo $(date %F %T) 进程 $PID 仍存活 else echo $(date %F %T) 进程 $PID 已退出 fi还有个底层但非常可靠的检查方式查看/proc/pid目录。Linux下每个进程都有一个对应的/proc/pid目录目录在进程就在。通过cat /proc/pid/status可以看到进程的状态码State字段是R运行、S睡眠、D不可中断睡眠通常是IO等待。如果状态是D说明进程卡在内核IO等待里这种状态下日志也可能长时间没输出但不代表进程挂了。排查这类问题还有一个高阶思路去看日志文件描述符本身。进程在运行不等于日志在写但日志在写一定说明进程活着。用lsof -p pid | grep log可以看到进程打开的日志文件再用ls -l /proc/pid/fd/1查看标准输出指向哪里。如果标准输出指向文件且文件大小在变化说明主线程还在活跃。我再分享一个实战经验脚本明明没有日志但系统负载和CPU使用率在波动大概率程序还活着只是被阻塞在某次IO操作上。反之CPU、内存、磁盘全部平得像一条直线进程还占着PID那可能真的是线程卡死在等待某个永不返回的资源上。3.4 后台任务与定时任务日志的输出管理eladmin这类后台管理系统里定时任务非常多定时清理缓存、定时同步数据、定时发送报表。定时任务的日志管理有个天然难点——跑的时候不一定有人盯着控制台出错了又必须第一时间知道。我的做法是给定时任务单独建日志文件并且把关键结果通过额外渠道推送出来。Logback里按任务名称创建独立的Loggerprivate static final Logger TASK_LOGGER LoggerFactory.getLogger(task-cache-clean);然后在logback-spring.xml里给这个Logger指定独立的Appenderlogger nametask-cache-clean levelINFO additivityfalse appender-ref refTASK_FILE / /logger这样定时任务的日志不会和业务日志混在一起排查任务问题时只需要看对应的任务日志文件不用在一个巨大的app.log里大海捞针。定时任务运行时长的监控也很重要。我的经验是每个定时任务必须有开始日志和结束日志结束日志里必须带上任务耗时和执行结果。否则任务卡死两小时你都不知道等到用户投诉了才去看任务还在卡着的状态。如果是短周期任务比如每分钟跑一次日志文件增长会很快。这种任务不要打太多INFO日志我见过一个三十秒钟的定时任务一次打几行INFO一天下来日志文件就涨2GB。定时任务的日志级别我通常设WARN只有任务失败或者数据校验不过才记录正常执行一笔带过。4. 日志系统常见问题排查与优化实录4.1 日志文件不输出的成因排查日志文件不输出是线上环境最让人恼火的问题之一。我判断这一类问题有一个固定的排查顺序先看进程再看配置最后看权限。第一步查进程。应用是不是真的启动了jps -l看一下如果进程都没有日志自然不会有。第二步查日志目录权限。应用账号对日志目录有没有写权限ls -ld看一下目录权限位。之前遇到过一例运维用root部署应用应用起来后生成了日志文件但属主是root后来应用因为被攻击换了个低权限账号重启新进程根本写不了原有日志文件日志文件就变成了0字节还不报错。第二步还有一个细节要注意日志文件被logrotate轮转后应用还在写旧文件句柄。Linux下日志轮转一般会改名原文件如果Logback的RollingFileAppender没正确处理这种情况日志就会持续写到已改名的文件里新生成的空日志文件中一条内容都没有。解决办法是应用支持SIGHUP信号或定时重新打开文件句柄或者统一用Logback自己的滚动策略避免外部logrotate和Java应用抢文件。4.2 异步日志丢失与缓冲区配置的权衡用了AsyncAppender之后如果配置不当高并发下日志丢失率会很高。AsyncAppender的默认队列大小是256超过之后默认的丢弃策略是丢弃TRACE、DEBUG、INFO级别的日志只保留WARN和ERROR。生产环境如果业务量大、INFO日志又多高峰期大量INFO日志被丢弃是常态排查问题时就发现缺乏关键线索。我自己调整的方式appender nameASYNC_FILE classch.qos.logback.classic.AsyncAppender queueSize8192/queueSize discardingThreshold0/discardingThreshold neverBlocktrue/neverBlock maxFlushTime1000/maxFlushTime appender-ref refFILE / /appenderqueueSize提高到8192缓冲区足以应对大部分业务高峰。discardingThreshold0表示队列满时不丢弃日志配合neverBlocktrue表示队列满时日志线程直接放弃等待保证业务线程不被阻塞。这两个参数看起来矛盾实际工作方式是队列满时新日志直接丢但至少缓冲区内已有的日志不丢。比默认策略好一点不至于高峰期的日志几乎全被清空。应用停机时也要注意AsyncAppender的flush问题。Spring Boot优雅停机时Logback的AsyncAppender会等待队列中的日志写完再退出但等待时间有限。如果队列里积压了几千条日志还没落盘进程直接被kill -9杀掉这部分日志就全丢了。如果是重要系统停机脚本里先调用loggerContext.stop()给异步Appender一个flush缓冲区的时间窗口。4.3 日志时区、编码与检索效率的细节日志排查里还有一个容易忽视的小问题时区。%d{yyyy-MM-dd HH:mm:ss.SSS}默认用的是JVM默认时区如果服务器的系统时区是CST而部署的容器时区是UTC日志时间和数据库记录时间错开8小时配合溯源时经常出现时间对不上、前后矛盾。我建议直接在pattern里显式指定时区pattern%d{yyyy-MM-dd HH:mm:ss.SSS}{GMT8} %-5level .../pattern或者在启动脚本里加上-Duser.timezoneAsia/Shanghai这样JVM、日志、数据库的时间都统一到同一个时区。日志文件的中文编码问题也是如此。Linux系统默认locale是POSIX或C的话控制台输出中文可能变成问号日志文件打开也是乱码。Logback的Encoder里必须显式写charsetUTF-8/charset同时在file.encoding启动参数上也保持一致。乱码日志比没有日志更难处理因为看起来有内容实际全是无意义符号。检索效率方面日志文件切小了之后配合zgrep基本够用# 搜索某时间点的异常日志 zgrep 2025-06-10 14: logs/app.2025-06-10.*.log.gz | grep -i exception # 查看某个traceId的完整日志链路 zgrep traceIdabc123 logs/app.*.log.gz日志检索的最大成本其实来自文件太多一次zgrep要遍历几十个压缩文件。如果系统复杂、日志量大就升级成ELK或者Loki这类集中式日志平台Logback配置个LogstashAppender批量推送检索效率就不是一个数量级了。4.4 日志表膨胀问题与数据清理策略日志表数据量是每个后台系统迟早要面对的。操作日志一张表存几百万条很常见不清理的话写入变慢、查询变慢、索引膨胀、备份也痛苦。eladmin的日志表在数据量达到百万条级别后需要引入归档和清理策略。最简单的方案是定时任务每天凌晨删除超过N天前的日志DELETE FROM sys_log WHERE create_time DATE_SUB(CURDATE(), INTERVAL 90 DAY) LIMIT 5000;但直接DELETE大表数据有个问题锁和IO。删除操作会触发大量redo日志和索引更新我在生产环境测试过一次删几十万条数据即使只删十分钟也有明显IO抖动。稳妥做法是分批次删除每批限制几百条配合sleep间隔降低对主库的影响。如果日志量到了千万级就要考虑按天分表或按月分表。日志查询通常都带时间范围按时间分表能把旧表直接归档甚至迁移到冷存储查询也只落到对应月份的表里。分表逻辑不一定要用分库分表中间件简单的MyBatis动态表名也可以实现关键是设计时提前规划。日志表的索引策略也要跟着业务走如果绝大多数查询是按用户和时间那就联合索引(username, create_time)就够了如果经常查某个操作的异常记录可以考虑加(operation, status)的二级索引但索引不是越多越好日志表写入频繁每多一个索引就多一份写入负担平衡点要在实际压测中找。5. 日志系统二次开发与扩展实践5.1 接入日志平台与链路追踪的扩展方案eladmin原生的日志系统在单机场景足够用但服务一多、系统一拆分日志分散在各台机器上的问题就会暴露出来登录A服务器看A日志登录B服务器看B日志跨服务调用一条操作要打开三个终端手动对齐时间令人头大。这个阶段就要把日志系统接入集中式日志平台。最常见的技术栈组合是ELK或者LokiGrafana两者取舍方案优势劣势适用场景ELK查询能力强、生态成熟、字段解析灵活资源占用高、搭建复杂大型系统、容器化环境Loki轻量级、和Grafana集成好、成本低全文检索能力弱中小规模、已有Grafana监控接入ELK时Logback的Appender改成LogstashEncoder彩色输出比较直接。以前需要Filebeat采集文件再传输后来我更喜欢直接让应用通过TCP或UDP输出给Logstash省掉Filebeat这一层转发。不过增加网络传输后日志可靠性又会受到网络波动影响Logback里有一个LogstashTcpSocketAppender可以配置重连机制。链路追踪方面eladmin二次开发时最合适的方案是引入spring-cloud-sleuth-zh旧或者micrometer-tracing新搭配spring-boot-starter-actuator。引入后在MDC里会自动带上traceId和spanIdLogback配置里把%X{traceId}打印进每条日志再到日志平台直接按traceId检索一次用户请求经过的所有服务日志全部串起来。5.2 敏感操作告警与安全审计的增强思路日志系统不只是事后排查工具还能做主动安全防御。eladmin的用户管理、菜单权限、字典管理这些敏感模块操作日志除了写入数据库还应该通过日志平台触发告警。我的做法是给审计日志打标记配置独立的告警通道。在切面注解里增加一个isSensitive布尔值标记为敏感的日志写入后通过MQ发布一个告警事件由告警服务发钉钉或者邮件通知安全负责人。典型的敏感操作包括删除用户、修改角色权限、导出全量用户数据、修改系统配置。这类操作不一定都是恶意的但都是高风险的出问题前需要提前感知、出问题后需要第一时间响应。日志系统如果能在这些操作发生时自动提示安全人员的工作就轻松一大截。另一个增强思路是登录日志的异动检测。eladmin本身有登录日志记录登录IP、浏览器等。扩展成一个简单的风险检测规则比如同一个人短时间内不同IP频繁登录或者凌晨三点有登录操作就可以打一个风险标记。这个规则不需要复杂的算法基于日志的简单统计就很有价值。5.3 日志系统的性能压测与参数调优实践日志模块这层看起来不显眼性能问题往往在流量上来时才暴露。我的压测经验来自一次活动大促接口峰值从几百QPS涨到两三千结果日志成了第一个瓶颈数据库连接池被日志写入任务占满接口大面积超时。压测时重点观察几个指标日志写入耗时、异步队列积压情况、日志文件IO。用iostat -x 1看磁盘%util如果长期超过80%说明磁盘IO已经成为瓶颈。此时有两个优化方向一是给日志文件换独立的磁盘避免业务数据和日志抢IO二是加大Logback异步队列把磁盘写放大延后到低峰期。还有一个小技巧日志格式里的%method和%line非常耗费性能。Logback打印方法和行号需要分析StackTrace这个操作在高并发下有比较大的开销。生产环境建议去掉%L、%M这两个转换符保留类名和线程名足够定位问题了。压测数据上去掉行号信息后日志打印吞吐提升10%到15%很常见收益非常可观。日志格式也不宜太长。一条日志包含时间、级别、线程、Logger名、traceId、消息已经足够丰富。有的团队还把完整IP、主机名全塞进去一条日志几百字节高峰期的带宽和磁盘压力都成倍增加务必克制。日志是系统的另一双眼睛。它平时藏在不引人注意的角落里平时正常工作没有存在感一出问题你就发现它是唯一能把那几秒钟发生的事情还原出来的证据。eladmin这套日志系统给我最大的启发不是它用了什么框架、什么切面技巧而是它把用户行为审计、系统状态留痕、异常追踪三件事融合进了一套简洁的设计里。DDD里把“事件溯源”讲得再高级落到工程上也就是一条可靠的日志记录和一套能快速查询的入口。如果你也在用eladmin建议按这个顺序去打磨日志系统先补全审计日志字段确认每一个敏感操作都有记录然后调整Logback参数把文件切割、清理策略、异步缓冲配好最后接一个日志平台让日志从“文件里的文本”变成“可以检索、可以告警、可以可视化的资产”。日志系统不用一步到位但每一步都做扎实系统运行久了你会回来感谢现在的自己。最后分享一个踩过多次坑之后的习惯每次上线发版之后我都会趴在日志前面至少观察半小时你看不到用户怎么操作但日志会告诉你哪些接口在报错、哪些任务在失败、哪些耗时异常。日志在替你说话关键是你得先把它设计成“会说话的样子”。