手动仪表化

OpenTelemetry Java 的手动仪表化

Manual instrumentation is the act of adding observability code to an app yourself.

If you’re instrumenting an app, you need to use the OpenTelemetry SDK for your language. You’ll then use the SDK to initialize OpenTelemetry and the API to instrument your code. This will emit telemetry from your app, and any library you installed that also comes with instrumentation.

If you’re instrumenting a library, only install the OpenTelemetry API package for your language. Your library will not emit telemetry on its own. It will only emit telemetry when it is part of an app that uses the OpenTelemetry SDK. For more on instrumenting libraries, see Libraries.

For more information about the OpenTelemetry API and SDK, see the specification.

示例应用程序准备

本页使用从开始使用修改的示例应用程序版本,以帮助您了解手动仪表化。

您不必使用示例应用程序:如果您想为自己的应用程序或库添加仪表化,请按照此处的说明来调整流程以适应您自己的代码。

依赖关系

首先,在名为 java-simple 的新目录中设置一个环境。在该目录中,创建一个名为 build.gradle.kts 的文件,并添加以下内容:

plugins {
  id("java")
  id("org.springframework.boot") version "3.0.6"
  id("io.spring.dependency-management") version "1.1.0"
}

sourceSets {
  main {
    java.setSrcDirs(setOf("."))
  }
}

repositories {
  mavenCentral()
}

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web")
}

创建和启动 HTTP 服务器

为了突出仪表化 和独立 应用程序 之间的区别,将掷骰子拆分为一个 类,然后将其作为一个依赖项导入应用程序。

创建名为 Dice.java库文件,并添加以下代码:

package otel;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class Dice {

  private int min;
  private int max;

  public Dice(int min, int max) {
    this.min = min;
    this.max = max;
  }

  public List<Integer> rollTheDice(int rolls) {
    List<Integer> results = new ArrayList<Integer>();
    for (int i = 0; i < rolls; i++) {
      results.add(this.rollOnce());
    }
    return results;
  }

  private int rollOnce() {
    return ThreadLocalRandom.current().nextInt(this.min, this.max + 1);
  }
}

创建应用程序文件 DiceApplication.javaRollController.java,并将以下代码添加到其中:

// DiceApplication.java
package otel;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.Banner;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DiceApplication {
  public static void main(String[] args) {

    SpringApplication app = new SpringApplication(DiceApplication.class);
    app.setBannerMode(Banner.Mode.OFF);
    app.run(args);
  }
}
// RollController.java
package otel;

import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

import otel.Dice;

@RestController
public class RollController {
  private static final Logger logger = LoggerFactory.getLogger(RollController.class);

  @GetMapping("/rolldice")
  public List<Integer> index(@RequestParam("player") Optional<String> player,
      @RequestParam("rolls") Optional<Integer> rolls) {

    if (!rolls.isPresent()) {
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing rolls parameter", null);
    }

    List<Integer> result = new Dice(1, 6).rollTheDice(rolls.get());

    if (player.isPresent()) {
      logger.info("{} is rolling the dice: {}", player.get(), result);
    } else {
      logger.info("Anonymous player is rolling the dice: {}", result);
    }
    return result;
  }
}

为了确保它工作正常,请使用以下命令运行应用程序并在 Web 浏览器中打开 http://localhost:8080/rolldice?rolls=12

gradle assemble
java -jar ./build/libs/java-simple.jar

您在浏览器窗口中应该得到一个由12个数字组成的列表,例如:

[5,6,5,3,6,1,2,5,4,4,2,4]

手动仪表化设置

无论是库还是应用程序仪表化,第一步是安装 OpenTelemetry API 的依赖关系。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web");
    implementation("io.opentelemetry:opentelemetry-api:1.31.0");
}

在本文档中,您将添加附加依赖关系。有关完整的构件坐标列表,请参见[发布][releases]和语义约定发布(semantic-conventions-java)。

<project>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.opentelemetry</groupId>
                <artifactId>opentelemetry-bom</artifactId>
                <version>1.31.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-api</artifactId>
        </dependency>
    </dependencies>
</project>

初始化 SDK

如果您为 Java 应用程序进行仪表化,则安装 OpenTelemetry SDK 的依赖关系。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web");
    implementation("io.opentelemetry:opentelemetry-api:1.31.0");
    implementation("io.opentelemetry:opentelemetry-sdk:1.31.0");
    implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.31.0");
    implementation("io.opentelemetry:opentelemetry-exporter-logging:1.31.0");
    implementation("io.opentelemetry:opentelemetry-semconv:1.31.0-alpha");
}
<project>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.opentelemetry</groupId>
                <artifactId>opentelemetry-bom</artifactId>
                <version>1.31.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <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-sdk-metrics</artifactId>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-logging</artifactId>
        </dependency>
        <dependency>
            <!-- Not managed by opentelemetry-bom -->
            <groupId>io.opentelemetry.semconv</groupId>
            <artifactId>opentelemetry-semconv</artifactId>
            <version>1.22.0-alpha</version>
        </dependency>
    </dependencies>
</project>

如果您是应用程序开发人员,您需要尽早在应用程序中配置 OpenTelemetrySdk 实例。这可以通过使用 OpenTelemetrySdk.builder() 或通过使用 SDK 自动配置扩展来手动完成,后者更容易使用并且具有各种附加功能。

自动配置

