Java日志SDK零侵入实现方案
作者:小沈同学呀
一、传统日志埋点的痛
本人不才,之前做业务开发的时候,每次加日志都要在代码里插那么几行,插着插着就变这样了:
public Order create(CreateOrderReq req) {
String traceId = MDC.get("traceId"); // 先取
log.info("[{}] 收到创建订单请求, userId={}", traceId, req.getUserId());
// 中间省略一万行...
log.info("[{}] 订单创建完成, orderId={}", traceId, order.getId());
}
是不是贼眼熟?所以说传统埋点有几个绕不开的坑:
1.业务代码被污染 —— 满屏都是 traceId 的取值和传递,业务逻辑反而看不清
2.改需求就改日志 —— 产品说"我们要加上游调用方信息",研发就得满世界找改
3.特殊场景根本搞不定 —— MQ 消费、定时任务、异步线程池,这些场景 traceId 怎么传递?手工传的话能把人累死
本文不敢班门弄斧,就分享一下我们是怎么用顶级 Filter + Span 机制实现零侵入日志采集的。

二、我们的方案:顶级 Filter + Span 机制
2.1 Filter 自动注入,告别手写 traceId
核心思路是这样的:所有请求都从 Filter 进,我们在 Filter 里把 traceId 和场景值一次性注入到 MDC,后续业务代码正常写 log 就完事了。
@Component
public class LogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws Exception {
// 1. 从请求头拿 traceId,没有就新生成
String traceId = request.getHeader(traceIdHeader);
if (traceId == null) {
traceId = UUID.randomUUID().toString();
}
// 2. 创建根上下文
LogContext.createRoot(traceId, "upstream_ingress",
request.getRequestURI(), "HTTP");
// 3. 响应头回写 traceId,方便前端排查
if (responseTraceIdEnabled) {
response.setHeader(traceIdHeader, traceId);
}
try {
chain.doFilter(request, response);
} finally {
LogContext.clear(); // 清理,避免内存泄漏
}
}
}
然后业务代码直接写:
log.info(“收到创建订单请求, userId={}”, req.getUserId()); // 自动带 traceId
香! 不用取、不用传,Filter 都给你安排好了。
2.2 Scene 场景值:串联上下游调用的关键
这是最精髓的设计——我们用 scene 这个字段表示当前调用处于哪个阶段:
| 场景值 | 含义 |
|---|---|
| upstream_ingress | 网关入口进来 |
| downstream_egress | 调用下游服务/第三方 |
| mq_consume | MQ 消费 |
| scheduled | 定时任务 |
问题来了:内部调用下游服务时,scene 怎么变成 downstream_egress?
答案:Span.try-with-resources,自动切换、自动恢复:
@Service
public class OrderService {
public Order create(CreateOrderReq req) {
log.info("开始校验参数"); // scene=upstream_ingress
// 调用下游支付网关,用 try-with-resources 包裹
try (Span span = LogTracer.span("downstream_egress")) {
log.info("调用支付网关预扣款"); // scene 临时变为 downstream_egress
PaymentResult result = paymentClient.preDeduct(req);
log.info("支付网关返回, status={}", result.getStatus());
} // 自动恢复到 upstream_ingress
log.info("写入订单数据"); // scene 正确恢复到 upstream_ingress!
return orderDao.save(buildOrder(req, user));
}
}
看下控制台输出的日志,感受一下完整的调用链:
// 第一条:入口
{"traceId":"abc123","scene":"upstream_ingress","message":"收到创建订单请求"}
// 第二条:下游调用
{"traceId":"abc123","scene":"downstream_egress","message":"调用支付网关预扣款"}
// 第三条:恢复
{"traceId":"abc123","scene":"upstream_ingress","message":"写入订单数据"}
一条 traceId 串起完整链路,哪个环节出问题一眼就能定位。
三、真香现场:特殊场景怎么办
MQ 消费、定时任务、异步线程池——这三个场景顶级 Filter 可不管,得咱们自己动手。不过别担心,都有现成的方案。
3.1 MQ 消费者手动埋点
@Component
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")
public class OrderMessageListener implements RocketMQListener<OrderMessage> {
private static final Logger log = LoggerFactory.getLogger(OrderMessageListener.class);
@Override
public void onMessage(OrderMessage message) {
// 从消息头提取 traceId,或新生成
String traceId = message.getTraceId();
// 手动创建根上下文,scene 为 mq_consume
LogContext.createRootWithTraceId(traceId, "mq_consume",
message.getTopic() + "/" + message.getTags(), "consume");
try {
log.info("收到订单消息, orderId={}", message.getOrderId());
processOrder(message);
log.info("订单消息处理完成");
} finally {
LogContext.clear(); // 切记清理
}
}
}
3.2 定时任务手动埋点
@Component
public class DailyReportJob {
private static final Logger log = LoggerFactory.getLogger(DailyReportJob.class);
@Scheduled(cron = "0 0 2 * * ?")
public void generateReport() {
// 用 LogTracer.run 包裹,自动创建根上下文
LogTracer.run("scheduled", () -> {
log.info("开始生成日报"); // traceId=新生成, scene=scheduled
List<Order> orders = orderDao.findYesterdayOrders();
log.info("查询到 {} 条订单", orders.size()); // 自动继承
// 还可以嵌套子 Span
try (Span span = LogTracer.span("downstream_egress")) {
log.info("上传报表到 OSS"); // scene=downstream_egress
ossClient.upload(buildReport(orders));
}
log.info("日报生成完成"); // scene 恢复为 scheduled
});
}
}
3.3 线程池上下文传递
异步任务最头疼的就是上下文丢失,不过我们有 LogTracer.wrap:
@Service
public class AsyncService {
@Autowired
private ThreadPoolExecutor executor;
public void asyncProcess(List<Order> orders) {
for (Order order : orders) {
// 包装后提交,上下文自动传递
executor.submit(LogTracer.wrap(() -> {
log.info("异步处理订单, orderId={}", order.getId());
// 自动继承主线程的 traceId 和 scene
}));
}
}
}
四、上效果!控制台日志 Demo 对比
来,直接上控制台输出对比,看看零侵入前后的差距:
开启前:普通文本日志
2026-05-28 10:30:15.123 [http-nio-48080-exec-1] INFO c.c.o.OrderController:23 - 收到创建订单请求, userId=10001 2026-05-28 10:30:15.456 [http-nio-48080-exec-1] INFO c.c.o.OrderService:45 - 开始校验参数 2026-05-28 10:30:16.789 [http-nio-48080-exec-1] INFO c.c.o.PaymentClient:67 - 调用支付网关预扣款 2026-05-28 10:30:17.012 [http-nio-48080-exec-1] INFO c.c.o.OrderService:78 - 写入订单数据
问题:只能看到时间顺序,根本不知道谁调用了谁,排查问题时要在几万行日志里慢慢捞。
开启后:结构化 JSON
{"timestamp":"2026-05-28T10:30:15.123Z","level":"INFO","traceId":"abc123","scene":"upstream_ingress","message":"收到创建订单请求, userId=10001","service":"order-service","env":"prod"}
{"timestamp":"2026-05-28T10:30:15.456Z","level":"INFO","traceId":"abc123","scene":"upstream_ingress","message":"开始校验参数","service":"order-service","env":"prod"}
{"timestamp":"2026-05-28T10:30:16.789Z","level":"INFO","traceId":"abc123","scene":"downstream_egress","message":"调用支付网关预扣款","service":"order-service","env":"prod","duration_ms":223}
{"timestamp":"2026-05-28T10:30:17.012Z","level":"INFO","traceId":"abc123","scene":"upstream_ingress","message":"写入订单数据, orderId=ORDER_999","service":"order-service","env":"prod"}
香! 带上 traceId、scene、duration_ms,按 traceId 一过滤,完整调用链就出来了,哪个环节耗时多也一目了然。
五、SDK 搭建步骤
手痒想自己搭一个试试?这就带你跑通!从零开始,一个文件一个文件来实现。
5.1 创建 Maven 项目
mvn archetype:generate -DgroupId=cn.cce -DartifactId=java-logging-sdk -Dversion=1.0.0-SNAPSHOT -Dpackage=cn.cce.javaloggingsdk
5.2 pom.xml 依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>5.3 核心代码实现
下面一步一步来实现每个文件。
Step 1: 场景枚举 - LoggingSceneEnum.java
定义所有可能的调用场景:
/**
* LoggingSceneEnum
*
* @author senfel
* @version 1.0
* @date 2026/5/28 14:07
*/
public enum LoggingSceneEnum {
UPSTREAM_INGRESS("upstream_ingress"), // 网关入口
DOWNSTREAM_EGRESS("downstream_egress"), // 调用下游
INTERNAL("internal"), // 内部业务
SCHEDULED("scheduled"), // 定时任务
MQ_CONSUME("mq_consume"), // MQ 消费
MQ_PRODUCE("mq_produce"), // MQ 生产
ASYNC_TASK("async_task"); // 异步任务
private final String value;
LoggingSceneEnum(String value) { this.value = value; }
public String getValue() { return value; }
}
Step 2: 配置属性 - LogProperties.java
支持 application.yml 中灵活配置:
/**
* LogProperties
*
* @author senfel
* @version 1.0
* @date 2026/5/27 17:27
*/
@ConfigurationProperties(prefix = "logging.sdk")
public class LogProperties {
private boolean enabled = true; // 总开关
private String service = "unknown"; // 服务名
private String env = "unknown"; // 环境
private String traceIdHeader = "X-Trace-Id"; // 透传 Key
private boolean responseTraceIdEnabled = true; // 响应头回写
private boolean consoleEnabled = true; // 控制台 JSON 输出
private boolean remoteEnabled = false; // 远程上报
private String remoteUrl = "http://localhost:8080/api/collect";
private int remoteBatchSize = 100;
// getters & setters 省略...
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getService() { return service; }
public void setService(String service) { this.service = service; }
public String getEnv() { return env; }
public void setEnv(String env) { this.env = env; }
public String getTraceIdHeader() { return traceIdHeader; }
public void setTraceIdHeader(String traceIdHeader) { this.traceIdHeader = traceIdHeader; }
public boolean isResponseTraceIdEnabled() { return responseTraceIdEnabled; }
public void setResponseTraceIdEnabled(boolean responseTraceIdEnabled) { this.responseTraceIdEnabled = responseTraceIdEnabled; }
public boolean isConsoleEnabled() { return consoleEnabled; }
public void setConsoleEnabled(boolean consoleEnabled) { this.consoleEnabled = consoleEnabled; }
public boolean isRemoteEnabled() { return remoteEnabled; }
public void setRemoteEnabled(boolean remoteEnabled) { this.remoteEnabled = remoteEnabled; }
public String getRemoteUrl() { return remoteUrl; }
public void setRemoteUrl(String remoteUrl) { this.remoteUrl = remoteUrl; }
public int getRemoteBatchSize() { return remoteBatchSize; }
public void setRemoteBatchSize(int remoteBatchSize) { this.remoteBatchSize = remoteBatchSize; }
}
Step 3: 日志上下文 - LogContext.java
整个 SDK 的核心,所有上下文都存这里:
/**
* LogContext
*
* @author senfel
* @version 1.0
* @date 2026/5/27 17:29
*/
public class LogContext {
private static final String TRACE_ID = "traceId";
private static final String SPAN_ID = "spanId";
private static final String PARENT_SPAN_ID = "parentSpanId";
private static final String SCENE = "scene";
private static final String PATH = "path";
private static final String METHOD = "method";
private static final String START_TIME = "startTime";
/** 顶级埋点:新建上下文 */
public static void createRoot(String scene, String path, String method) {
clear();
MDC.put(TRACE_ID, generateTraceId());
MDC.put(SCENE, scene);
MDC.put(PATH, path);
MDC.put(METHOD, method);
MDC.put(START_TIME, String.valueOf(System.currentTimeMillis()));
}
/** 从上游透传 traceId */
public static void createRootWithTraceId(String traceId, String scene, String path, String method) {
clear();
MDC.put(TRACE_ID, traceId != null ? traceId : generateTraceId());
MDC.put(SCENE, scene);
MDC.put(PATH, path);
MDC.put(METHOD, method);
MDC.put(START_TIME, String.valueOf(System.currentTimeMillis()));
}
/** 获取快照(跨线程传递用) */
public static Map<String, String> getSnapshot() {
return MDC.getCopyOfContextMap();
}
/** 恢复快照 */
public static void restoreSnapshot(Map<String, String> snapshot) {
if (snapshot != null) MDC.setContextMap(snapshot);
}
public static String getTraceId() { return MDC.get(TRACE_ID); }
public static String getScene() { return MDC.get(SCENE); }
public static Long getStartTime() {
String st = MDC.get(START_TIME);
return st != null ? Long.parseLong(st) : null;
}
public static void clear() { MDC.clear(); }
private static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
}
Step 4: Span 作用域 - Span.java
用 try-with-resources 自动切换/恢复 scene:
/**
* Span
*
* @author senfel
* @version 1.0
* @date 2026/5/27 17:30
*/
public class Span implements AutoCloseable {
private final String previousScene;
private final String previousSpanId;
private final String spanId;
private final long startTime;
public Span(String kind) {
this.spanId = Long.toHexString(System.nanoTime());
this.previousScene = LogContext.getScene();
this.previousSpanId = MDC.get("spanId");
this.startTime = System.currentTimeMillis();
// 切换为新的 scene
MDC.put("scene", kind);
MDC.put("spanId", spanId);
if (previousSpanId != null) MDC.put("parentSpanId", previousSpanId);
MDC.put("spanStartTime", String.valueOf(startTime));
}
public long getDurationMs() {
return System.currentTimeMillis() - startTime;
}
@Override
public void close() {
// 恢复到之前的 scene
if (previousScene != null) {
MDC.put("scene", previousScene);
} else {
MDC.remove("scene");
}
if (previousSpanId != null) {
MDC.put("spanId", previousSpanId);
} else {
MDC.remove("spanId");
}
MDC.remove("parentSpanId");
MDC.remove("spanStartTime");
}
}
Step 5: 追踪 器入口 - LogTracer.java
给业务代码用的 API:
/**
* LogTracer
*
* @author senfel
* @version 1.0
* @date 2026/5/27 17:31
*/
public class LogTracer {
/** 无返回值场景(定时任务等) */
public static void run(String scene, Runnable task) {
LogContext.createRoot(scene, null, null);
try {
task.run();
} finally {
LogContext.clear();
}
}
/** 有返回值场景 */
public static <T> T call(String scene, Callable<T> task) throws Exception {
LogContext.createRoot(scene, null, null);
try {
return task.call();
} finally {
LogContext.clear();
}
}
/** 内部嵌套 Span(下游调用) */
public static Span span(String kind) {
return new Span(kind);
}
/** 跨线程传递(线程池) */
public static Runnable wrap(Runnable task) {
Map<String, String> snapshot = LogContext.getSnapshot();
return () -> {
LogContext.restoreSnapshot(snapshot);
try {
task.run();
} finally {
LogContext.clear();
}
};
}
}
Step 6: HTTP 过滤器 - LogFilter.java
顶级埋点核心,请求进来时自动注入:
/**
* LogFilter
*
* @author senfel
* @version 1.0
* @date 2026/5/27 17:33
*/
public class LogFilter extends OncePerRequestFilter {
private final LogProperties properties;
public LogFilter(LogProperties properties) {
this.properties = properties;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 1. 从请求头拿 traceId,没有就新生成
String traceId = request.getHeader(properties.getTraceIdHeader());
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
// 2. 创建根上下文
LogContext.createRootWithTraceId(
traceId,
LoggingSceneEnum.UPSTREAM_INGRESS.getValue(),
request.getRequestURI(),
request.getMethod()
);
try {
// 3. 响应头回写 traceId
if (properties.isResponseTraceIdEnabled()) {
response.setHeader(properties.getTraceIdHeader(), traceId);
}
chain.doFilter(request, response);
} finally {
LogContext.clear(); // 清理!
}
}
}
Step 7: 日志数据构建 - LogDataBuilder.java
把 Logback 事件转成 JSON 数据:
/**
* LogDataBuilder
*
* @author senfel
* @date 2026/5/28 11:10
*/
public class LogDataBuilder {
public static Map<String, Object> buildLogData(ILoggingEvent event, String service, String env) {
Map<String, Object> log = new LinkedHashMap<>();
// 基础字段
log.put("timestamp", java.time.Instant.ofEpochMilli(event.getTimeStamp()).toString());
log.put("level", event.getLevel().toString());
log.put("message", event.getFormattedMessage());
// MDC 上下文
log.put("traceId", MDC.get("traceId"));
log.put("spanId", MDC.get("spanId"));
log.put("parentSpanId", MDC.get("parentSpanId"));
log.put("scene", MDC.get("scene"));
log.put("path", MDC.get("path"));
log.put("method", MDC.get("method"));
// 耗时计算
String spanStart = MDC.get("spanStartTime");
if (spanStart != null) {
log.put("duration_ms", System.currentTimeMillis() - Long.parseLong(spanStart));
} else {
String traceStart = MDC.get("startTime");
if (traceStart != null) {
log.put("duration_ms", System.currentTimeMillis() - Long.parseLong(traceStart));
}
}
// 系统字段
log.put("host", getHostname());
log.put("service", service);
log.put("env", env);
log.put("thread", event.getThreadName());
// 异常信息
if (event.getThrowableProxy() != null) {
log.put("error", event.getThrowableProxy().getMessage());
}
return log;
}
private static String getHostname() {
try {
return java.net.InetAddress.getLocalHost().getHostName();
} catch (Exception e) {
return "unknown";
}
}
}
Step 8: JSON 布局 - StructuredLayout.java
Logback 专用的 Layout,把日志转 JSON 输出:
/**
* StructuredLayout
*
* @author senfel
* @version 1.0
* @date 2026/5/27 17:32
*/
public class StructuredLayout extends LayoutBase<ILoggingEvent> {
private final ObjectMapper mapper = new ObjectMapper();
private String service = "unknown";
private String env = "unknown";
@Override
public String doLayout(ILoggingEvent event) {
Map<String, Object> log = LogDataBuilder.buildLogData(event, service, env);
try {
return mapper.writeValueAsString(log) + "\n";
} catch (Exception e) {
return "{\"error\":\"json_serialize_failed\"}\n";
}
}
public void setService(String service) { this.service = service; }
public void setEnv(String env) { this.env = env; }
}
Step 9: 远程 - RemoteReportAppender.java
将日志事件推送远程服务:
/**
* RemoteReportAppender
*
* @author senfel
* @version 1.0
* @date 2026/5/27 17:33
*/
public class RemoteReportAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
private HttpTransport transport;
private String remoteUrl;
private int batchSize = 100;
private String service = "unknown";
private String env = "unknown";
private final List<Map<String, Object>> buffer = new ArrayList<>();
@Override
public void start() {
transport = new HttpTransport(remoteUrl);
super.start();
}
@Override
protected void append(ILoggingEvent event) {
Map<String, Object> log = buildLogMap(event);
buffer.add(log);
if (buffer.size() >= batchSize) {
flush();
}
}
private void flush() {
if (buffer.isEmpty()){
return;
}
try {
transport.sendBatch(new ArrayList<>(buffer));
buffer.clear();
} catch (Exception e) {
addError("Remote report failed", e);
}
}
@Override
public void stop() {
flush();
super.stop();
}
public void setRemoteUrl(String url) { this.remoteUrl = url; }
public void setBatchSize(int batchSize) { this.batchSize = batchSize; }
// setter for service/env config
public void setService(String service) { this.service = service; }
public void setEnv(String env) { this.env = env; }
private Map<String, Object> buildLogMap(ILoggingEvent event) {
// 使用公共方法构建日志数据
return LogDataBuilder.buildLogData(event, this.service, this.env);
}
}
OkHttpClient 推送类:
/**
* HttpTransport
*
* @author senfel
* @version 1.0
* @date 2026/5/27 17:43
*/
public class HttpTransport {
private final OkHttpClient client = new OkHttpClient();
private final String remoteUrl;
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
public HttpTransport(String remoteUrl) {
this.remoteUrl = remoteUrl;
}
public void sendBatch(List<Map<String, Object>> logs) throws Exception {
// 若引入了 jackson,可序列化 logs 为 JSON
String json = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(logs);
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(remoteUrl)
.post(body)
.build();
client.newCall(request).execute();
}
}
Step 10: 自动装配 - LogAutoConfiguration.java
最后把一切都组装起来:
/**
* LogAutoConfiguration
*
* @author senfel
* @version 1.0
* @date 2026/5/27 17:29
*/
@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(LogProperties.class)
@ConditionalOnProperty(
prefix = "logging.sdk",
name = "enabled",
havingValue = "true",
matchIfMissing = true // 默认开启
)
public class LogAutoConfiguration {
private final LogProperties properties;
public LogAutoConfiguration(LogProperties properties) {
this.properties = properties;
}
@Bean
public FilterRegistrationBean<LogFilter> logFilter() {
FilterRegistrationBean<LogFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new LogFilter(properties));
bean.addUrlPatterns("/*");
bean.setOrder(Integer.MIN_VALUE); // 最先执行
return bean;
}
}
5.4 自动装配配置 sprinbgboot3
创建 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
cn.cce.javaloggingsdk.config.LogAutoConfiguration
这一步很重要,没有这文件 Spring Boot 可不认识你的自动装配。
5.4 一键打包
mvn clean package -DskipTests
打包成功后会得到 target/java-logging-sdk-0.0.1-SNAPSHOT.jar,丢到你的 Spring Boot 项目里就能用了。
六、快速上手指南
这节是给使用者看的,如果你想自己开发改造 SDK,请看第五章。
6.1 Maven 依赖
<!-- 有仓库的情况下,引入 Starter 即可,自动装配 Filter 和 Appender -->
<dependency>
<groupId>cn.cce</groupId>
<artifactId>java-logging-sdk</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- local logging sdk require -->
<dependency>
<groupId>cn.cce</groupId>
<artifactId>java-logging-sdk</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>system</scope>
<systemPath>${basedir}/libs/java-logging-sdk-0.0.1-SNAPSHOT.jar</systemPath>
</dependency>6.2 application.yml 配置
logging:
sdk:
# 总开关:false 时 SDK 完全关闭,Filter / Layout / 远程上报均不生效
enabled: true
# 应用标识
service: ${spring.application.name}
env: ${spring.profiles.active}
# 透传 Key 自定义:适配企业网关规范(如阿里云 MSE 用 X-Request-Id,自研网关用 Trace-ID)
traceIdHeader: X-Trace-Id
# 是否在响应头回写 traceId,方便前端排查
responseTraceIdEnabled: true
# 控制台 JSON 输出开关
consoleEnabled: true
# 远程上报开关与地址
remoteEnabled: false
remoteUrl: http://127.0.0.1:48099/base/api/collect
remoteBatchSize: 1006.3 一键关闭
本地开发或压测时,一行配置搞定:
logging:
sdk:
enabled: false # 关闭后日志恢复正常,完全无侵入6.4 logback-spring.xml
兼容默认输出与SDK本地与上传输出配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty scope="context" name="SERVICE" source="logging.sdk.service" defaultValue="unknown"/>
<springProperty scope="context" name="ENV" source="logging.sdk.env" defaultValue="unknown"/>
<springProperty scope="context" name="CONSOLE_ENABLED" source="logging.sdk.consoleEnabled" defaultValue="true"/>
<springProperty scope="context" name="REMOTE_ENABLED" source="logging.sdk.remoteEnabled" defaultValue="false"/>
<springProperty scope="context" name="REMOTE_URL" source="logging.sdk.remoteUrl"/>
<springProperty scope="context" name="REMOTE_BATCH" source="logging.sdk.remoteBatchSize" defaultValue="100"/>
<springProperty scope="context" name="LOGGING_SDK_ENABLED" source="logging.sdk.enabled" defaultValue="false"/>
<!--默认控制台配置-->
<property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n"/>
<!-- 默认控制台 Appender -->
<if condition='!${LOGGING_SDK_ENABLED}'>
<then>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
</then>
</if>
<!-- 控制台输出 -->
<if condition='${LOGGING_SDK_ENABLED} && ${CONSOLE_ENABLED}'>
<then>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="cn.cce.javaloggingsdk.appender.StructuredLayout">
<service>${SERVICE}</service>
<env>${ENV}</env>
</layout>
</encoder>
</appender>
</then>
</if>
<!-- 远程上报(RemoteReportAppender) -->
<if condition='${LOGGING_SDK_ENABLED} && ${REMOTE_ENABLED}'>
<then>
<appender name="REMOTE" class="cn.cce.javaloggingsdk.appender.RemoteReportAppender">
<service>${SERVICE}</service>
<env>${ENV}</env>
<remoteUrl>${REMOTE_URL}</remoteUrl>
<batchSize>${REMOTE_BATCH}</batchSize>
</appender>
</then>
</if>
<!-- 根日志配置 -->
<root level="INFO">
<if condition='!${LOGGING_SDK_ENABLED}'>
<then><appender-ref ref="STDOUT"/></then>
</if>
<if condition='${LOGGING_SDK_ENABLED} && ${CONSOLE_ENABLED}'>
<then><appender-ref ref="CONSOLE"/></then>
</if>
<if condition='${LOGGING_SDK_ENABLED} &&${REMOTE_ENABLED}'>
<then><appender-ref ref="REMOTE"/></then>
</if>
</root>
</configuration>七、总结
这套方案下来,核心就三点:
1.顶级 Filter 自动注入 —— HTTP 请求进入时一次性搞定,不用业务代码操心
2.Span.try-with-resources —— 调用下游时自动切换 scene,执行完自动恢复,丝滑般流畅
3.SDK 小而美 —— 一个 Starter 引入,配置即用
适用场景建议:
- 微服务架构需全链路追踪
- 第三方调用量大需排查问题
- MQ 消费、定时任务需串联上下文
不太适用的场景:
- 单体应用,调用链简单
- 对性能极端敏感的场景(Filter 有微小开销)
以上就是Java日志SDK零侵入实现方案的详细内容,更多关于Java日志SDK零侵入的资料请关注脚本之家其它相关文章!
