支持集成Agent 自动
插桩(Instrumentation) - 无需改变代码
手动
编码进行插桩 - 侵入代码
Exporter - 导出器,将OTel相关数据导出到OTel Collector 或者 具体的监控后端
OTel Collector - OTel官方的收集器(支持OTLP gRpc/http协议)
支持的编程语言:
Java自动插桩使用的Java agent JAR可以附加到任何Java 8+应用程序。它动态注入字节码来捕获来自许多流行库和框架的遥测。它可以用于在应用程序或服务的“边缘”捕获遥测数据,例如入站请求、出站HTTP调用、数据库调用等。
支持的库、框架等包括:
具体支持情况可参见:
https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md
下载:opentelemetry-javaagent.jar
方式1 - 使用java启动命令指定javaagment:
java -javaagent:path/to/opentelemetry-javaagent.jar
-Dotel.service.name=your-service-name
-jar myapp.jar
方式2 - 通过环境变量全局配置javaagent:
export JAVA_TOOL_OPTIONS="-javaagent:path/to/opentelemetry-javaagent.jar"
export OTEL_SERVICE_NAME="your-service-name"
java -jar myapp.jar
方式1 - 通过Java系统属性:
java -javaagent:path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=your-service-name \
-Dotel.traces.exporter=zipkin \
-jar myapp.jar
方式2 - 通过环境变量:
OTEL_SERVICE_NAME=your-service-name \
OTEL_TRACES_EXPORTER=zipkin \
java -javaagent:path/to/opentelemetry-javaagent.jar \
-jar myapp.jar
方式3 - 通过属性文件:
OTEL_JAVAAGENT_CONFIGURATION_FILE=path/to/properties/file.properties \
java -javaagent:path/to/opentelemetry-javaagent.jar \
-jar myapp.jar
开启agent debug日志:
-Dotel.javaagent.debug=true
关于opentelemetry-javaagent.jar的更多配置可参见:
https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md
https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/
添加maven依赖:
<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-instrumentation-annotationsartifactId>
<version>1.31.0version>
dependency>
dependencies>
import io.opentelemetry.instrumentation.annotations.WithSpan;
public class MyClass {
@WithSpan
public void myMethod() {
<...>
}
@WithSpan
public void myMethod(@SpanAttribute("parameter1") String parameter1,
@SpanAttribute("parameter2") long parameter2) {
<...>
}
}
-Dotel.instrumentation.opentelemetry-instrumentation-annotations.exclude-methods=
my.package.MyClass1[method1,method2];my.package.MyClass2[method3]
适用于无法修改源码的情况:
-Dotel.instrumentation.methods.include=
my.package.MyClass1[method1,method2];my.package.MyClass2[method3]
https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/#java-agent-logging-output
The agent’s logging output can be configured by setting the following property:
System property: otel.javaagent.logging
Description: The Java agent logging mode. The following 3 modes are supported:
simple
: The agent will print out its logs using the standard error stream. Only INFO or higher logs will be printed. This is the default Java agent logging mode.none
: The agent will not log anything - not even its own version.application
: The agent will attempt to redirect its own logs to the instrumented application’s slf4j logger. This works the best for simple one-jar applications that do not use multiple classloaders; Spring Boot apps are supported as well. The Java agent output logs can be further configured using the instrumented application’s logging configuration (e.g. logback.xml or log4j2.xml). Make sure to test that this mode works for your application before running it in a production environment应用端通过Agent注入opentelemetry-javaagent.jar,
应用端代码仅需集成OTel API接口依赖(无需集成SDK实现依赖),
额外需集成Logback OTel Appender相关依赖(若不需要可移除)。
注:
最推荐此种集成方式,
保留了Agent注入,减少了代码侵入,
仅当有自定义Traces/Metrics等需求时,依赖OTel Api完成指标等埋点,
即便不注入Agent,也不影响本地应用开发与运行,
在线上环境通过Docker镜像、K8S挂载等完成Agent注入。
<properties>
<otel.version>1.32.0otel.version>
<otel.springboot.version>1.32.0-alphaotel.springboot.version>
<otel.logback.version>1.32.0-alphaotel.logback.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetrygroupId>
<artifactId>opentelemetry-bomartifactId>
<version>${otel.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-instrumentation-annotationsartifactId>
<version>${otel.version}version>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-logback-appender-1.0artifactId>
<version>${otel.logback.version}version>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-logback-mdc-1.0artifactId>
<version>${otel.logback.version}version>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetrygroupId>
<artifactId>opentelemetry-apiartifactId>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-instrumentation-annotationsartifactId>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-logback-appender-1.0artifactId>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-logback-mdc-1.0artifactId>
dependency>
dependencies>
启动命令注入及配置opentelemetry-javaagent.jar:
java
-javaagent:D:/programs/Java/OTel/opentelemetry-javaagent.jar
# 导出traces为控制台日志打印
-Dotel.traces.exporter=logging
# 导出metrics为控制台日志打印
-Dotel.metrics.exporter=logging
# 禁用log日志导出(若使用logging则控制台会出现日志框架如logback打印一次,OTel logging再打印一次,比较混乱,故暂且禁用)
-Dotel.logs.exporter=none
-jar myapp.jar
如下为导出traces、metrics、logs均到OTLP Collector的相关配置:
java
-javaagent:D:/programs/Java/OTel/opentelemetry-javaagent.jar
-Dotel.traces.exporter=otlp
-Dotel.metrics.exporter=otlp
-Dotel.logs.exporter=otlp
-Dotel.exporter.otlp.endpoint=http://localhost:4317
-jar myapp.jar
如下为导出traces到Jaeger的相关配置:
注:
此种方式已被弃用,目前最新版版本的Jaeger已经内嵌OTel Collector,
可直接通过OTLP协议接收数据。
java
-javaagent:D:/programs/Java/OTel/opentelemetry-javaagent.jar
# 导出traces到Jaeger端(Jaeger后端需根据实际环境进行调整)
-Dotel.traces.exporter=jaeger
-Dotel.exporter.jaeger.endpoint=http://10.170.xx.xxx:xxx
-Dotel.exporter.jaeger.timeout=10000
# 导出metrics为控制台日志打印
-Dotel.metrics.exporter=logging
# 禁用log日志导出
-Dotel.logs.exporter=none
-jar myapp.jar
导出方式:
关于导出器的更多配置可参见:
https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md#exporters
OTel工具类:
/**
* OpenTelemetry工具类
*
* @author luohq
* @date 2023-11-20 10:01
*/
@Component
public class OTelUtils {
private static final Logger log = LoggerFactory.getLogger(OTelUtils.class);
/**
* OTel实例
*/
private static OpenTelemetry OTEL_INSTANCE;
/**
* 兼容OTel SpringBoot Starter自动注入
*
* @param openTelemetry SpringBoot OTel实例
*/
@Autowired(required = false)
public void setOpenTelemetry(OpenTelemetry openTelemetry) {
OTelUtils.OTEL_INSTANCE = openTelemetry;
}
/**
* 获取OTel实例
*
* @return OTel实例
*/
public static OpenTelemetry openTelemetry() {
//获取自动注入的OTel实例
if (Objects.nonNull(OTEL_INSTANCE)) {
return OTEL_INSTANCE;
}
//获取全局配置中的OTel实例(使用Agent注入)
return GlobalOpenTelemetry.get();
}
}
业务代码集成OTel:
//使用@WithSpan和@SpanAttribute生成Span
@WithSpan(value = "Manual::GoodsService::findGoodsPage")
@Override
public RespResult<GoodsVo> findGoodsPage(@SpanAttribute GoodsPageQueryDto goodsPageQueryDto) {
/** 获取当前Span */
Span curSpan = Span.current();
curSpan.setAttribute("attr.custom", "luohq-test-svc");
/** 自定义指标 */
Meter meter = OTelUtils.openTelemetry().meterBuilder("GoodsService::findGoodsPage")
.setInstrumentationVersion("v1.0")
.build();
//构建计数器
LongCounter findCounter = meter.counterBuilder("findGoodsPage.count")
.setDescription("FindGoodsPage Sum Count")
.setUnit("1")
.build();
findCounter.add(1);
/** 自定义Span */
Tracer tracer = OTelUtils.openTelemetry().getTracer(GoodsMapper.class.getSimpleName());
Span daoSpan = tracer.spanBuilder("Manual::GoodsMapper::findGoodsWithCNamePage").startSpan();
daoSpan.setAttribute("attr.custom", "luohq-test-dao");
try (Scope scope = daoSpan.makeCurrent()) {
//原处理逻辑
log.info("findGoodsPage param: {}", goodsPageQueryDto);
IPage<GoodsVo> goodsPage = this.goodsMapper.findGoodsWithCNamePage(this.toPage(goodsPageQueryDto), goodsPageQueryDto);
log.info("findGoodsPage result: {}", goodsPage);
return RespResult.successRows(goodsPage.getTotal(), goodsPage.getRecords());
} catch (Throwable throwable) {
//设置Span状态
daoSpan.setStatus(StatusCode.ERROR, "Something bad happened!");
//记录异常堆栈
daoSpan.recordException(throwable);
return RespResult.failed();
}finally {
daoSpan.end();
}
}
如下日志配置需使用spring.profiles.active来激活对应的otel或者otel-mdc配置,
如果不需要可移除springProfile段落,直接在configuration下配置相应的日志配置即可,
其中otel、otel-mdc均会自动将日志框架Logback集成OTel并导出日志到相应后端(如默认导出到OTel Collector),
相较于otel,otel-mdc通过 MDC(Mapped Diagnostic Context, 映射调试上下文机制) 将trace_id、span_id、trace_flags添加到日志中。
具体logback-spring.xml配置:
<configuration scan="true" scanPeriod=" 5 seconds">
<springProfile name="default">
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
pattern>
encoder>
appender>
<root level="INFO">
<appender-ref ref="console"/>
root>
springProfile>
<springProfile name="otel">
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
pattern>
encoder>
appender>
<appender name="OpenTelemetry"
class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
appender>
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="OpenTelemetry"/>
root>
springProfile>
<springProfile name="otel-mdc">
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{trace_id}:%X{span_id}:%X{trace_flags}] [%thread] %-5level %logger{36} - %msg%npattern>
encoder>
appender>
<appender name="otel" class="io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender">
<appender-ref ref="console"/>
appender>
<root level="INFO">
<appender-ref ref="otel"/>
root>
springProfile>
configuration>
关于trace_flags定义参见:
https://www.w3.org/TR/trace-context/#trace-flags
SpringBoot应用需依赖opentelemetry-spring-boot-starter
,
此种方式下由于starter端依赖了OTel SDK,所以无需Agent注入,
该starter集成了OTel API/SDK,并对OpenTelemetry进行了自动配置,
该starter引入的依赖如下:
<properties>
<otel.version>1.32.0otel.version>
<otel.springboot.version>1.32.0-alphaotel.springboot.version>
<otel.logback.version>1.32.0-alphaotel.logback.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetrygroupId>
<artifactId>opentelemetry-bomartifactId>
<version>${otel.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-instrumentation-annotationsartifactId>
<version>${otel.version}version>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-logback-appender-1.0artifactId>
<version>${otel.logback.version}version>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-logback-mdc-1.0artifactId>
<version>${otel.logback.version}version>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-spring-boot-starterartifactId>
<version>${otel.springboot.version}version>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-logback-appender-1.0artifactId>
dependency>
<dependency>
<groupId>io.opentelemetry.instrumentationgroupId>
<artifactId>opentelemetry-logback-mdc-1.0artifactId>
dependency>
<dependency>
<groupId>io.opentelemetrygroupId>
<artifactId>opentelemetry-exporter-jaegerartifactId>
dependency>
dependencies>
application.yaml:
otel:
# 导出器配置
exporter:
# OTLP导出
otlp:
enabled: false
endpoint: http://localhost:4317
timeout: 10s
# 导出到Zipkin
zipkin:
enabled: false
endpoint: http://localhost:9411/api/v2/spans
# 导出到Jaeger
jaeger:
enabled: true
endpoint: http://10.170.xx.xxx:xxxx
timeout: 10s
# 导出到日志
logging:
enabled: true
traces:
sampler:
# 采样频率
probability: 1.0
更多配置说明可参见:
https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/spring/spring-boot-autoconfigure/README.md#features
关于自定义Traces / Metrics、Logback集成OTel可参见 方式2。
// ...
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
public class Dice {
private int min;
private int max;
private Tracer tracer;
//自动注入OpenTelemetry对象
public Dice(int min, int max, OpenTelemetry openTelemetry) {
this.min = min;
this.max = max;
//获取Tracer
this.tracer = openTelemetry.getTracer(Dice.class.getName(), "0.1.0");
}
public Dice(int min, int max) {
this(min, max, OpenTelemetry.noop())
}
public List<Integer> rollTheDice(int rolls) {
//通过Tracer创建Span
Span parentSpan = tracer.spanBuilder("parent")
//添加关联的Context
//.addLink(parentSpan1.getSpanContext())
//.addLink(parentSpan2.getSpanContext())
//.addLink(remoteSpanContext)
.startSpan();
//为Span添加属性
parentSpan .setAttribute(SemanticAttributes.HTTP_METHOD, "GET");
parentSpan .setAttribute(SemanticAttributes.HTTP_URL, "/rolldice");
//为Span添加事件
Attributes eventAttributes = Attributes.of(
AttributeKey.stringKey("key"), "value",
AttributeKey.longKey("result"), 0L
);
parentSpan .addEvent("End Computation", eventAttributes);
//开始Span处理
try (Scope scope = parentSpan.makeCurrent()) {
List<Integer> results = new ArrayList<Integer>();
for (int i = 0; i < rolls; i++) {
//span嵌套
results.add(this.rollOnce());
}
return results;
} catch (Throwable throwable) {
//设置Span状态
span.setStatus(StatusCode.ERROR, "Something bad happened!");
//记录异常堆栈
span.recordException(throwable);
}finally {
//结束Span
parentSpan.end();
}
}
private int rollOnce() {
//子Span处理
Span childSpan = tracer.spanBuilder("child")
// NOTE: setParent(...) is not required;
// `Span.current()` is automatically added as the parent
.startSpan();
try(Scope scope = childSpan.makeCurrent()) {
return ThreadLocalRandom.current().nextInt(this.min, this.max + 1);
} finally {
//结束Span
childSpan.end();
}
}
}
Context传递示例:
//Context读取(从请求头中获取context信息,如请求头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);
});
return keys.
}
};
//Context设置(向请求中写入context信息,如请求头traceparent)
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();
}
}
}
种类:
OpenTelemetry openTelemetry = // obtain instance of OpenTelemetry
// Gets or creates a named meter instance
Meter meter = openTelemetry.meterBuilder("instrumentation-library-name")
.setInstrumentationVersion("1.0.0")
.build();
// Build counter e.g. LongCounter
LongCounter counter = meter
.counterBuilder("processed_jobs")
.setDescription("Processed jobs")
.setUnit("1")
.build();
// It is recommended that the API user keep a reference to Attributes they will record against
Attributes attributes = Attributes.of(AttributeKey.stringKey("Key"), "SomeWork");
// Record data
counter.add(123, attributes);
------------------------------
// Build an asynchronous instrument, e.g. Gauge
meter
.gaugeBuilder("cpu_usage")
.setDescription("CPU Usage")
.setUnit("ms")
.buildWithCallback(measurement -> {
measurement.record(getCpuUsage(), Attributes.of(AttributeKey.stringKey("Key"), "SomeWork"));
});