要使用自动配置,请将以下依赖项添加到应用程序:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web");
    implementation("io.opentelemetry:opentelemetry-api:1.31.0");
    implementation("io.opentelemetry:opentelemetry-sdk:1.31.0");
    implementation("io.opentelemetry:opentelemetry-sdk-metrics:1.31.0");
    implementation("io.opentelemetry:opentelemetry-exporter-logging:1.31.0");
    implementation("io.opentelemetry.semconv:opentelemetry-semconv:1.22.0-alpha")
    implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:1.31.0");
    implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.31.0");
}
<project>
    <dependencies>
        <dependency>
          <groupId>io.opentelemetry</groupId>
          <artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
        </dependency>
        <dependency>
          <groupId>io.opentelemetry</groupId>
          <artifactId>opentelemetry-sdk-extension-autoconfigure-spi</artifactId>
        </dependency>
    </dependencies>
</project>

它允许您根据一组支持的标准环境变量和系统属性自动配置 OpenTelemetry SDK。每个环境变量都有一个对应的系统属性,其名称相同,但是使用 . (句点) 字符而不是 _ (下划线) 作为分隔符。

可以通过 OTEL_SERVICE_NAME 环境变量(或 otel.service.name 系统属性)指定逻辑服务名。

跟踪、指标或日志导出器可以通过 OTEL_TRACES_EXPORTEROTEL_METRICS_EXPORTEROTEL_LOGS_EXPORTER 环境变量进行设置。例如,OTEL_TRACES_EXPORTER=logging 将配置应用程序使用将所有跟踪写入控制台的导出器。相应的导出器库也必须提供在应用程序的类路径中。

对于调试和本地开发目的,可以使用 logging 导出器。在完成设置手动仪表化后,需要为应用程序的类路径提供一个适当的导出器库,以将应用程序的遥测数据导出到一个或多个遥测后端。

为了允许提供商在构建者内部进行处理,自动配置在应用程序生命周期的尽早阶段初始化。它遍历所提供的环境变量(或系统属性)并使用构建器设置 OpenTelemetry 实例。

import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;

OpenTelemetrySdk sdk = AutoConfiguredOpenTelemetrySdk.initialize()
    .getOpenTelemetrySdk();

示例应用程序 的情况下,DiceApplication 类的更新如下:

package otel;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.Banner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;

@SpringBootApplication
public class DiceApplication {
  public static void main(String[] args) {
    SpringApplication app = new SpringApplication(DiceApplication.class);
    app.setBannerMode(Banner.Mode.OFF);
    app.run(args);
  }

  @Bean
  public OpenTelemetry openTelemetry() {
    return AutoConfiguredOpenTelemetrySdk.initialize().getOpenTelemetrySdk();
  }
}

验证代码,构建并运行应用程序:

gradle assemble
env \
OTEL_SERVICE_NAME=dice-server \
OTEL_TRACES_EXPORTER=logging \
OTEL_METRICS_EXPORTER=logging \
OTEL_LOGS_EXPORTER=logging \
java -jar ./build/libs/java-simple.jar

此基本设置对您的应用程序没有任何影响。您需要为跟踪指标和/或日志中添加代码。

手动配置

OpenTelemetrySdk.builder() 返回 OpenTelemetrySdkBuilder 的实例,该实例获取与信号、跟踪和指标相关的提供者,以构建 OpenTelemetry 实例。

您可以使用 SdkTracerProvider.builder()SdkMeterProvider.builder() 方法构建提供者。

示例应用程序 的情况下,DiceApplication 类的更新如下:

package otel;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.Banner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.exporter.logging.LoggingMetricExporter;
import io.opentelemetry.exporter.logging.LoggingSpanExporter;
import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import io.opentelemetry.semconv.ResourceAttributes;

@SpringBootApplication
public class DiceApplication {
  public static void main(String[] args) {
    SpringApplication app = new SpringApplication(DiceApplication.class);
    app.setBannerMode(Banner.Mode.OFF);
    app.run(args);
  }

  @Bean
  public OpenTelemetry openTelemetry() {
    Resource resource = Resource.getDefault().toBuilder().put(ResourceAttributes.SERVICE_NAME, "dice-server").put(ResourceAttributes.SERVICE_VERSION, "0.1.0").build();

    SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder()
        .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create()))
        .setResource(resource)
        .build();

    SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder()
        .registerMetricReader(PeriodicMetricReader.builder(LoggingMetricExporter.create()).build())
        .setResource(resource)
        .build();

    SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder()
        .addLogRecordProcessor(BatchLogRecordProcessor.builder(SystemOutLogRecordExporter.create()).build())
        .setResource(resource)
        .build();

    OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
        .setTracerProvider(sdkTracerProvider)
        .setMeterProvider(sdkMeterProvider)
        .setLoggerProvider(sdkLoggerProvider)
        .setPropagators(ContextPropagators.create(TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance())))
        .buildAndRegisterGlobal();

    return openTelemetry;
  }
}

为调试和本地开发目的,示例将遥测导出到控制台。在设置手动仪表化后,需要配置适当的导出器以将应用程序的遥测数据导出到一个或多个遥测后端。

示例还设置了必需的 SDK 默认属性 service.name,其中包含服务的逻辑名称,以及可选(但强烈鼓励!)属性 service.version,其包含服务 API 或实施的版本。

还存在其他方法来设置资源属性。有关更多信息,请参见资源

验证代码,构建并运行应用程序:

gradle assemble
java -jar ./build/libs/java-simple.jar

此基本设置对您的应用程序没有任何影响。您需要为跟踪指标和/或日志中添加代码。

追踪

初始化追踪

要在您的应用中启用追踪,您需要初始化 TracerProvider。这将允许您创建一个Tracer

import io.opentelemetry.sdk.trace.SdkTracerProvider;

SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder()
  .addSpanProcessor(spanProcessor)
  .setResource(resource)
  .build();

如果没有创建TracerProvider,OpenTelemetry追踪API将使用一个无操作实现并无法生成数据。

如果您按照上述的初始化SDK的指示操作,则已经为您设置了TracerProvider。您可以继续进行获取跟踪器的操作。

获取跟踪器

要进行追踪,您需要获取一个Tracer

**注意:**不应调用OpenTelemetry SDK的方法。

首先,必须获取一个负责创建跟踪器并与上下文交互的Tracer。通过使用OpenTelemetry API指定instrumentation library(正在工具化的库)名称和版本,可以获取跟踪器。有关详细信息,请参阅规范中的获取跟踪器章节。

如果您在应用程序的任何位置写入手动追踪代码,应该调用getTracer获取跟踪器。例如:

import io.opentelemetry.api.trace.Tracer;

Tracer tracer = openTelemetry.getTracer("instrumentation-scope-name", "instrumentation-scope-version");

‘instrumentation-scope-name’和‘instrumentation-scope-version’的值应该能够唯一标识Instrumentation Scope,例如包、模块或类名。这将有助于以后确定遥测数据的来源。虽然名称是必需的,但建议仍然提供版本尽管它是可选的。请注意,由单个OpenTelemetry实例创建的所有Tracer都将互操作,而与名称无关。

通常建议在应用程序中需要时调用getTracer而不是导出tracer实例给应用程序的其他部分。这有助于避免加载涉及其他所需依赖项时出现更棘手的应用程序加载问题。

示例应用的情况下,有两个地方可能会获取到跟踪器,具体如下:

首先,在RollControllerindex方法中,如下所示:

package otel;

// ...
import org.springframework.beans.factory.annotation.Autowired;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;

@RestController
public class RollController {
  private static final Logger logger = LoggerFactory.getLogger(RollController.class);
  private final Tracer tracer;

  @Autowired
  RollController(OpenTelemetry openTelemetry) {
    tracer = openTelemetry.getTracer(RollController.class.getName(), "0.1.0");
  }
  // ...
}

其次,在_library file_ Dice.java中:

// ...
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;

public class Dice {

  private int min;
  private int max;
  private Tracer tracer;

  public Dice(int min, int max, OpenTelemetry openTelemetry) {
    this.min = min;
    this.max = max;
    this.tracer = openTelemetry.getTracer(Dice.class.getName(), "0.1.0");
  }

  public Dice(int min, int max) {
    this(min, max, OpenTelemetry.noop())
  }

  // ...
}

另外,如果您正在编写库仪表化代码,强烈建议为用户提供将OpenTelemetry实例注入到仪表化代码中的能力。如果由于某种原因无法实现此操作,可以回退到使用GlobalOpenTelemetry类的实例:

import io.opentelemetry.api.GlobalOpenTelemetry;

Tracer tracer = GlobalOpenTelemetry.getTracer("instrumentation-scope-name", "instrumentation-scope-version");

请注意,无法强制要求用户配置全局设置,因此对于库的仪表化来说,这是最脆弱的选项。

创建跟踪

现在,您已经初始化了跟踪器,可以创建跟踪了。

要创建跟踪,只需要指定跟踪的名称即可。跟踪的开始和结束时间由OpenTelemetry SDK自动设置。

下面的代码演示了如何创建一个跟踪:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;

// ...
  @GetMapping("/rolldice")
  public List<Integer> index(@RequestParam("player") Optional<String> player,
      @RequestParam("rolls") Optional<Integer> rolls) {
    Span span = tracer.spanBuilder("rollTheDice").startSpan();

    // 使跟踪成为当前跟踪
    try (Scope scope = span.makeCurrent()) {

      if (!rolls.isPresent()) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing rolls parameter", null);
      }

      List<Integer> result = new Dice(1, 6).rollTheDice(rolls.get());

      if (player.isPresent()) {
        logger.info("{} is rolling the dice: {}", player.get(), result);
      } else {
        logger.info("Anonymous player is rolling the dice: {}", result);
      }
      return result;
    } catch(Throwable t) {
      span.recordException(t);
      throw t;
    } finally {
      span.end();
    }
  }

当您希望跟踪结束时,调用end()是必需的。

如果遵循了到目前为止的关于示例应用的指示,您可以将上述代码复制到RollControllerindex方法中。现在,您应该能够看到从您的应用程序发出的跟踪。

按照以下方式启动应用程序,然后通过访问http://localhost:8080/rolldice使用浏览器或curl发送请求:

gradle assemble
env \
OTEL_SERVICE_NAME=dice-server \
OTEL_TRACES_EXPORTER=logging \
OTEL_METRICS_EXPORTER=logging \
OTEL_LOGS_EXPORTER=logging \
java -jar ./build/libs/java-simple.jar

一段时间后,您应该在控制台上看到由LoggingSpanExporter打印的跟踪内容,类似于以下内容:

2023-08-02T17:22:22.658+02:00  INFO 2313 --- [nio-8080-exec-1] i.o.e.logging.LoggingSpanExporter        : 'rollTheDice' : 565232b11b9933fa6be8d6c4a1307fe2 6e1e011e2e8c020b INTERNAL [tracer: otel.RollController:0.1.0] {}

创建嵌套跟踪

大多数情况下,我们希望将嵌套操作的跟踪相关联。OpenTelemetry支持进程内和跨远程进程的追踪。有关如何共享远程进程之间的上下文的详细信息,请参阅上下文传播

