多租户应用下结合包/类级别的运行时日志级别调整方案咨询
多租户+包/类级运行时日志级别动态调整方案
核心思路
基于你已实现的MDC租户标识和Turbo Filter,结合Actuator端点,扩展日志级别存储为租户ID -> 包/类路径 -> 日志级别的二维映射结构,在日志过滤环节优先匹配当前租户对应包/类的级别,无匹配时 fallback 到租户根级别或全局默认级别,同时通过扩展Actuator接口实现运行时动态更新该映射。
具体实现步骤
1. 定义租户日志级别存储结构
使用线程安全的双层Map存储租户级别的包/类日志配置,确保运行时更新的线程安全:
// 全局存储:租户ID -> {包/类路径: 日志级别} private static final ConcurrentHashMap<String, ConcurrentHashMap<String, Level>> TENANT_LOG_LEVELS = new ConcurrentHashMap<>(); // 初始化默认租户配置示例 static { // ACOMP默认根级别INFO ConcurrentHashMap<String, Level> acompConfig = new ConcurrentHashMap<>(); acompConfig.put("", Level.INFO); // 空字符串代表租户根级别 TENANT_LOG_LEVELS.put("ACOMP", acompConfig); // BCOMP默认根级别DEBUG ConcurrentHashMap<String, Level> bcompConfig = new ConcurrentHashMap<>(); bcompConfig.put("", Level.DEBUG); TENANT_LOG_LEVELS.put("BCOMP", bcompConfig); }
2. 扩展Turbo Filter的级别判断逻辑
修改现有Turbo Filter的decide方法,优先匹配当前租户的包/类级别:
@Override public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { // 从MDC获取当前租户ID String tenantId = MDC.get("tenantId"); if (tenantId == null) { return FilterReply.NEUTRAL; // 无租户标识时走全局逻辑 } // 获取该租户的日志配置 ConcurrentHashMap<String, Level> tenantConfig = TENANT_LOG_LEVELS.getOrDefault(tenantId, new ConcurrentHashMap<>()); String loggerName = logger.getName(); // 优先匹配精确的包/类级别 Level targetLevel = tenantConfig.get(loggerName); if (targetLevel == null) { // 逐级匹配父包(比如com.test.samplepackage -> com.test -> com) String parentLoggerName = loggerName; while ((parentLoggerName = parentLoggerName.substring(0, parentLoggerName.lastIndexOf('.'))) != null) { targetLevel = tenantConfig.get(parentLoggerName); if (targetLevel != null) break; } // 无匹配则用租户根级别,再无则用全局默认 if (targetLevel == null) { targetLevel = tenantConfig.getOrDefault("", Level.INFO); } } // 对比当前日志级别与目标级别,决定是否过滤 return level.isGreaterOrEqual(targetLevel) ? FilterReply.ACCEPT : FilterReply.DENY; }
3. 扩展Actuator端点实现动态更新
基于现有Actuator日志端点,新增支持租户维度的更新接口,示例如下:
@RestController @RequestMapping("/actuator/loggers") public class TenantLoggerEndpoint { @PostMapping("/{tenantId}/{loggerName}") public ResponseEntity<Void> updateTenantLoggerLevel( @PathVariable String tenantId, @PathVariable String loggerName, @RequestParam Level level) { // 获取或初始化租户配置 ConcurrentHashMap<String, Level> tenantConfig = TENANT_LOG_LEVELS.computeIfAbsent(tenantId, k -> new ConcurrentHashMap<>()); // 更新指定包/类的级别(空字符串代表租户根级别) tenantConfig.put(loggerName, level); return ResponseEntity.ok().build(); } // 可选:添加查询租户日志配置的接口 @GetMapping("/{tenantId}") public ResponseEntity<Map<String, Level>> getTenantLoggerLevels(@PathVariable String tenantId) { Map<String, Level> tenantConfig = TENANT_LOG_LEVELS.getOrDefault(tenantId, Collections.emptyMap()); return ResponseEntity.ok(tenantConfig); } }
场景示例验证
针对你提到的需求:
- 初始化ACOMP根级别为INFO,BCOMP根级别为DEBUG(通过静态初始化或启动加载)
- 调用Actuator接口设置包级别:
- 设置ACOMP的
com.test.samplepackage为DEBUG:curl -X POST "http://your-app/actuator/loggers/ACOMP/com.test.samplepackage?level=DEBUG" - 设置BCOMP的
com.test.samplepackage为ERROR:curl -X POST "http://your-app/actuator/loggers/BCOMP/com.test.samplepackage?level=ERROR"
- 设置ACOMP的
- 日志过滤逻辑会自动生效:
- ACOMP的
com.test.samplepackage类日志会输出DEBUG及以上级别,其他类输出INFO及以上 - BCOMP的
com.test.samplepackage类日志仅输出ERROR及以上级别,其他类输出DEBUG及以上
- ACOMP的
注意事项
- 线程安全:必须使用线程安全的容器(如
ConcurrentHashMap)存储配置,避免多线程更新时的并发问题 - 性能优化:可对常用的租户-包级别映射做本地缓存,减少Turbo Filter中的字符串拆分匹配次数
- 持久化:若需重启应用后保留配置,可将
TENANT_LOG_LEVELS的内容持久化到数据库或配置中心,启动时加载
内容的提问来源于stack exchange,提问作者Manjunath
相关产品推荐
相关产品推荐

