java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java日志SDK零侵入

Java日志SDK零侵入实现方案

作者:小沈同学呀

还在为日志埋点污染业务代码发愁吗,本文分享零侵入日志采集方案,通过顶级Filter自动注入traceId,用Span机制无缝切换场景,轻松实现全链路追踪,覆盖MQ消费、定时任务、异步线程池等特殊场景,附完整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_consumeMQ 消费
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: 100

6.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 引入,配置即用
适用场景建议:

不太适用的场景:

以上就是Java日志SDK零侵入实现方案的详细内容,更多关于Java日志SDK零侵入的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文