例如,在Dice类的rollTheDice方法中调用rollOnce方法时,可以手动链接跟踪如下:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
// ...
  public List<Integer> rollTheDice(int rolls) {
    Span parentSpan = tracer.spanBuilder("parent").startSpan();
    List<Integer> results = new ArrayList<Integer>();
    try {
      for (int i = 0; i < rolls; i++) {
        results.add(this.rollOnce(parentSpan));
      }
      return results;
    } finally {
      parentSpan.end();
    }
  }

  private int rollOnce(Span parentSpan) {
    Span childSpan = tracer.spanBuilder("child")
        .setParent(Context.current().with(parentSpan))
        .startSpan();
    try {
      return ThreadLocalRandom.current().nextInt(this.min, this.max + 1);
    } finally {
      childSpan.end();
    }
  }

OpenTelemetry API还提供了一种自动方式来在当前线程上传播父跟踪:

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
// ...
  public List<Integer> rollTheDice(int rolls) {
    Span parentSpan = tracer.spanBuilder("parent").startSpan();
    try (Scope scope = parentSpan.makeCurrent()) {
      List<Integer> results = new ArrayList<Integer>();
      for (int i = 0; i < rolls; i++) {
        results.add(this.rollOnce());
      }
      return results;
    } finally {
      parentSpan.end();
    }
  }

  private int rollOnce() {
    Span childSpan = tracer.spanBuilder("child")
    // 注意:不需要`setParent(...)`;`Span.current()`自动添加为父级
    .startSpan();
    try(Scope scope = childSpan.makeCurrent()) {
      return ThreadLocalRandom.current().nextInt(this.min, this.max + 1);
    } finally {
      childSpan.end();
    }
  }
}

要链接远程进程的跟踪,只需要将Remote Context设置为父级即可。

Span childRemoteParent = tracer.spanBuilder("Child").setParent(remoteContext).startSpan();

获取当前跟踪

有时在程序执行的特定点上使用当前/活动的跟踪可能会很有帮助。

Span span = Span.current()

如果您想要特定Context对象的当前跟踪:

Span span = Span.fromContext(context)

跟踪属性

在OpenTelemetry中,可以自由地创建跟踪,并可以使用表示其所跟踪操作的属性对其进行注释。属性为跟踪提供关于其所跟踪操作的特定上下文,例如结果或操作属性。

Span span = tracer.spanBuilder("/resource/path").setSpanKind(SpanKind.CLIENT).startSpan();
span.setAttribute("http.method", "GET");
span.setAttribute("http.url", url.toString());

语义属性

为代表使用HTTP或数据库调用等众所周知的协议中的操作的跟踪定义了语义约定。这些跟踪的语义约定在规范中定义,详见Trace语义约定

首先,将语义约定作为依赖项添加到您的应用程序中:

dependencies {
  implementation("io.opentelemetry.semconv:opentelemetry-semconv:1.22.0-alpha")
}
<dependency>
    <groupId>io.opentelemetry.semconv</groupId>
    <artifactId>opentelemetry-semconv</artifactId>
    <version>1.22.0-alpha</version>
</dependency>

最后,您可以更新您的文件以包括语义属性:

Span span = tracer.spanBuilder("/resource/path").setSpanKind(SpanKind.CLIENT).startSpan();
span.setAttribute(SemanticAttributes.HTTP_METHOD, "GET");
span.setAttribute(SemanticAttributes.HTTP_URL, url.toString());

创建带有事件的跟踪

跟踪可以使用命名事件(称为跟踪事件)进行注释,这些事件可以携带零个或多个跟踪属性,每个跟踪属性本身都是一个自动与时间戳配对的键:值映射。

span.addEvent("Init");
...
span.addEvent("End");
Attributes eventAttributes = Attributes.of(
    AttributeKey.stringKey("key"), "value",
    AttributeKey.longKey("result"), 0L);

span.addEvent("End Computation", eventAttributes);

创建带链接的跟踪

一个跟踪可以链接到零个或多个与其因果相关的其他跟踪,这些跟踪通过跟踪链接相连。链接可用于表示批量操作,其中每个跟踪是由多个初始化跟踪发起的,每个初始化跟踪都表示批处理中正在处理的单个传入项。

Span child = tracer.spanBuilder("childWithLink")
        .addLink(parentSpan1.getSpanContext())
        .addLink(parentSpan2.getSpanContext())
        .addLink(parentSpan3.getSpanContext())
        .addLink(remoteSpanContext)
    .startSpan();

有关如何从远程进程中读取上下文的详细信息,请参阅上下文传播

设置跟踪状态

可以在跟踪上设置状态,通常用于指定跟踪尚未成功完成 - SpanStatus.Error。在极少数情况下,可以将Error状态覆盖为OK,但请勿在成功完成的跟踪上设置OK

可以在调用结束跟踪之前的任何时候设置状态:

Span span = tracer.spanBuilder("my span").startSpan();
// 将跟踪放入当前上下文
try (Scope scope = span.makeCurrent()) {
	// 进行一些操作
} catch (Throwable t) {
  span.setStatus(StatusCode.ERROR, "Something bad happened!");
  throw t;
} finally {
  span.end(); // 此调用后无法设置跟踪
}

在跟踪中记录异常

当异常发生时,将异常记录下来可能是一种很好的做法。建议在设置跟踪状态时同时执行此操作。

Span span = tracer.spanBuilder("my span").startSpan();
// 将跟踪放入当前上下文
try (Scope scope = span.makeCurrent()) {
	// 进行某些操作
} catch (Throwable throwable) {
  span.setStatus(StatusCode.ERROR, "Something bad happened!");
  span.recordException(throwable);
} finally {
  span.end(); // 此调用后无法设置跟踪
}

