David Hope

使用 OpenTelemetry 手动检测 Java 应用程序

OpenTelemetry 为云原生软件提供了一个可观测性框架,使我们能够无缝地跟踪、监视和调试应用程序。在本文中,我们将探讨如何使用 OpenTelemetry 手动检测 Java 应用程序。

阅读时长 17 分钟
Manual instrumentation of Java applications with OpenTelemetry

在快速发展的软件开发领域,尤其是在云原生领域,DevOps 和 SRE 团队正日益成为应用程序稳定性和增长的重要合作伙伴。

DevOps 工程师不断优化软件交付,而 SRE 团队则充当应用程序可靠性、可扩展性和顶级性能的管理者。挑战是什么?这些团队需要一个尖端的可观测性解决方案,该解决方案应包含全栈洞察力,使他们能够在潜在中断升级为运营挑战之前快速管理、监视和纠正它们。

我们现代分布式软件生态系统中的可观测性不仅仅是简单的监视,它还需要无限的数据收集、精确的处理以及将这些数据关联成可操作的见解。然而,实现这种整体视图的道路上充满了障碍:从处理版本不兼容到与受限的专有代码作斗争。

以下是采用 OpenTelemetry (OTel) 的好处:

  • 使用 OTel 摆脱供应商限制,使您摆脱供应商锁定并确保一流的可观测性。
  • 了解统一的日志、指标和跟踪如何和谐地结合在一起,从而提供完整的系统视图。
  • 通过更丰富和增强的检测来改进您的应用程序监督。
  • 利用向后兼容性的优势来保护您之前的检测投资。
  • 通过简单的学习曲线开始 OpenTelemetry 之旅,简化入职和可扩展性。
  • 依靠经过验证的、面向未来的标准来增强您对每一项投资的信心。

在本博客中,我们将探讨如何使用 Docker 在 Java 应用程序中使用 手动检测,而无需重构应用程序代码的任何部分。我们将使用一个名为 Elastiflix 的应用程序。此方法比使用自动检测稍微复杂一些。

这样做的好处是不需要 otel-collector!此设置使您能够根据最适合您业务的时间线,缓慢且轻松地将应用程序迁移到带有 Elastic 的 OTel。

应用程序、先决条件和配置

我们在本博客中使用的应用程序名为 Elastiflix,是一个电影流式传输应用程序。它由几个以 .NET、NodeJS、Go 和 Python 编写的微服务组成。

在我们检测示例应用程序之前,我们需要先了解 Elastic 如何接收遥测数据。

Elastic Observability 的所有 APM 功能都可用于 OTel 数据。其中包括:

  • 服务地图
  • 服务详细信息(延迟、吞吐量、失败的事务)
  • 服务之间的依赖关系,分布式跟踪
  • 事务(跟踪)
  • 机器学习 (ML) 相关性
  • 日志关联

除了 Elastic 的 APM 和遥测数据的统一视图之外,您还可以使用 Elastic 强大的机器学习功能来减少分析和警报,从而帮助减少 MTTR。

先决条件

查看示例源代码

完整的源代码,包括本博客中使用的 Dockerfile,可以在 GitHub 上找到。该存储库还包含没有检测的同一应用程序。这使您可以比较每个文件并查看差异。

特别是,我们将处理以下文件

Elastiflix/java-favorite/src/main/java/com/movieapi/ApiServlet.java

以下步骤将向您展示如何检测此应用程序并在命令行或 Docker 中运行它。如果您对更完整的 OTel 示例感兴趣,请查看 此处的 docker-compose 文件,该文件将启动整个项目。

在我们开始之前,让我们先看一下未检测的代码。

分步指南

步骤 0:登录到您的 Elastic Cloud 帐户

本博客假设您拥有 Elastic Cloud 帐户 — 如果没有,请按照说明开始使用 Elastic Cloud

步骤 1:设置 OpenTelemetry

第一步是在您的 Java 应用程序中设置 OpenTelemetry SDK。您可以首先将 OpenTelemetry Java SDK 及其依赖项添加到项目的构建文件(例如 Maven 或 Gradle)中。在我们的示例应用程序中,我们使用 Maven。将以下依赖项添加到您的 pom.xml

