From 1319b5055e8321afd531f43ce848e577f2ad0204 Mon Sep 17 00:00:00 2001 From: wyhhai <2509326799@qq.com> Date: Sun, 20 Sep 2026 11:36:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(java):=20=E9=83=A8=E7=BD=B2=E7=BA=A7?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E4=BB=8E=E5=85=A8=E5=B1=80=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=EF=BC=8C=E4=B8=8D=E5=86=8D=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E6=9C=AC=E5=9C=B0=20MDC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 请求线程打日志时 service/env/instance 曾因 init() 只写启动线程 MDC 而整条缺失; ObsJsonProvider 改为直接读 ObsLogging 全局静态访问器(未 init 回退契约默认), community 保持 MDC 请求级覆盖 > 部署默认语义。补新线程回归测试。 --- .../obssdk/log/ObsJsonProvider.java | 32 +++++++++----- .../opensourceways/obssdk/log/ObsLogging.java | 38 ++++++++++------ .../obssdk/ObsJsonProviderTest.java | 43 +++++++++++++++++++ .../opensourceways/obssdk/ObsLoggingTest.java | 25 +++++++++++ 4 files changed, 115 insertions(+), 23 deletions(-) diff --git a/java/src/main/java/io/opensourceways/obssdk/log/ObsJsonProvider.java b/java/src/main/java/io/opensourceways/obssdk/log/ObsJsonProvider.java index 2d6e843..7ae715c 100644 --- a/java/src/main/java/io/opensourceways/obssdk/log/ObsJsonProvider.java +++ b/java/src/main/java/io/opensourceways/obssdk/log/ObsJsonProvider.java @@ -31,9 +31,11 @@ * *