这将在跟踪中捕获当前堆栈跟踪等信息。

上下文传播

OpenTelemetry提供了一种使用W3C Trace Context HTTP标头将上下文传播到远程服务的基于文本的方法。

以下是使用HttpURLConnection进行出站HTTP请求的示例代码:

// 告诉OpenTelemetry在HTTP标头中注入上下文
TextMapSetter<HttpURLConnection> setter =
  new TextMapSetter<HttpURLConnection>() {
    @Override
    public void set(HttpURLConnection carrier, String key, String value) {
        // 作为标头插入上下文
        carrier.setRequestProperty(key, value);
    }
};

URL url = new URL("http://127.0.0.1:8080/resource");
Span outGoing = tracer.spanBuilder("/resource").setSpanKind(SpanKind.CLIENT).startSpan();
try (Scope scope = outGoing.makeCurrent()) {
  // 使用语义约定。
  // (请注意,要设置这些属性,Span在Context或Scope中不需要当前实例。)
  outGoing.setAttribute(SemanticAttributes.HTTP_METHOD, "GET");
  outGoing.setAttribute(SemanticAttributes.HTTP_URL, url.toString());
  HttpURLConnection transportLayer = (HttpURLConnection) url.openConnection();
  // 使用包含当前跟踪的上下文注入请求
  openTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), transportLayer, setter);
  // 进行去外部调用
} finally {
  outGoing.end();
}
...

同样,也可以使用基于文本的方法从传入请求中读取W3C Trace Context。下面是使用HttpExchange处理传入HTTP请求的例子:

TextMapGetter<HttpExchange> getter =
  new TextMapGetter<>() {
    @Override
    public String get(HttpExchange carrier, String key) {
      if (carrier.getRequestHeaders().containsKey(key)) {
        return carrier.getRequestHeaders().get(key).get(0);
      }
      return null;
    }

   @Override
   public Iterable<String> keys(HttpExchange carrier) {
     return carrier.getRequestHeaders().keySet();
   }
};
...
public void handle(HttpExchange httpExchange) {
  // 从请求中提取SpanContext和其他元素。
  Context extractedContext = openTelemetry.getPropagators().getTextMapPropagator()
        .extract(Context.current(), httpExchange, getter);
  try (Scope scope = extractedContext.makeCurrent()) {
    // 使用提取的SpanContext自动使用为父级。
    Span serverSpan = tracer.spanBuilder("GET /resource")
        .setSpanKind(SpanKind.SERVER)
        .startSpan();
    try {
      // 添加语义约定中定义的属性
      serverSpan.setAttribute(SemanticAttributes.HTTP_METHOD, "GET");
      serverSpan.setAttribute(SemanticAttributes.HTTP_SCHEME, "http");
      serverSpan.setAttribute(SemanticAttributes.HTTP_HOST, "localhost:8080");
      serverSpan.setAttribute(SemanticAttributes.HTTP_TARGET, "/resource");
      // 处理请求
      ...
    } finally {
      serverSpan.end();
    }
  }
}

下面的代码展示了一个示例,读取传入请求的W3C Trace Context,添加跟踪并进一步传播上下文。该示例使用 HttpHeaders 来获取用于上下文传播的 traceparent 标头。

TextMapGetter<HttpHeaders> getter =
  new TextMapGetter<HttpHeaders>() {
    @Override
    public String get(HttpHeaders headers, String s) {
      assert headers != null;
      return headers.getHeaderString(s);
    }

    @Override
    public Iterable<String> keys(HttpHeaders headers) {
      List<String> keys = new ArrayList<>();
      MultivaluedMap<String, String> requestHeaders = headers.getRequestHeaders();
      requestHeaders.forEach((k, v) ->{
        keys.add(k);
      });
    }
};

TextMapSetter<HttpURLConnection> setter =
  new TextMapSetter<HttpURLConnection>() {
    @Override
    public void set(HttpURLConnection carrier, String key, String value) {
        // Insert the context as Header
        carrier.setRequestProperty(key, value);
    }
};

//...
public void handle(<Library Specific Annotation> HttpHeaders headers){
        Context extractedContext = opentelemetry.getPropagators().getTextMapPropagator()
                .extract(Context.current(), headers, getter);
        try (Scope scope = extractedContext.makeCurrent()) {
            // Automatically use the extracted SpanContext as parent.
            Span serverSpan = tracer.spanBuilder("GET /resource")
                .setSpanKind(SpanKind.SERVER)
                .startSpan();

            try(Scope ignored = serverSpan.makeCurrent()) {
                // Add the attributes defined in the Semantic Conventions
                serverSpan.setAttribute(SemanticAttributes.HTTP_METHOD, "GET");
                serverSpan.setAttribute(SemanticAttributes.HTTP_SCHEME, "http");
                serverSpan.setAttribute(SemanticAttributes.HTTP_HOST, "localhost:8080");
                serverSpan.setAttribute(SemanticAttributes.HTTP_TARGET, "/resource");

                HttpURLConnection transportLayer = (HttpURLConnection) url.openConnection();
                // Inject the request with the *current*  Context, which contains our current Span.
                openTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), transportLayer, setter);
                // Make outgoing call
            }finally {
                serverSpan.end();
            }
      }
}

指标

Spans提供了关于您的应用程序的详细信息,但生成的数据与系统负载成正比。相比之下,metrics将各个测量值合并为聚合值,生成的数据与系统负载的函数成比例。这些聚合值缺乏诊断低级问题所需的详细信息,但它们通过帮助识别趋势和提供应用程序运行时遥测数据来补充spans。

