Java 与 Go 服务通过 HTTP、gRPC 互调,使用一致的 JSON 日志字段和请求标识。Java 日志写入标准输出,Go 日志写入文件,由 LoongCollector 采集到 SLS。
配套代码:ARMS-Log-collect。
下载并运行两个示例服务
准备 Go 1.24、JDK 17 和 Maven 3.9。默认 Java 使用 HTTP 8080 / gRPC 9090,Go 使用 HTTP 8081 / gRPC 9091。
git clone https://github.com/Roliyal/CROlordCodelibrary.git
cd CROlordCodelibrary/Chapter6Custom/ARMS-Log-collect在两个终端分别启动服务:
# 终端一:在示例根目录执行
cd go-service
go mod tidy
go run ./...# 终端二:在示例根目录执行
cd java-service
mvn -q -DskipTests package
mvn -q spring-boot:runJava 日志写标准输出;Go 写 go-service/logs/app.log。也可以在示例根目录用已有的 docker-compose.yml 启动,注意容器内调用使用服务名。
统一日志字段
每条日志输出为一行 JSON,字段名与类型跨语言一致。下面是一个格式示例,链路标识为示意值:
{
"timestamp": "2026-11-02T02:15:30.123Z",
"level": "INFO",
"service": "order-api",
"env": "test",
"source": "OrderHandler",
"category": "order.create",
"protocol": "http",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"costMs": 12,
"message": "order created"
}| 字段 | 约定 |
|---|---|
timestamp |
统一时区与精度,推荐 ISO 8601 UTC |
level |
固定取值与大小写,避免同义值混用 |
service、env |
标识服务与环境 |
category |
稳定的业务动作或事件分类 |
traceId、spanId |
来自当前链路上下文,存在时记录 |
costMs |
数值类型,统一为毫秒 |
message |
简短说明本次发生的事情 |
方法、路由、远端服务、错误类型等按场景补充。后台任务没有链路上下文时,可以省略链路字段,不要为了满足格式填入固定值。
已有系统使用 trace_id、cost_ms 时,接入前统一映射。迁移时明确兼容期,避免同一个字段长期存在多种名字。
HTTP 与 gRPC 上下文
当前日志示例用 X-Trace-Id 及 gRPC Metadata 传递请求标识,字段名沿用代码中的 traceId。它首先解决日志关联,不等于已经接入标准分布式追踪。可以这样验证:
curl -fsS -X POST \
-H 'X-Trace-Id: logging-check-001' \
'http://127.0.0.1:8080/api/order/create?n=5'
rg 'logging-check-001' go-service/logs/app.logJava 控制台与 Go 日志应能找到同一标识,并包含 HTTP、gRPC 对应的调用分类。生产服务接入 OpenTelemetry 后,再使用当前 Span 的标准上下文。
已经接入 OpenTelemetry 的服务,直接从当前 Span 获取 Trace ID 和 Span ID,并通过 HTTP 或 gRPC 的 instrumentation 传播上下文。自行生成的 UUID 可以作为 request_id,但不能直接冒充标准 Trace ID。
Java 通过 Log4j2 ThreadContext 或日志框架 MDC 携带字段时,要保证请求结束后清理。清理放进 finally,否则异常返回可能让线程池中的下一个请求带上旧值。
异步任务还需要显式传播上下文。Go 则沿 context.Context 传递请求信息,避免用全局变量保存“当前请求”。gRPC 应通过拦截器处理入站与出站上下文,不能只创建 Metadata 却没有绑定到实际调用。
日志输出配置
Java 示例在 log4j2.xml 使用单行 JSON 模板布局,模板文件明确各字段映射:
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<JsonTemplateLayout eventTemplateUri="classpath:log-template.json"/>
</Console>
</Appenders>
<Loggers>
<Root level="${env:LOG_LEVEL:-info}">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>Go 使用 JSON Logger 和轮转文件,初始化代码位于 go-service/logger/logger.go:
writer := &lumberjack.Logger{
Filename: cfg.Path,
MaxSize: cfg.MaxSizeMB,
MaxBackups: cfg.MaxBackups,
MaxAge: cfg.MaxAgeDays,
}
base := log.NewJSONLogger(log.NewSyncWriter(writer))
base = log.With(base,
"timestamp", log.TimestampFormat(
func() time.Time { return time.Now().UTC() }, time.RFC3339Nano),
"service", "go-service",
"env", cfg.Env,
"version", cfg.Version,
)以上是代码片段,依赖由配套工程的 go.mod 管理。不要把初始化片段直接当作独立 Go 文件运行。
配置了 JSON Layout,不代表业务字段已经位于顶层。Java 中的 MapMessage、上下文字段和异常堆栈,可能按布局配置输出到不同位置;要打开实际日志检查解析结果。
Go 使用结构化 Logger 时,同样要检查数值是否被写成字符串、文件是否打开成功、错误是否被忽略。
容器服务通常优先输出到 stdout,由采集器读取。主机上的文件日志需要同时安排目录权限、轮转与保留期。一个服务不要无意中同时采集 stdout 和同一份文件,否则统计会重复。
异常堆栈可以保存在 JSON 字符串字段中,换行由序列化器转义。保持“一条事件一行”,不等于丢掉堆栈。
在 SLS 配置采集、解析和索引
- 创建或选定 Project、Logstore,设置保留时间。
- 为目标主机或容器配置 LoongCollector(Logtail)采集范围。
- 选择 JSON 解析,预览字段,确认采集器有读取与发送权限。
- 配置时间提取,核对日志事件时间与查询时区。
- 给常用字段建立索引,
costMs等分析字段使用数值类型。 - 新写入一条带唯一测试标识的日志,在目标 Logstore 查询。
采集器默认的增量读取行为需要留意。配置后没有读入旧文件,不应马上判定采集失败;先新增一条日志做验证。
预览能看到日志、查询却没有结果时,检查索引和时间范围;字段仍是一整段字符串时,检查 JSON 解析配置。文件已经轮转,还要确认路径规则能够覆盖新文件。
跨服务采集验证
先用 logging-check-001 发起跨服务调用,再到 SLS 查询相同请求标识。标准 Trace ID 的查询格式如下:
traceId: 4bf92f3577b34da6a3ce929d0e0e4736按服务统计错误数量的查询示例:
level: ERROR |
SELECT service, count(*) AS errors
GROUP BY service
ORDER BY errors DESC查询依赖对应字段已建立索引。验收还应检查同一事件有没有采集两次、异常日志能否关联原请求,以及采样或限流后是否仍保留必要的错误证据。
在示例根目录检查 Go 日志是否逐行可解析、核心字段是否齐全:
tail -n 100 go-service/logs/app.log | \
jq -e '.timestamp and .level and .service and .category and .traceId'再在测试环境停掉 Go 服务,发起同样的 Java 请求,检查 Java 是否记录真实的下游调用失败。恢复 Go 后复查,确认不会把失败日志误计为成功。
日志级别按事件含义使用。生产环境可以限制 DEBUG,但不必把所有信息都抬成 INFO;密码、令牌、Cookie 和敏感业务数据应在写入前处理,避免采集后再补救。