以下是一个完整的 Java 代码实现示例,使用 OpenTelemetry SDK 将日志数据转换为 Span 属性,并导出到 Elastic Observability (APM)。
1. 添加 Maven 依赖
确保 pom.xml
包含 OpenTelemetry 和 Elastic APM 导出器依赖:
<dependencies>
<!-- OpenTelemetry SDK -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.34.0</version>
</dependency>
<!-- OpenTelemetry Exporter for Elastic APM -->
<dependency>
<groupId>co.elastic.apm</groupId>
<artifactId>apm-opentelemetry-sdk-extensions</artifactId>
<version>1.45.0</version>
</dependency>
<!-- OpenTelemetry OTLP Exporter (可选) -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>1.34.0</version>
</dependency>
</dependencies>
2. Java 代码实现
(1) 初始化 OpenTelemetry Tracer
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
public class ElasticApmExample {
// 初始化 OpenTelemetry
private static final OpenTelemetry openTelemetry = initOpenTelemetry();
private static final Tracer tracer = openTelemetry.getTracer("bank-system");
// 配置 OpenTelemetry SDK
private static OpenTelemetry initOpenTelemetry() {
// 1. 定义资源属性(服务名称、环境等)
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(
ResourceAttributes.SERVICE_NAME, "personal-withdrawal-service",
ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "production"
)));
// 2. 配置 Elastic APM 导出器(或使用 OTLP 导出器)
OtlpGrpcSpanExporter exporter = OtlpGrpcSpanExporter.builder()
.setEndpoint("http://localhost:8200") // Elastic APM Server 地址
.build();
// 3. 创建 TracerProvider
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(exporter).build())
.setResource(resource)
.build();
// 4. 构建 OpenTelemetry 实例
return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.build();
}
}
(2) 解析日志并创建 Span
import java.time.Instant;
import java.time.format.DateTimeFormatter;
public class LogToSpanConverter {
public static void main(String[] args) {
// 模拟日志数据(实际可从文件或消息队列读取)
String logEntry = "Lable:START|TimeStamp:20250710092656478|TransID:20250710092656478_ceipt|TransCode:ceipt";
String mainTransLog = "Lable:END|TimeStamp:20250710092657910|TransID:20250710092417601_20205|TransCode:20205|StartTime:20250710092417601|EndTime:20250710092657910|ResCode:000000|ResDes:成功|UseTime:160309ms|TransName:个人定期一本通支取|Date:20250710|FaRen:101|JiGou:321027026|Teller:321027026210|BusiNum:3210270262100500034|FormId:STD16770";
// 1. 创建主 Span(个人定期一本通支取)
Span mainSpan = tracer.spanBuilder("personal_withdrawal")
.setStartTimestamp(parseTimestamp("20250710092417601")) // 从日志中提取开始时间
.setAttribute("transaction.id", "20250710092417601_20205")
.setAttribute("transaction.code", "20205")
.setAttribute("transaction.name", "个人定期一本通支取")
.setAttribute("business.date", "20250710")
.setAttribute("business.branch", "321027026")
.setAttribute("business.teller", "321027026210")
.setAttribute("business.customer", "101")
.setAttribute("business.serial", "3210270262100500034")
.setAttribute("form.id", "STD16770")
.startSpan();
try (Scope scope = mainSpan.makeCurrent()) {
// 2. 模拟子事务(如凭证处理)
Span subSpan = tracer.spanBuilder("sub_transaction")
.setStartTimestamp(parseTimestamp("2025-07-10 09:25:49.579")) // SUBED StartTime
.setAttribute("transaction.code", "21000000000019")
.setAttribute("duration_ms", 23)
.setAttribute("status.message", "业务脚本处理成功")
.startSpan();
try (Scope subScope = subSpan.makeCurrent()) {
// 模拟子事务逻辑
Thread.sleep(10);
} finally {
subSpan.end(parseTimestamp("2025-07-10 09:25:49.602")); // SUBED EndTime
}
// 3. 主事务结束
mainSpan.end(parseTimestamp("20250710092657910")); // END TimeStamp
}
// 关闭 TracerProvider(实际生产环境应由生命周期管理)
System.out.println("Spans exported to Elastic APM!");
}
// 辅助方法:解析时间戳(支持多种格式)
private static long parseTimestamp(String timestamp) {
try {
if (timestamp.contains("-")) { // ISO8601 格式
return Instant.from(DateTimeFormatter.ISO_DATE_TIME.parse(timestamp)).toEpochMilli();
} else { // 原始长整型时间戳(如 20250710092656478)
return Instant.parse(
timestamp.substring(0, 4) + "-" + timestamp.substring(4, 6) + "-" + timestamp.substring(6, 8) + "T" +
timestamp.substring(8, 10) + ":" + timestamp.substring(10, 12) + ":" + timestamp.substring(12, 14) + "." + timestamp.substring(14) + "Z"
).toEpochMilli();
}
} catch (Exception e) {
return System.currentTimeMillis(); // 失败时回退到当前时间
}
}
}
3. 关键点说明
-
Span 生命周期:
- 主 Span (
personal_withdrawal
) 包含子 Span (sub_transaction
)。 - 通过
try-with-resources
确保 Span 正确结束。
- 主 Span (
-
时间戳处理:
- 支持解析日志中的
StartTime
/EndTime
或原始长整型时间戳(如20250710092656478
)。
- 支持解析日志中的
-
属性映射:
- 日志中的字段(如
TransCode
、ResDes
)映射到 Span 的attributes
。
- 日志中的字段(如
-
导出到 Elastic APM:
- 通过
OtlpGrpcSpanExporter
将数据发送到 Elastic APM Server(默认端口8200
)。
- 通过
4. 运行与验证
-
启动 Elastic APM Server:
docker run -d --name elastic-apm -p 8200:8200 docker.elastic.co/apm/apm-server:8.12.0
-
运行 Java 程序:
mvn compile exec:java -Dexec.mainClass="com.example.LogToSpanConverter"
-
在 Kibana 中查看:
- 访问
http://localhost:5601/app/apm/services
,选择personal-withdrawal-service
。 - 查看 Trace 详情,确认主子事务关联和属性正确。
- 访问
5. 高级优化
- 动态日志解析:使用正则表达式或日志解析库(如 Logstash)提取字段。
- 批量处理:通过
BatchSpanProcessor
提高性能。 - 错误处理:捕获
ResCode != 000000
并标记 Span 为错误:if (!"000000".equals(resCode)) { span.setStatus(StatusCode.ERROR, resDes); }
通过以上代码,您的日志数据将转换为结构化的 Trace 数据,并在 Elastic Observability 中实现可视化分析。
上面并没有对日志进行解析,以下是一个完整的 Java 日志解析处理代码示例,将原始日志(如 Lable:START|TimeStamp:20250710092656478|TransID:123|...
)解析为结构化数据,并转换为 OpenTelemetry Span 属性,最终导出到 Elastic APM。
1. 日志解析工具类
功能
- 解析
key=value
或key:value
格式的日志。 - 支持嵌套字段(如
SUBED:StartTime=...
)。 - 处理时间戳格式转换。
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LogParser {
// 正则表达式匹配 key:value 或 key=value
private static final Pattern LOG_PATTERN = Pattern.compile("([\\w]+)[:=]([^|]+)");
/**
* 解析单行日志为 Map
* @param logLine 日志行(如 "Lable:START|TimeStamp:20250710092656478|...")
* @return 键值对 Map
*/
public static Map<String, String> parseLog(String logLine) {
Map<String, String> logData = new HashMap<>();
Matcher matcher = LOG_PATTERN.matcher(logLine);
while (matcher.find()) {
String key = matcher.group(1).trim();
String value = matcher.group(2).trim();
logData.put(key, value);
}
return logData;
}
/**
* 解析嵌套日志字段(如 "SUBED:StartTime=2025-07-10 09:25:49.579")
* @param logData 已解析的日志 Map
* @param prefix 嵌套字段前缀(如 "SUBED")
* @return 嵌套字段的 Map
*/
public static Map<String, String> parseNestedFields(Map<String, String> logData, String prefix) {
Map<String, String> nestedFields = new HashMap<>();
for (String key : logData.keySet()) {
if (key.startsWith(prefix + ":")) {
String nestedKey = key.substring(prefix.length() + 1); // 去掉 "prefix:"
nestedFields.put(nestedKey, logData.get(key));
}
}
return nestedFields;
}
}
2. 完整日志处理流程
功能
- 解析日志行。
- 提取主事务和子事务信息。
- 创建 OpenTelemetry Span 并关联属性。
- 导出到 Elastic APM。
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
import java.time.Instant;
import java.util.Map;
public class LogProcessingPipeline {
private static final OpenTelemetry openTelemetry = initOpenTelemetry();
private static final Tracer tracer = openTelemetry.getTracer("bank-system");
// 初始化 OpenTelemetry
private static OpenTelemetry initOpenTelemetry() {
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(
ResourceAttributes.SERVICE_NAME, "bank-transaction-service",
ResourceAttributes.DEPLOYMENT_ENVIRONMENT, "production"
)));
OtlpGrpcSpanExporter exporter = OtlpGrpcSpanExporter.builder()
.setEndpoint("http://localhost:8200") // Elastic APM Server
.build();
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(exporter).build())
.setResource(resource)
.build();
return OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.build();
}
/**
* 处理日志行并生成 Span
* @param logLine 日志行
*/
public static void processLog(String logLine) {
// 1. 解析日志
Map<String, String> logData = LogParser.parseLog(logLine);
String label = logData.get("Lable"); // START/END/SUBED
if ("START".equals(label)) {
// 2. 创建主 Span(从 START 日志提取信息)
String transName = logData.get("TransName");
String transId = logData.get("TransID");
long startTime = parseTimestamp(logData.get("TimeStamp"));
Span mainSpan = tracer.spanBuilder(transName != null ? transName : "unknown_transaction")
.setStartTimestamp(startTime)
.setAttribute("transaction.id", transId)
.setAttribute("transaction.code", logData.get("TransCode"))
.setAttribute("business.date", logData.get("Date"))
.setAttribute("business.branch", logData.get("JiGou"))
.setAttribute("business.teller", logData.get("Teller"))
.setAttribute("business.customer", logData.get("FaRen"))
.setAttribute("form.id", logData.get("FormId"))
.startSpan();
// 3. 模拟处理子事务(如 SUBED 日志)
try (Scope scope = mainSpan.makeCurrent()) {
if (logData.containsKey("SUBED:StartTime")) {
processSubTransaction(logData);
}
// 4. 等待 END 日志(实际场景可能需异步处理)
// 这里简化处理,直接结束 Span
mainSpan.end();
}
}
}
/**
* 处理子事务(如 SUBED 日志)
*/
private static void processSubTransaction(Map<String, String> logData) {
Map<String, String> subFields = LogParser.parseNestedFields(logData, "SUBED");
long startTime = parseTimestamp(subFields.get("StartTime"));
long endTime = parseTimestamp(subFields.get("EndTime"));
Span subSpan = tracer.spanBuilder("sub_transaction")
.setStartTimestamp(startTime)
.setAttribute("transaction.code", subFields.get("TransCode"))
.setAttribute("duration_ms", endTime - startTime)
.setAttribute("status.message", subFields.get("ResDes"))
.startSpan();
try (Scope scope = subSpan.makeCurrent()) {
// 模拟子事务逻辑
} finally {
subSpan.end(endTime);
}
}
/**
* 解析时间戳(支持多种格式)
*/
private static long parseTimestamp(String timestamp) {
try {
if (timestamp == null) {
return System.currentTimeMillis();
}
if (timestamp.contains("-")) { // ISO8601 格式
return Instant.parse(timestamp).toEpochMilli();
} else { // 原始长整型时间戳(如 20250710092656478)
return Instant.parse(
timestamp.substring(0, 4) + "-" + timestamp.substring(4, 6) + "-" + timestamp.substring(6, 8) + "T" +
timestamp.substring(8, 10) + ":" + timestamp.substring(10, 12) + ":" + timestamp.substring(12, 14) + "." + timestamp.substring(14) + "Z"
).toEpochMilli();
}
} catch (Exception e) {
return System.currentTimeMillis();
}
}
public static void main(String[] args) {
// 示例日志
String startLog = "Lable:START|TimeStamp:20250710092656478|TransID:20250710092656478_ceipt|TransCode:ceipt|TransName:个人定期一本通支取|Date:20250710|JiGou:321027026|Teller:321027026210|FaRen:101|FormId:STD16770";
String subLog = "Lable:SUBED|SUBED:StartTime=2025-07-10 09:25:49.579|SUBED:EndTime=2025-07-10 09:25:49.602|SUBED:TransCode=21000000000019|SUBED:ResDes=业务脚本处理成功";
String endLog = "Lable:END|TimeStamp:20250710092657910|TransID:20250710092417601_20205|TransCode:20205|ResCode:000000|ResDes:成功|UseTime:160309ms";
// 处理日志
processLog(startLog);
processLog(subLog); // 实际场景可能需关联到主 Span
processLog(endLog);
System.out.println("Logs processed and spans exported to Elastic APM!");
}
}
3. 关键点说明
-
日志解析:
- 使用正则表达式提取
key:value
或key=value
格式的字段。 - 支持嵌套字段(如
SUBED:StartTime
)。
- 使用正则表达式提取
-
时间戳处理:
- 支持
20250710092656478
(长整型)和2025-07-10 09:25:49.579
(ISO8601)格式。
- 支持
-
Span 生成:
- 主 Span 从
START
日志创建,子 Span 从SUBED
日志创建。 - 属性映射(如
TransCode
→transaction.code
)。
- 主 Span 从
-
导出到 Elastic APM:
- 通过
OtlpGrpcSpanExporter
发送数据到 Elastic APM Server(默认端口8200
)。
- 通过
4. 运行与验证
-
启动 Elastic APM Server:
docker run -d --name elastic-apm -p 8200:8200 docker.elastic.co/apm/apm-server:8.12.0
-
运行 Java 程序:
mvn compile exec:java -Dexec.mainClass="com.example.LogProcessingPipeline"
-
在 Kibana 中查看:
- 访问
http://localhost:5601/app/apm/services
,选择bank-transaction-service
。 - 检查 Trace 详情,确认主子事务关联和属性正确。
- 访问
5. 高级优化
- 批量处理日志:使用
BufferedReader
读取日志文件。 - 异步处理:结合
CompletableFuture
或 Kafka 消费者。 - 错误处理:捕获
ResCode != 000000
并标记 Span 为错误:if (!"000000".equals(logData.get("ResCode"))) { span.setStatus(StatusCode.ERROR, logData.get("ResDes")); }
通过此代码,您可以高效解析日志并转换为 OpenTelemetry Span,实现分布式追踪和性能分析。