metrics API定义了各种instruments。instruments记录测量值,这些测量值由metrics SDK聚合后最终在进程外导出。instruments有同步和异步两种类型。同步instruments接收测量值发生时进行记录。异步instruments注册一个回调函数,每次调用时会激发该回调函数,并记录此时的测量值。以下是可用的instruments:

  • LongCounter/DoubleCounter:仅记录正值,有同步和异步选项。用于计数,如通过网络发送的字节数。Counter测量值默认按照递增的单调和进行聚合。
  • LongUpDownCounter/DoubleUpDownCounter:记录正值和负值,有同步和异步选项。用于计数上升和下降的内容,例如队列的大小。Up down counter测量值默认按照非单调和进行聚合。
  • LongGauge/DoubleGauge:用异步回调函数来记录瞬时值。用于记录无法跨属性合并的值,例如CPU利用率百分比。Gauge测量值默认作为gauge进行聚合。
  • LongHistogram/DoubleHistogram:记录最有用于分析的直方图分布的测量值。没有异步选项。用于记录HTTP服务器处理请求的持续时间等内容。Histogram测量值默认按照显式bucket histogram进行聚合。

注意:计数器和上下计数器的异步类型假设注册的回调函数观察累积和。例如,如果您注册一个异步的计数器,它的回调函数记录通过网络发送的字节数,那么它必须记录通过网络发送的所有字节数的累加和,而不是尝试计算和记录自上次调用以来的差异。

所有指标都可以带有属性的注释:一些附加的限定词,用于帮助描述指标代表的测量细分。

下面是计数器用法示例:

OpenTelemetry openTelemetry = // 获取OpenTelemetry实例

// 获取或创建命名的meter实例
Meter meter = openTelemetry.meterBuilder("instrumentation-library-name")
        .setInstrumentationVersion("1.0.0")
        .build();

// 构建计数器,例如LongCounter
LongCounter counter = meter
      .counterBuilder("processed_jobs")
      .setDescription("Processed jobs")
      .setUnit("1")
      .build();

// 建议API用户保留要记录的属性的引用
Attributes attributes = Attributes.of(AttributeKey.stringKey("Key"), "SomeWork");

// 记录数据
counter.add(123, attributes);

下面是异步instrument的用法示例:

// 构建异步instrument,例如Gauge
meter
  .gaugeBuilder("cpu_usage")
  .setDescription("CPU Usage")
  .setUnit("ms")
  .buildWithCallback(measurement -> {
    measurement.record(getCpuUsage(), Attributes.of(AttributeKey.stringKey("Key"), "SomeWork"));
  });

日志

日志与度量和跟踪不同,因为没有面向用户的日志API。相反,有工具来将现有的流行日志框架(例如SLF4j、JUL、Logback、Log4j)与OpenTelemetry生态系统进行桥接。

下面讨论的两个典型工作流程分别满足不同的应用需求。

直接发送给收集器

在直接发送给收集器的工作流中,日志直接从应用程序通过网络协议(例如OTLP)发送到收集器。这种工作流程设置简单,因为它不需要任何额外的日志转发组件,并且允许应用程序轻松地发送符合日志数据模型的结构化日志。然而,由于应用程序需要将日志排队并导出到网络位置,因此它可能不适用于所有应用程序。

使用这种工作流程需要执行以下操作:

日志添加器

日志添加器将日志从日志框架中桥接到OpenTelemetry的日志SDK,并使用Logs Bridge API。针对各种流行的Java日志框架,都提供了日志添加器:

上面的链接包含完整的使用说明和安装文档,但通常的安装步骤如下:

  • 通过gradle或maven添加所需的依赖项。
  • 扩展应用程序的日志配置(例如logback.xmllog4j.xml等),包括对OpenTelemetry日志添加器的引用。
    • 可选择配置日志框架来确定传递给添加器的日志(例如,按严重性或记录器名称过滤)。
    • 可选择配置添加器来指示如何将日志映射到OpenTelemetry日志记录(例如,捕获线程信息、上下文数据、标记等)。

日志添加器自动将跟踪上下文包括在日志记录中,以实现日志与跟踪的关联。

日志添加器示例演示了多种场景的设置。

通过文件或标准输出

在文件或标准输出的工作流中,日志被写入文件或标准输出。另一个组件(例如FluentBit)负责读取/追踪日志,将其解析为更结构化的格式,并将其转发到目标位置,例如收集器。这种工作流程可能更适用于不允许从直接发送给收集器中产生额外开销的应用程序要求。但是,它要求将所有需要在下游处理的日志字段编码到日志中,并且读取日志的组件将数据解析为日志数据模型。日志转发组件的安装和配置不在本文档的范围内。

通过安装日志上下文插桩,可以实现日志与跟踪的关联。

日志上下文插桩

OpenTelemetry提供了一些用于各种流行的Java日志框架的log上下文富化器:

上面的链接包含完整的使用说明和安装文档,但通常的安装步骤如下:

  • 通过gradle或maven添加所需的依赖项。
  • 修改应用程序的日志配置(例如logback.xmllog4j.xml等),以引用日志模式中的跟踪上下文字段。

SDK配置

本文档中报告的配置示例仅适用于opentelemetry-sdk提供的SDK。其他实现可能提供不同的配置机制。

跟踪SDK

应用程序必须使用导出器安装一个跨度处理器,并可以自定义OpenTelemetry SDK的行为。

例如,基本配置实例化SDK跟踪器提供程序,并设置将跟踪导出到日志流。

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
  .addSpanProcessor(BatchSpanProcessor.builder(LoggingSpanExporter.create()).build())
  .build();

采样器

