Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
*
* <p>字段顺序:{@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} 是<b>部署级字段</b>,来自全局配置
* ({@link ObsLogging} 静态访问器,不依赖线程本地 MDC),恒非空;{@code community}
* 在请求线程上取 MDC 请求级覆盖,未覆盖回退部署默认。其余请求级字段
* ({@code request_id / trace_id / span_id})由中间件经 {@link ObsLogging#enrich}
* 写入 MDC,空值一律省略({@code trace_id} / {@code span_id} 为二期预留,首期通常不出现)。
*
* <p>业务字段不属于本 provider 的职责:接入方可在本 provider 之后追加
* {@code <keyValuePairs/>} 或 {@code <mdc/>} 等 provider,输出会落在固定字段之后。
Expand All @@ -44,12 +46,11 @@ public class ObsJsonProvider extends AbstractJsonProvider<ILoggingEvent> {
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 中属于契约的<b>请求级</b>键,按契约顺序输出;空值省略。
* 部署级字段(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,
Expand All @@ -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<String, String> 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);
Expand Down
38 changes: 25 additions & 13 deletions java/src/main/java/io/opensourceways/obssdk/log/ObsLogging.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@
import java.util.Optional;

/**
* 日志结构化装配:把部署级默认字段 + 请求级覆盖字段写入 SLF4J MDC,
* 日志结构化装配:持有全局部署级配置,并把请求级覆盖字段写入 SLF4J MDC,
* 由接入服务的 logback JSON encoder(logstash-logback-encoder,见
* {@code examples/logback-json.xml})输出为单行 JSON。
*
* <p>固定键:{@code service / env / instance / community / request_id / trace_id / span_id}
* (与 spec/common-fields.md、spec/log-format.md 对齐);{@code trace_id} / {@code span_id}
* 为二期 trace 预留位,首期恒空、不写入 MDC。
*
* <p>community 双层注入与其它语言 SDK 一致:部署级默认来自 {@code OBS_*}/Config,
* 请求级由中间件在可信判定点解析后经 {@link RequestContext#push} 写入,
* 本助手在取数时用请求级覆盖默认(同 metrics 的 resolve 语义)。
* <p>部署级字段({@code service / env / instance / community} 默认值)本质是全局静态配置
* (来源 {@code OBS_*} / Config),<b>不随请求线程变化</b>,因此不走线程本地 MDC ——
* {@link ObsJsonProvider} 直接从本类的全局静态访问器取数。MDC 只承载请求级字段:
* {@code community} 请求覆盖(可信判定点解析后经 {@link RequestContext#push} 写入)
* 优先于部署默认,{@code request_id} 由中间件注入。
*/
public final class ObsLogging {

Expand All @@ -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()));
}

/**
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions java/src/test/java/io/opensourceways/obssdk/ObsLoggingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Loading