OpenTelemetry学习笔记(七):使用 OpenTelemetry SDK 将日志数据转换为 Span 属性,并导出到 Elastic Observability (APM)

以下是一个完整的 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. 关键点说明

  1. Span 生命周期

    • 主 Span (personal_withdrawal) 包含子 Span (sub_transaction)。
    • 通过 try-with-resources 确保 Span 正确结束。
  2. 时间戳处理

    • 支持解析日志中的 StartTime/EndTime 或原始长整型时间戳(如 20250710092656478)。
  3. 属性映射

    • 日志中的字段(如 TransCodeResDes)映射到 Span 的 attributes
  4. 导出到 Elastic APM

    • 通过 OtlpGrpcSpanExporter 将数据发送到 Elastic APM Server(默认端口 8200)。

4. 运行与验证

  1. 启动 Elastic APM Server

    docker run -d --name elastic-apm -p 8200:8200 docker.elastic.co/apm/apm-server:8.12.0
    
  2. 运行 Java 程序

    mvn compile exec:java -Dexec.mainClass="com.example.LogToSpanConverter"
    
  3. 在 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=valuekey: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. 完整日志处理流程

功能

  1. 解析日志行。
  2. 提取主事务和子事务信息。
  3. 创建 OpenTelemetry Span 并关联属性。
  4. 导出到 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. 关键点说明

  1. 日志解析

    • 使用正则表达式提取 key:valuekey=value 格式的字段。
    • 支持嵌套字段(如 SUBED:StartTime)。
  2. 时间戳处理

    • 支持 20250710092656478(长整型)和 2025-07-10 09:25:49.579(ISO8601)格式。
  3. Span 生成

    • 主 Span 从 START 日志创建,子 Span 从 SUBED 日志创建。
    • 属性映射(如 TransCodetransaction.code)。
  4. 导出到 Elastic APM

    • 通过 OtlpGrpcSpanExporter 发送数据到 Elastic APM Server(默认端口 8200)。

4. 运行与验证

  1. 启动 Elastic APM Server

    docker run -d --name elastic-apm -p 8200:8200 docker.elastic.co/apm/apm-server:8.12.0
    
  2. 运行 Java 程序

    mvn compile exec:java -Dexec.mainClass="com.example.LogProcessingPipeline"
    
  3. 在 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,实现分布式追踪和性能分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飞翔的佩奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值