字段顺序:{@code time / level / msg / service / env / instance / community / * request_id / trace_id / span_id / logger / error}。其中 - * {@code service / env / instance / community} 来自 {@link ObsLogging#init} 写入的 MDC, - * 其余请求级字段由中间件经 {@link ObsLogging#enrich} 写入,空值一律省略 - * ({@code trace_id} / {@code span_id} 为二期预留,首期通常不出现)。 + * {@code service / env / instance / community} 是部署级字段,来自全局配置 + * ({@link ObsLogging} 静态访问器,不依赖线程本地 MDC),恒非空;{@code community} + * 在请求线程上取 MDC 请求级覆盖,未覆盖回退部署默认。其余请求级字段 + * ({@code request_id / trace_id / span_id})由中间件经 {@link ObsLogging#enrich} + * 写入 MDC,空值一律省略({@code trace_id} / {@code span_id} 为二期预留,首期通常不出现)。 * *

业务字段不属于本 provider 的职责:接入方可在本 provider 之后追加 * {@code } 或 {@code } 等 provider,输出会落在固定字段之后。 @@ -44,12 +46,11 @@ public class ObsJsonProvider extends AbstractJsonProvider { private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withZone(ZoneOffset.UTC); - /** MDC 中属于固定契约的键,按契约顺序输出;空值省略。 */ - private static final String[] MDC_CONTRACT_KEYS = { - ObsLogging.MDC_SERVICE, - ObsLogging.MDC_ENV, - ObsLogging.MDC_INSTANCE, - ObsLogging.MDC_COMMUNITY, + /** + * MDC 中属于契约的请求级键,按契约顺序输出;空值省略。 + * 部署级字段(service/env/instance/community 默认)不走 MDC,见 {@link ObsLogging}。 + */ + private static final String[] MDC_REQUEST_KEYS = { ObsLogging.MDC_REQUEST_ID, ObsLogging.MDC_TRACE_ID, ObsLogging.MDC_SPAN_ID, @@ -61,8 +62,19 @@ public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOExcep generator.writeStringField("level", levelName(event.getLevel())); generator.writeStringField("msg", event.getFormattedMessage()); + // 部署级字段来自全局静态配置,不随请求线程变化:直接取 ObsLogging 访问器, + // 避免 web 容器工作线程的 MDC 缺失导致整条字段消失。 + generator.writeStringField("service", ObsLogging.deploymentService()); + generator.writeStringField("env", ObsLogging.deploymentEnv()); + generator.writeStringField("instance", ObsLogging.deploymentInstance()); + Map mdc = event.getMDCPropertyMap(); - for (String key : MDC_CONTRACT_KEYS) { + // community:请求级覆盖(中间件写 MDC)优先,未覆盖回退部署默认,恒非空。 + String community = mdc == null ? null : mdc.get(ObsLogging.MDC_COMMUNITY); + generator.writeStringField("community", + community != null && !community.isEmpty() ? community : ObsLogging.deploymentCommunity()); + + for (String key : MDC_REQUEST_KEYS) { String value = mdc == null ? null : mdc.get(key); if (value != null && !value.isEmpty()) { generator.writeStringField(key, value); diff --git a/java/src/main/java/io/opensourceways/obssdk/log/ObsLogging.java b/java/src/main/java/io/opensourceways/obssdk/log/ObsLogging.java index d28c0e0..2bf68ab 100644 --- a/java/src/main/java/io/opensourceways/obssdk/log/ObsLogging.java +++ b/java/src/main/java/io/opensourceways/obssdk/log/ObsLogging.java @@ -8,7 +8,7 @@ import java.util.Optional; /** - * 日志结构化装配:把部署级默认字段 + 请求级覆盖字段写入 SLF4J MDC, + * 日志结构化装配:持有全局部署级配置,并把请求级覆盖字段写入 SLF4J MDC, * 由接入服务的 logback JSON encoder(logstash-logback-encoder,见 * {@code examples/logback-json.xml})输出为单行 JSON。 * @@ -16,9 +16,11 @@ * (与 spec/common-fields.md、spec/log-format.md 对齐);{@code trace_id} / {@code span_id} * 为二期 trace 预留位,首期恒空、不写入 MDC。 * - *

community 双层注入与其它语言 SDK 一致:部署级默认来自 {@code OBS_*}/Config, - * 请求级由中间件在可信判定点解析后经 {@link RequestContext#push} 写入, - * 本助手在取数时用请求级覆盖默认(同 metrics 的 resolve 语义)。 + *

部署级字段({@code service / env / instance / community} 默认值)本质是全局静态配置 + * (来源 {@code OBS_*} / Config),不随请求线程变化,因此不走线程本地 MDC —— + * {@link ObsJsonProvider} 直接从本类的全局静态访问器取数。MDC 只承载请求级字段: + * {@code community} 请求覆盖(可信判定点解析后经 {@link RequestContext#push} 写入) + * 优先于部署默认,{@code request_id} 由中间件注入。 */ public final class ObsLogging { @@ -38,10 +40,6 @@ private ObsLogging() { /** 全局初始化一次:登记部署级默认字段(通常服务启动时调用)。 */ public static void init(ObsSdkConfig config) { cfg = config; - MDC.put(MDC_SERVICE, nvl(config.service())); - MDC.put(MDC_ENV, nvl(config.envName())); - MDC.put(MDC_INSTANCE, nvl(config.instance())); - MDC.put(MDC_COMMUNITY, nvl(config.community())); } /** @@ -90,13 +88,27 @@ public static void clearRequestScope() { MDC.put(MDC_COMMUNITY, deploymentCommunity()); } - /** 部署级默认 community;{@code init} 未调用时退到契约默认(见 internal/Env)。 */ - private static String deploymentCommunity() { + /** 部署级 service;{@code init} 未调用时回退契约默认(见 internal/Env)。 */ + public static String deploymentService() { ObsSdkConfig current = cfg; - return current != null && current.community() != null ? current.community() : Env.DEFAULT_VALUE; + return current != null && current.service() != null ? current.service() : Env.DEFAULT_VALUE; } - private static String nvl(String v) { - return v == null ? "" : v; + /** 部署级 env;{@code init} 未调用时回退契约默认(见 internal/Env)。 */ + public static String deploymentEnv() { + ObsSdkConfig current = cfg; + return current != null && current.envName() != null ? current.envName() : Env.DEFAULT_VALUE; + } + + /** 部署级 instance;{@code init} 未调用时回退契约默认(见 internal/Env)。 */ + public static String deploymentInstance() { + ObsSdkConfig current = cfg; + return current != null && current.instance() != null ? current.instance() : Env.DEFAULT_VALUE; + } + + /** 部署级默认 community;{@code init} 未调用时回退契约默认(见 internal/Env)。 */ + public static String deploymentCommunity() { + ObsSdkConfig current = cfg; + return current != null && current.community() != null ? current.community() : Env.DEFAULT_VALUE; } } diff --git a/java/src/test/java/io/opensourceways/obssdk/ObsJsonProviderTest.java b/java/src/test/java/io/opensourceways/obssdk/ObsJsonProviderTest.java index ccee651..658a198 100644 --- a/java/src/test/java/io/opensourceways/obssdk/ObsJsonProviderTest.java +++ b/java/src/test/java/io/opensourceways/obssdk/ObsJsonProviderTest.java @@ -221,6 +221,49 @@ void tearDown() { assertFalse(lastLine().contains("\"error\""), lastLine()); } + /** + * 缺陷回归:部署级字段曾只写入 {@code init()} 所在线程的 MDC,web 容器请求线程 + * 拿不到 service/env/instance,导致整条字段消失。修复后部署级字段来自全局配置, + * 请求线程即使只写请求级 MDC 也完整输出。 + */ + @Test + void 请求线程模拟_部署级字段来自全局配置而非线程MDC() throws Exception { + Thread request = new Thread(() -> { + MDC.put(ObsLogging.MDC_COMMUNITY, "mindspore"); + MDC.put(ObsLogging.MDC_REQUEST_ID, "req-9"); + LoggerFactory.getLogger("com.x.ReviewSvc").info("job done"); + }); + request.start(); + request.join(); + + String json = lastLine(); + assertTrue(json.contains("\"service\":\"review\""), json); + assertTrue(json.contains("\"env\":\"test\""), json); + assertTrue(json.contains("\"instance\":\"pod-1\""), json); + // community 保持「MDC 请求级覆盖 > 部署默认」语义 + assertTrue(json.contains("\"community\":\"mindspore\""), json); + assertTrue(json.contains("\"request_id\":\"req-9\""), json); + } + + /** + * 异步/定时线程不打请求级 MDC 时,部署级字段(含 community)也必须输出部署默认, + * 而不是整条从 JSON 里消失(契约要求四个部署级字段恒非空)。 + */ + @Test + void 新线程无请求级MDC时_部署级字段回退部署默认() throws Exception { + Thread async = new Thread(() -> + LoggerFactory.getLogger("com.x.ReviewSvc").info("cron tick")); + async.start(); + async.join(); + + String json = lastLine(); + assertTrue(json.contains("\"service\":\"review\""), json); + assertTrue(json.contains("\"env\":\"test\""), json); + assertTrue(json.contains("\"instance\":\"pod-1\""), json); + assertTrue(json.contains("\"community\":\"openEuler\""), json); + assertFalse(json.contains("request_id"), json); + } + private String line(int index) { String[] lines = captured.toString(StandardCharsets.UTF_8).split("\n"); assertTrue(lines.length > index, "日志行数不足:" + captured); diff --git a/java/src/test/java/io/opensourceways/obssdk/ObsLoggingTest.java b/java/src/test/java/io/opensourceways/obssdk/ObsLoggingTest.java index 68854f7..32bf41a 100644 --- a/java/src/test/java/io/opensourceways/obssdk/ObsLoggingTest.java +++ b/java/src/test/java/io/opensourceways/obssdk/ObsLoggingTest.java @@ -82,4 +82,29 @@ void tearDown() { "应复位为部署默认,而不是残留上一个请求的覆盖值"); } } + + @Test + void 部署级静态访问器返回init登记的值() { + assertEquals("review", ObsLogging.deploymentService()); + assertEquals("test", ObsLogging.deploymentEnv()); + assertEquals("pod-1", ObsLogging.deploymentInstance()); + assertEquals("openeuler", ObsLogging.deploymentCommunity()); + } + + /** + * 缺陷回归:部署级字段一旦只存在于线程本地 MDC 就会随线程消失;访问器是全局静态 + * 取值,{@code init} 未调用时仍须回退契约默认("unknown"),保证 provider 恒能 + * 输出非空部署级字段。 + */ + @Test + void 未init时部署级访问器回退契约默认() throws Exception { + java.lang.reflect.Field cfgField = ObsLogging.class.getDeclaredField("cfg"); + cfgField.setAccessible(true); + cfgField.set(null, null); + + assertEquals("unknown", ObsLogging.deploymentService()); + assertEquals("unknown", ObsLogging.deploymentEnv()); + assertEquals("unknown", ObsLogging.deploymentInstance()); + assertEquals("unknown", ObsLogging.deploymentCommunity()); + } }