在应用程序中不总是可行的对每个用户请求都进行跟踪和导出。为了在可观测性和开销之间寻找平衡,可以对跟踪进行采样。

OpenTelemetry SDK提供了四种内置采样器:

  • AlwaysOnSampler:无论上游采样决策如何,都对每个跟踪进行采样。
  • AlwaysOffSampler:不对任何跟踪进行采样,无论上游采样决策如何。
  • ParentBased:使用父跨度进行采样决策(如果存在)。
  • TraceIdRatioBased:对可配置百分比的跟踪进行采样,并额外对上游采样过的跨度进行采样。

可以通过实现io.opentelemetry.sdk.trace.Sampler接口来提供其他采样器。

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
  .setSampler(Sampler.alwaysOn())
  //或
  .setSampler(Sampler.alwaysOff())
  //或
  .setSampler(Sampler.traceIdRatioBased(0.5))
  .build();

跨度处理器

OpenTelemetry提供了多种Span处理器。SimpleSpanProcessor立即将已结束的Span转发给导出器,而BatchSpanProcessor会将其批量处理并以批量形式发送。可以使用MultiSpanProcessor同时配置多个处于活动状态的Span处理器。

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
  .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create()))
  .addSpanProcessor(BatchSpanProcessor.builder(LoggingSpanExporter.create()).build())
  .build();

导出器

跨度处理器通过导出器进行初始化,导出器将负责将跟踪数据发送到特定的后端。OpenTelemetry提供了五种内置导出器:

  • InMemorySpanExporter:将数据保留在内存中,用于测试和调试。
  • Jaeger导出器:通过gRPC将收集的遥测数据准备并发送到Jaeger后端。包括JaegerGrpcSpanExporterJaegerThriftSpanExporter等不同的类型。
  • ZipkinSpanExporter:将收集的遥测数据准备并发送到Zipkin后端。
  • 日志导出器:将遥测数据保存到日志流中。包括LoggingSpanExporterOtlpJsonLoggingSpanExporter等不同的类型。
  • OpenTelemetry协议导出器:将数据以OTLP格式发送给[OpenTelemetry收集器]或其他OTLP接收器。包括OtlpGrpcSpanExporterOtlpHttpSpanExporter等不同的类型。

可以在[OpenTelemetry注册表]中找到其他导出器。

ManagedChannel jaegerChannel = ManagedChannelBuilder.forAddress("localhost", 3336)
  .usePlaintext()
  .build();

JaegerGrpcSpanExporter jaegerExporter = JaegerGrpcSpanExporter.builder()
  .setEndpoint("localhost:3336")
  .setTimeout(30, TimeUnit.SECONDS)
  .build();

SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
  .addSpanProcessor(BatchSpanProcessor.builder(jaegerExporter).build())
  .build();

Metrics SDK

应用程序必须使用指标读取器和导出器安装一个度量读取器,并可以进一步自定义OpenTelemetry SDK的行为。

例如,基本配置实例化SDK度量计提供程序,并设置将度量读取到日志流。

SdkMeterProvider meterProvider = SdkMeterProvider.builder()
  .registerMetricReader(PeriodicMetricReader.builder(LoggingMetricExporter.create()).build())
  .build();

指标读取器

指标读取器读取聚合指标。

SdkMeterProvider meterProvider = SdkMeterProvider.builder()
  .registerMetricReader(...)
  .build();

OpenTelemetry内置提供了多种指标读取器:

  • PeriodicMetricReader:在可配置的时间间隔上读取指标,并将其推送到MetricExporter
  • InMemoryMetricReader:将指标读取到内存中,有助于调试和测试。
  • PrometheusHttpServer(Alpha):设置一个HTTP服务器,读取度量并将其序列化为Prometheus文本格式。

当前不支持自定义指标读取器实现。

导出器

PeriodicMetricReader与度量导出器配对,后者负责将遥测数据发送到特定的后端。OpenTelemetry提供了以下内置的导出器:

  • InMemoryMetricExporter:将数据保留在内存中,用于测试和调试。
  • 日志导出器:将遥测数据保存到日志流中。包括LoggingMetricExporterOtlpJsonLoggingMetricExporter等不同的类型。
  • OpenTelemetry协议导出器:将数据以OTLP格式发送给[OpenTelemetry收集器]或其他OTLP接收器。包括OtlpGrpcMetricExporterOtlpHttpMetricExporter等不同的类型。

可以在[OpenTelemetry注册表]中找到其他导出器。

视图

视图提供了一种控制如何将测量聚合为指标的机制。它们由InstrumentSelectorView组成。InstrumentSelector包含一系列选项,用于选择应用于视图的instruments。可以通过名称、类型、meter名称、meter版本和meter模式URL的组合来选择instruments。视图描述了如何聚合测量结果。视图可以更改名称、描述、聚合方式,并定义在保留的属性key集合中的属性键。

SdkMeterProvider meterProvider = SdkMeterProvider.builder()
  .registerView(
    InstrumentSelector.builder()
      .setName("my-counter") // 选择名称为"my-counter"的instrument(s)
      .build(),
    View.builder()
      .setName("new-counter-name") // 将名称更改为"new-counter-name"
      .build())
  .registerMetricReader(...)
  .build();

每个instrument都有一个默认视图,保留原始名称、描述和属性,并具有基于instrument类型的默认聚合。当注册的视图与instrument匹配时,默认视图将被注册的视图替换。与instrument匹配的其他注册视图是累加的,并导致为该instrument导出多个指标。

日志SDK

日志SDK定义了在使用直接发送给收集器工作流程时如何处理日志。在使用通过文件或标准输出工作流程时不需要日志SDK。