<dependency>
      <groupId>io.opentelemetry.instrumentation</groupId>
      <artifactId>opentelemetry-logback-mdc-1.0</artifactId>
      <version>1.25.1-alpha</version>
    </dependency>

    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-api</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-sdk</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-semconv</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry</groupId>
      <artifactId>opentelemetry-exporter-otlp-logs</artifactId>
    </dependency>
    <dependency>
      <groupId>io.opentelemetry.instrumentation</groupId>
      <artifactId>opentelemetry-logback-appender-1.0</artifactId>
      <version>1.25.1-alpha</version>
    </dependency>

同时,从 OpenTelemetry 添加以下物料清单

<dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-bom</artifactId>
        <version>1.25.0</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-bom-alpha</artifactId>
        <version>1.25.0-alpha</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

步骤 2. 添加应用程序配置

我们建议您将以下配置添加到应用程序的 main 方法中,以便在任何应用程序代码启动之前启动。这样做可以为您提供更多的控制和灵活性,并确保 OpenTelemetry 在应用程序生命周期的任何阶段都可用。在示例中,我们将此代码放在 Spring Boot 应用程序启动之前。Elastic 支持通过 HTTP 和 GRPC 的 OTLP。在此示例中,我们使用的是 GRPC。

String SERVICE_NAME = System.getenv("OTEL_SERVICE_NAME");

// set service name on all OTel signals
Resource resource = Resource.getDefault().merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME,SERVICE_NAME,ResourceAttributes.SERVICE_VERSION,"1.0",ResourceAttributes.DEPLOYMENT_ENVIRONMENT,"production")));

