统一 Java 与 Go 日志,再接入 SLS

Java、Go 的 JSON 日志格式、HTTP/gRPC 请求标识与 SLS 采集配置。

可观测

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。

Shell
git clone https://github.com/Roliyal/CROlordCodelibrary.git
cd CROlordCodelibrary/Chapter6Custom/ARMS-Log-collect

在两个终端分别启动服务:

Shell
# 终端一:在示例根目录执行
cd go-service
go mod tidy
go run ./...
Shell
# 终端二:在示例根目录执行
cd java-service
mvn -q -DskipTests package
mvn -q spring-boot:run

Java 日志写标准输出;Go 写 go-service/logs/app.log。也可以在示例根目录用已有的 docker-compose.yml 启动,注意容器内调用使用服务名。

统一日志字段

每条日志输出为一行 JSON,字段名与类型跨语言一致。下面是一个格式示例,链路标识为示意值:

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。它首先解决日志关联,不等于已经接入标准分布式追踪。可以这样验证:

Shell
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.log

Java 控制台与 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 模板布局,模板文件明确各字段映射:

XML
<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:

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 配置采集、解析和索引

  1. 创建或选定 Project、Logstore,设置保留时间。
  2. 为目标主机或容器配置 LoongCollector(Logtail)采集范围。
  3. 选择 JSON 解析,预览字段,确认采集器有读取与发送权限。
  4. 配置时间提取,核对日志事件时间与查询时区。
  5. 给常用字段建立索引,costMs 等分析字段使用数值类型。
  6. 新写入一条带唯一测试标识的日志,在目标 Logstore 查询。

采集器默认的增量读取行为需要留意。配置后没有读入旧文件,不应马上判定采集失败;先新增一条日志做验证。

预览能看到日志、查询却没有结果时,检查索引和时间范围;字段仍是一整段字符串时,检查 JSON 解析配置。文件已经轮转,还要确认路径规则能够覆盖新文件。

跨服务采集验证

先用 logging-check-001 发起跨服务调用,再到 SLS 查询相同请求标识。标准 Trace ID 的查询格式如下:

Text
traceId: 4bf92f3577b34da6a3ce929d0e0e4736

按服务统计错误数量的查询示例:

SQL
level: ERROR |
SELECT service, count(*) AS errors
GROUP BY service
ORDER BY errors DESC

查询依赖对应字段已建立索引。验收还应检查同一事件有没有采集两次、异常日志能否关联原请求,以及采样或限流后是否仍保留必要的错误证据。

在示例根目录检查 Go 日志是否逐行可解析、核心字段是否齐全:

Shell
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 和敏感业务数据应在写入前处理,避免采集后再补救。

参考

一丝痕迹

很高兴,在这里遇见你。

头像,随邮箱而来。

填写邮箱后,会自动匹配 Gravatar。