典型的日志SDK配置安装日志记录处理器和导出器。例如,以下配置安装了BatchLogRecordProcessor,它会定期通过OtlpGrpcLogRecordExporter导出到网络位置:

SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder()
  .addLogRecordProcessor(
    BatchLogRecordProcessor.builder(
      OtlpGrpcLogRecordExporter.builder()
          .setEndpoint("http://localhost:4317")
          .build())
      .build())
  .build();

日志记录处理器

日志记录处理器处理由日志添加器发出的日志记录。

OpenTelemetry提供了以下日志记录处理器:

通过实现LogRecordProcessor接口,可以支持自定义日志记录处理器。常见用例包括使用上下文数据(如行李)丰富日志记录,或过滤/混淆敏感数据。

日志记录导出器

BatchLogRecordProcessorSimpleLogRecordProcessorLogRecordExporter配对,后者负责将遥测数据发送到特定的后端。OpenTelemetry提供以下导出器:

  • OpenTelemetry协议导出器:将数据以OTLP格式发送给[OpenTelemetry收集器]或其他OTLP接收器。包括OtlpGrpcLogRecordExporterOtlpHttpLogRecordExporter等不同的类型。
  • InMemoryLogRecordExporter:将数据保留在内存中,用于测试和调试。
  • 日志导出器:将遥测数据保存到日志流中。包括SystemOutLogRecordExporterOtlpJsonLoggingLogRecordExporter等不同的类型。注意:OtlpJsonLoggingLogRecordExporter记录到JUL,如果没有仔细配置可能会导致无限循环(即JUL -> SLF4J -> Logback -> OpenTelemetry Appender -> OpenTelemetry Log SDK -> JUL)。

通过实现LogRecordExporter接口,可以支持自定义导出器。

自动配置

您可以通过SDK的自动配置扩展(通过opentelemetry-sdk-extension-autoconfigure模块提供)来自动创建OpenTelemetry实例,而不是通过代码直接使用SDK构建器。

通过将以下依赖项添加到应用程序中,可以使用此模块:

<dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
</dependency>

它允许您基于一组标准支持的环境变量和系统属性自动配置OpenTelemetry SDK。每个环境变量都有一个相应的命名系统属性,命名方式与环境变量相同,但使用小写的.(点)字符作为分隔符,而不是_(下划线)。

逻辑服务名称可以通过OTEL_SERVICE_NAME环境变量(或otel.service.name系统属性)来指定。

跟踪、度量或日志导出器可以通过OTEL_TRACES_EXPORTEROTEL_METRICS_EXPORTEROTEL_LOGS_EXPORTER环境变量进行设置。例如,OTEL_TRACES_EXPORTER=jaeger配置应用程序使用Jaeger导出器。还必须在应用程序的类路径中提供相应的Jaeger导出器库。

还可以通过OTEL_PROPAGATORS环境变量来设置传播器,例如使用tracecontext值来使用W3C Trace Context

更多详细信息,请参阅该模块的自述文件中的所有支持的配置选项。

必须从您的代码中初始化SDK自动配置,以便让模块遍历提供的环境变量(或系统属性),并使用内部的构建器设置OpenTelemetry实例。

OpenTelemetrySdk sdk = AutoConfiguredOpenTelemetrySdk.initialize()
    .getOpenTelemetrySdk();

当环境变量或系统属性不足时,可以使用通过自动配置SPI提供的一些扩展点和AutoConfiguredOpenTelemetrySdk类中的多个方法。

以下是添加额外自定义跨度处理器的代码片段示例。

AutoConfiguredOpenTelemetrySdk.builder()
        .addTracerProviderCustomizer(
            (sdkTracerProviderBuilder, configProperties) ->
                sdkTracerProviderBuilder.addSpanProcessor(
                    new SpanProcessor() { /* implementation omitted for brevity */ }))
        .build();

SDK日志记录和错误处理

OpenTelemetry使用java.util.logging来记录有关OpenTelemetry的信息,包括有关配置错误和数据导出失败的错误和警告。

默认情况下,日志消息由应用程序中的根处理程序处理。如果您没有为应用程序安装自定义根处理程序,则默认情况下将将级别为INFO或更高的日志发送到控制台。

您可能希望更改OpenTelemetry记录器的行为。例如,您可以降低日志记录级别以在调试时输出附加信息,将特定类的级别提高以忽略来自该类的错误,或安装自定义处理程序或过滤器以在OpenTelemetry记录特定消息时运行自定义代码。

示例

# 关闭所有OpenTelemetry日志记录
io.opentelemetry.level = OFF
# 仅关闭BatchSpanProcessor的日志记录
io.opentelemetry.sdk.trace.export.BatchSpanProcessor.level = OFF
# 以方便进行调试的“FINE”级别记录
io.opentelemetry.level = FINE

# 设置默认ConsoleHandler的日志记录级别
# 请注意,这也会影响OpenTelemetry以外的日志记录
java.util.logging.ConsoleHandler.level = FINE

通过自定义处理程序和过滤器,可以实现更精细的控制和特殊情况处理。

// 自定义过滤器,不记录来自导出的错误
public class IgnoreExportErrorsFilter implements Filter {

 public boolean isLoggable(LogRecord record) {
    return !record.getMessage().contains("Exception thrown by the export");
 }
}
# 在BatchSpanProcessor上注册自定义过滤器
io.opentelemetry.sdk.trace.export.BatchSpanProcessor = io.opentelemetry.extension.logging.IgnoreExportErrorsFilter
最后修改 December 13, 2023: improve glossary translation (46f8201b)