// init OTel logger provider with export to OTLP
SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder().setResource(resource).addLogRecordProcessor(BatchLogRecordProcessor.builder(OtlpGrpcLogRecordExporter.builder().setEndpoint(System.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")).addHeader("Authorization", "Bearer " + System.getenv("ELASTIC_APM_SECRET_TOKEN")).build()).build()).build();

// init OTel trace provider with export to OTLP
SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder().setResource(resource).setSampler(Sampler.alwaysOn()).addSpanProcessor(BatchSpanProcessor.builder(OtlpGrpcSpanExporter.builder().setEndpoint(System.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")).addHeader("Authorization", "Bearer " + System.getenv("ELASTIC_APM_SECRET_TOKEN")).build()).build()).build();

// init OTel meter provider with export to OTLP
SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder().setResource(resource).registerMetricReader(PeriodicMetricReader.builder(OtlpGrpcMetricExporter.builder().setEndpoint(System.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")).addHeader("Authorization", "Bearer " + System.getenv("ELASTIC_APM_SECRET_TOKEN")).build()).build()).build();

// create sdk object and set it as global
OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().setTracerProvider(sdkTracerProvider).setLoggerProvider(sdkLoggerProvider).setMeterProvider(sdkMeterProvider).setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())).build();

GlobalOpenTelemetry.set(sdk);
// connect logger
GlobalLoggerProvider.set(sdk.getSdkLoggerProvider());
// Add hook to close SDK, which flushes logs
Runtime.getRuntime().addShutdownHook(new Thread(sdk::close));

步骤 3. 创建 Tracer 并在 TracingFilter 内部启动 OpenTelemetry Span

在 Spring Boot 示例中,您会注意到我们有一个扩展了 OncePerRequestFilter 类的 TracingFilter 类。此 Filter 是一个组件,位于请求处理链的前端。它的主要作用是拦截传入的请求和传出的响应,执行诸如日志记录、身份验证、请求/响应实体的转换等任务。因此,我们在此处所做的是拦截进入 Favorite 服务的请求,以便我们可以提取可能包含来自上游系统的跟踪信息的标头。

我们首先使用 OpenTelemetry Tracer,它是 OpenTelemetry 的核心组件,允许您创建 Span、启动和停止它们,以及添加属性和事件。在您的 Java 代码中,导入必要的 OpenTelemetry 类,并在您的应用程序中创建 Tracer 的实例。

我们使用它来创建一个新的下游 Span,它将继续作为从上游系统中使用我们从上游请求中获得的信息创建的 Span 的子级。在我们的 Elastiflix 示例中,这将是 nodejs 应用程序。

@Override
protected void doFilterInternal(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, jakarta.servlet.FilterChain filterChain) throws jakarta.servlet.ServletException, IOException {
        Tracer tracer = GlobalOpenTelemetry.getTracer(SERVICE_NAME);

        Context extractedContext = GlobalOpenTelemetry.getPropagators()
                .getTextMapPropagator()
                .extract(Context.current(), request, getter);

        Span span = tracer.spanBuilder(request.getRequestURI())
                .setSpanKind(SpanKind.SERVER)
                .setParent(extractedContext)
                .startSpan();

        try (Scope scope = span.makeCurrent()) {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            span.setStatus(StatusCode.ERROR);
            throw e;
        } finally {
            span.end();
        }
    }

步骤 4. 使用 Span 检测其他感兴趣的代码

要使用 Span 进行检测并跟踪代码的特定区域,可以使用 Tracer 的 SpanBuilder 来创建 Span。为了准确衡量特定操作的持续时间,请确保在代码中的适当位置启动和停止 Span。使用 Tracer 提供的 startSpan 和 endSpan 方法来标记 Span 的开始和结束。例如,您可以在代码中的特定方法或操作周围创建一个 Span,如这里 handleCanary 方法中所示

private void handleCanary() throws Exception {
        Span span = GlobalOpenTelemetry.getTracer(SERVICE_NAME).spanBuilder("handleCanary").startSpan();
        Scope scope = span.makeCurrent();

///.....


 span.setStatus(StatusCode.OK);

        span.end();

        scope.close();
    }

步骤 5. 向 Span 添加属性和事件

您可以使用附加属性和事件来增强 Span,以提供有关所跟踪操作的更多上下文和详细信息。属性可以是描述 Span 的键值对,而事件可以用于标记 Span 生命周期中的重要点。这也在 handleCanary 方法中显示

private void handleCanary() throws Exception {

            Span.current().setAttribute("canary", "test-new-feature");
            Span.current().setAttribute("quiz_solution", "correlations");

            span.addEvent("a span event", Attributes
                    .of(AttributeKey.longKey("someKey"), Long.valueOf(93)));
    }

步骤 6. 检测后端

让我们考虑一个示例,其中我们正在检测 Redis 数据库调用。我们正在使用 Java OpenTelemetry SDK,我们的目标是创建一个跟踪,捕获每个“Post User Favorites”操作到数据库。

以下是执行操作并收集遥测数据的 Java 方法

public void postUserFavorites(String user_id, String movieID) {
  ...
}

让我们逐行浏览它

初始化 Span
我们方法的第一条重要行是我们初始化 Span 的地方。Span 表示跟踪中的单个操作,它可以是数据库调用、远程过程调用 (RPC) 或您想要衡量的任何代码段。

Span span = GlobalOpenTelemetry.getTracer(SERVICE_NAME).spanBuilder("Redis.Post").setSpanKind(SpanKind.CLIENT).startSpan();

设置 Span 属性
接下来,我们向我们的 Span 添加属性。属性是键值对,提供有关 Span 的其他信息。为了使后端调用正确显示在服务地图中,为后端调用类型正确设置属性至关重要。在此示例中,我们将 db.system 属性设置为 redis。

span.setAttribute("db.system", "redis");
span.setAttribute("db.connection_string", redisHost);
span.setAttribute(
  "db.statement",
  "POST user_id " + user_id + " AND movie_id " + movieID
);

这将确保对后端 redis 后端的调用如下所示被跟踪

捕获操作的结果
然后,我们在 try-catch 块中执行我们感兴趣的操作。如果在操作执行期间发生异常,我们会将其记录在 Span 中。

try (Scope scope = span.makeCurrent()) {
    ...
} catch (Exception e) {
    span.setStatus(StatusCode.ERROR, "Error while getting data from Redis");
    span.recordException(e);
}

关闭资源
最后,我们关闭 Redis 连接并结束 Span。

finally {
    jedis.close();
    span.end();
}

步骤 7. 配置日志记录

日志记录是应用程序监控和故障排除的重要组成部分。OpenTelemetry 允许您与现有的日志记录框架(如 Logback 或 Log4j)集成,以捕获日志以及遥测数据。配置您选择的日志记录框架,以捕获与检测的 Span 相关的日志。在我们的示例应用程序中,请查看 logback 配置,该配置显示如何将日志直接导出到 Elastic。

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">

    <appender name="otel-otlp"
        class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
        <captureExperimentalAttributes>false</captureExperimentalAttributes>
        <captureCodeAttributes>true</captureCodeAttributes>
        <captureKeyValuePairAttributes>true</captureKeyValuePairAttributes>
    </appender>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="DEBUG">
     <appender-ref ref="otel-otlp" />
        <appender-ref ref="STDOUT" />

    </root>
</configuration>

步骤 8. 使用环境变量运行 Docker 镜像

OTEL Java 文档 中指定,我们将使用环境变量并传入配置值,使其能够与 Elastic Observability 的 APM 服务器 连接。

由于 Elastic 本机接受 OTLP,我们只需要提供 OTEL Exporter 需要发送数据的端点和身份验证,以及一些其他环境变量。

获取 Elastic Cloud 变量
您可以从 Kibana 中的以下路径复制端点和令牌

/app/home#/tutorial/apm
.

您将需要复制以下环境变量

OTEL_EXPORTER_OTLP_ENDPOINT

以及来自

OTEL_EXPORTER_OTLP_HEADERS

构建 Docker 镜像

docker build -t java-otel-manual-image .

运行 Docker 镜像

docker run \
       -e OTEL_EXPORTER_OTLP_ENDPOINT="REPLACE WITH OTEL_EXPORTER_OTLP_ENDPOINT" \
       -e ELASTIC_APM_SECRET_TOKEN="REPLACE WITH TOKEN" \
       -e OTEL_RESOURCE_ATTRIBUTES="service.version=1.0,deployment.environment=production" \
       -e OTEL_SERVICE_NAME="java-favorite-otel-manual" \
       -p 5000:5000 \
       java-otel-manual-image

您现在可以发出一些请求以生成跟踪数据。请注意,这些请求预计会返回错误,因为此服务依赖于与您当前未运行的 Redis 的连接。如前所述,您可以在 此处 找到使用 docker-compose 的更完整的示例。

curl localhost:5000/favorites

# or alternatively issue a request every second

while true; do curl "localhost:5000/favorites"; sleep 1; done;

步骤 9. 在 Elastic APM 中探索跟踪和日志

一旦您启动并运行此操作,您就可以 ping 检测服务的端点(在我们的例子中,这是 /favorites),您应该会在 Elastic APM 中看到该应用程序,如下所示

它将首先跟踪 SRE 需要关注的吞吐量和延迟关键指标。

深入研究,我们可以看到所有事务的概述。

并查看特定事务

单击日志,我们会看到日志也被带过来。OTel Agent 将自动引入日志并将它们与跟踪相关联

这使您可以完全了解日志、指标和跟踪!

总结

使用 OpenTelemetry 手动检测 Java 应用程序可以更好地控制要跟踪和监控的内容。通过遵循本博文中概述的步骤,您可以有效地监控 Java 应用程序的性能、识别问题,并深入了解应用程序的整体运行状况。

请记住,OpenTelemetry 是一个强大的工具,适当的检测需要仔细考虑对于您的特定用例来说哪些指标、跟踪和日志是必不可少的。尝试不同的配置,利用 Java 文档的 OpenTelemetry SDK,并不断迭代以实现应用程序的可观测性目标。

在本博客中,我们讨论了以下内容

  • 如何使用 OpenTelemetry 手动检测 Java
  • 如何正确初始化和检测 Span
  • 如何轻松地从 Elastic 设置 OTLP ENDPOINT 和 OTLP HEADERS,而无需收集器

希望这提供了一个易于理解的使用 OpenTelemetry 检测 Java 的演练,以及将跟踪发送到 Elastic 的简易程度。

开发者资源

常规配置和用例资源

还没有 Elastic Cloud 帐户?注册 Elastic Cloud 并试用我上面讨论的自动检测功能。我很想收到您关于使用 Elastic 了解您的应用程序堆栈的体验的反馈。

本文中描述的任何特性或功能的发布和时间安排均由 Elastic 自行决定。任何当前不可用的特性或功能可能不会按时交付或根本不会交付。

分享这篇文章