一、依赖配置
已在 `pom.xml` 中添加以下opentelemetry相关依赖:
<!-- OpenTelemetry API -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.24.0</version>
</dependency>
<!-- OpenTelemetry 注解支持 -->
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>1.24.0</version>
</dependency>
<!-- OpenTelemetry SDK -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.24.0</version>
</dependency>
<!-- OpenTelemetry OTLP Exporter -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>1.24.0</version>
</dependency>
二、应用配置
使用方式
1. Controller 层监控
在 Controller 方法上使用 `@Trace` 注解或 `@WithSpan` 注解:
package com.webfunny.member.controller;
import com.webfunny.common.annotation.Trace;
import com.webfunny.common.entity.FebsResponse;
import com.webfunny.member.entity.Member;
import com.webfunny.member.service.IMemberService;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("member")
public class MemberController {
@Autowired
private IMemberService memberService;
/**
* 方式一:使用自定义 @Trace 注解(推荐)
* 会自动采集 HTTP 请求信息:URL、方法、IP、User-Agent 等
*/
@GetMapping("/{id}")
@Trace(value = "getMemberById", kind = "controller")
public FebsResponse getMemberById(@PathVariable Long id) {
Member member = memberService.findById(id);
return new FebsResponse().success().data(member);
}
/**
* 方式二:使用 OpenTelemetry 原生 @WithSpan 注解
*/
@PostMapping("/save")
@WithSpan("saveMember")
public FebsResponse saveMember(@RequestBody Member member) {
memberService.save(member);
return new FebsResponse().success();
}
}
`@Trace` 自定义注解
/**
* OpenTelemetry 追踪注解
* 用于标记需要进行链路追踪的方法
*
* 使用方式:
* 1. 在 Controller 方法上使用,追踪 HTTP 请求
* 2. 在 Service 方法上使用,追踪业务逻辑
* 3. 在 Mapper 方法上使用,追踪数据库查询(通常 SQL 会自动被拦截器追踪)
*
* @author yulei
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Trace {
/**
* Span 名称,如果不指定则使用方法名
*/
String value() default "";
/**
* Span 类型(可选)
* 例如:controller、service、dao 等
*/
String kind() default "";
}
2. Service 层监控
在 Service 方法上使用注解:
package com.webfunny.member.service.impl;
import com.webfunny.common.annotation.Trace;
import com.webfunny.member.entity.Member;
import com.webfunny.member.mapper.MemberMapper;
import com.webfunny.member.service.IMemberService;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MemberServiceImpl implements IMemberService {
@Autowired
private MemberMapper memberMapper;
/**
* 使用 @Trace 注解监控 Service 方法
* 会记录方法执行时间、参数等
*/
@Override
@Trace(value = "findMemberById", kind = "service")
public Member findById(Long id) {
// 业务逻辑
Member member = memberMapper.selectById(id);
// 可以添加额外的业务处理
if (member != null) {
// 处理逻辑
}
return member;
}
/**
* 使用 @WithSpan 注解
*/
@Override
@WithSpan("saveMemberService")
public void save(Member member) {
memberMapper.insert(member);
}
/**
* 复杂业务场景,多个数据库操作
*/
@Override
@Trace(value = "updateMemberWithOrders", kind = "service")
public void updateMemberWithOrders(Long memberId, List<Order> orders) {
// 更新会员信息
Member member = memberMapper.selectById(memberId);
member.setUpdateTime(new Date());
memberMapper.updateById(member);
// 更新订单信息(这些 SQL 也会被自动追踪)
for (Order order : orders) {
orderMapper.updateById(order);
}
}
}
3. DAO/Mapper 层监控
**重要:** Mapper 层的 SQL 会被 `SqlTraceInterceptor` 自动拦截和追踪,**不需要手动添加注解**。
SqlTraceInterceptor:
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
/**
* MyBatis SQL 追踪拦截器
* 用于捕获 SQL 执行信息并上报到 OpenTelemetry
*
* @author yulei
*/
@Slf4j
@Component
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlTraceInterceptor implements Interceptor {
private static final Tracer tracer = GlobalOpenTelemetry.getTracer("mybatis-sql-tracer");
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 获取 MappedStatement
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
String sqlId = mappedStatement.getId();
// 获取 BoundSql
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
// 获取参数对象
Object parameterObject = boundSql.getParameterObject();
// 创建 span
Span span = tracer.spanBuilder("sql.query")
.setAttribute("db.system", "mysql")
.setAttribute("db.operation", getSqlOperation(sql))
.setAttribute("db.statement", formatSql(sql))
.setAttribute("db.sql.id", sqlId)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 添加格式化后的 SQL(带参数)
String formattedSql = showSql(mappedStatement.getConfiguration(), boundSql);
span.setAttribute("db.statement.formatted", formattedSql);
long startTime = System.currentTimeMillis();
// 执行 SQL
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 记录执行时间
span.setAttribute("db.execution.time.ms", duration);
// 如果执行时间超过阈值,记录警告
if (duration > 1000) {
span.setAttribute("db.slow.query", true);
log.warn("慢查询检测 - 执行时间: {}ms, SQL: {}", duration, formattedSql);
}
span.setStatus(StatusCode.OK);
return result;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, "SQL execution failed: " + e.getMessage());
throw e;
} finally {
span.end();
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以从配置文件读取配置
}
/**
* 获取 SQL 操作类型
*/
private String getSqlOperation(String sql) {
if (sql == null || sql.isEmpty()) {
return "UNKNOWN";
}
String upperSql = sql.trim().toUpperCase();
if (upperSql.startsWith("SELECT")) {
return "SELECT";
} else if (upperSql.startsWith("INSERT")) {
return "INSERT";
} else if (upperSql.startsWith("UPDATE")) {
return "UPDATE";
} else if (upperSql.startsWith("DELETE")) {
return "DELETE";
}
return "UNKNOWN";
}
/**
* 格式化 SQL(移除多余空白)
*/
private String formatSql(String sql) {
if (sql == null) {
return "";
}
return sql.replaceAll("\\s+", " ").trim();
}
/**
* 显示完整的 SQL(包含参数值)
*/
private String showSql(Configuration configuration, BoundSql boundSql) {
try {
Object parameterObject = boundSql.getParameterObject();
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
String sql = boundSql.getSql().replaceAll("\\s+", " ");
if (parameterMappings.size() > 0 && parameterObject != null) {
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
for (ParameterMapping parameterMapping : parameterMappings) {
String propertyName = parameterMapping.getProperty();
if (metaObject.hasGetter(propertyName)) {
Object obj = metaObject.getValue(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
} else if (boundSql.hasAdditionalParameter(propertyName)) {
Object obj = boundSql.getAdditionalParameter(propertyName);
sql = sql.replaceFirst("\\?", getParameterValue(obj));
}
}
}
}
return sql;
} catch (Exception e) {
log.error("格式化 SQL 失败", e);
return boundSql.getSql();
}
}
/**
* 获取参数值的字符串表示
*/
private String getParameterValue(Object obj) {
String value;
if (obj instanceof String) {
value = "'" + obj + "'";
} else if (obj instanceof Date) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
value = "'" + formatter.format(new Date()) + "'";
} else {
if (obj != null) {
value = obj.toString();
} else {
value = "null";
}
}
return value;
}
}
但如果有复杂的自定义方法,也可以添加注解:
package com.webfunny.member.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.webfunny.member.entity.Member;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface MemberMapper extends BaseMapper<Member> {
/**
* 普通方法,SQL 会被自动追踪,无需添加注解
*/
List<Member> findActiveMembers();
/**
* 复杂查询方法,可以添加注解以便更好地标识
*/
@WithSpan("findMembersByCondition")
List<Member> findByCondition(@Param("condition") MemberCondition condition);
}
三、启动脚本
去opentelemetry官网下载opentelemetry-javaagent探针
idea中添加add VM options:
-javaagent:/Users/yulei/Documents/package/opentelemetry/opentelemetry-javaagent.jar
-Dotel.service.name=webfunny-manage
-Dotel.resource.attributes=deployment.environment=webfunny_20251129_230724_sit,service.instance.id=webfunny_20251129_230724_sit,service.version=2.0
-Dotel.exporter.otlp.endpoint=http://xxx.webfunny.cn:4317
-Dotel.exporter.otlp.protocol=grpc
-Dotel.exporter.otlp.timeout=10s
-Dotel.traces.exporter=otlp
-Dotel.metrics.exporter=none
-Dotel.logs.exporter=none
-Dotel.javaagent.logging.level=INFO
-Dotel.instrumentation.jvm-metrics.enabled=false
-Dotel.instrumentation.system-metrics.enabled=false
-Dotel.instrumentation.jdbc.enabled=true
-Dotel.instrumentation.spring-webmvc.enabled=true
-Dotel.instrumentation.http-client.enabled=true
-Dotel.instrumentation.spring-webmvc.capture-request-parameters=true
-Dotel.instrumentation.jdbc.statement-sanitizer.enabled=true
应用脚本:
#!/bin/bash
# 应用名称
prog="webfunny-manage"
# OpenTelemetry 配置
OTEL_SERVICE_NAME="webfunny-manage-生产环境"
OTEL_RESOURCE_ATTRIBUTES="deployment.environment=webfunny_20251129_230724_pro,service.instance.id=webfunny_20251129_230724_pro"
OTEL_EXPORTER_OTLP_ENDPOINT="http://xxx.webfunny.cn:4317"
OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
OTEL_EXPORTER_OTLP_TIMEOUT="10s"
OTEL_TRACES_EXPORTER="otlp"
OTEL_METRICS_EXPORTER="none"
OTEL_LOGS_EXPORTER="otlp"
OTEL_JAVAAGENT_LOGGING_LEVEL="INFO"
OTEL_INSTRUMENTATION_JVM_METRICS_ENABLED="true"
OTEL_INSTRUMENTATION_SYSTEM_METRICS_ENABLED="true"
OTEL_INSTRUMENTATION_JDBC_ENABLED="true"
OTEL_INSTRUMENTATION_SPRING_WEBMVC_ENABLED="true"
OTEL_INSTRUMENTATION_HTTP_CLIENT_ENABLED="true"
OTEL_INSTRUMENTATION_SPRING_WEBMVC_CAPTURE_REQUESR_PARAMETERS="true"
OTEL_INSTRUMENTATION_JDBC_STATEMENT_SANITIZER_ENABLED="true"
# Java 代理路径
OTEL_AGENT_JAR="/home/yulei/manage/opentelemetry-agent/opentelemetry-javaagent.jar"
# 执行启动命令
exec java \
-javaagent:"$OTEL_AGENT_JAR" \
-Dotel.service.name="$OTEL_SERVICE_NAME" \
-Dotel.resource.attributes="$OTEL_RESOURCE_ATTRIBUTES" \
-Dotel.exporter.otlp.endpoint="$OTEL_EXPORTER_OTLP_ENDPOINT" \
-Dotel.exporter.otlp.protocol="$OTEL_EXPORTER_OTLP_PROTOCOL" \
-Dotel.exporter.otlp.timeout="$OTEL_EXPORTER_OTLP_TIMEOUT" \
-Dotel.traces.exporter="$OTEL_TRACES_EXPORTER" \
-Dotel.metrics.exporter="$OTEL_METRICS_EXPORTER" \
-Dotel.logs.exporter="$OTEL_LOGS_EXPORTER" \
-Dotel.javaagent.logging.level="$OTEL_JAVAAGENT_LOGGING_LEVEL" \
-Dotel.instrumentation.jvm-metrics.enabled="$OTEL_INSTRUMENTATION_JVM_METRICS_ENABLED" \
-Dotel.instrumentation.system-metrics.enabled="$OTEL_INSTRUMENTATION_SYSTEM_METRICS_ENABLED" \
-Dotel.instrumentation.jdbc.enabled="$OTEL_INSTRUMENTATION_JDBC_ENABLED" \
-Dotel.instrumentation.spring-webmvc.enabled="$OTEL_INSTRUMENTATION_SPRING_WEBMVC_ENABLED" \
-Dotel.instrumentation.http-client.enabled="$OTEL_INSTRUMENTATION_HTTP_CLIENT_ENABLED" \
-Dotel.instrumentation.spring-webmvc.capture-request-parameters="$OTEL_INSTRUMENTATION_SPRING_WEBMVC_CAPTURE_REQUESR_PARAMETERS" \
-Dotel.instrumentation.jdbc.statement-sanitizer.enabled="$OTEL_INSTRUMENTATION_JDBC_STATEMENT_SANITIZER_ENABLED" \
-jar -Xms256M -Xmx256M \
webfunny_manage-2.0.jar >/dev/null 2>&1 &
四、监控信息采集
自动采集的信息
1. HTTP 请求信息(Controller 层)
- `http.method`: HTTP 方法(GET、POST 等)
- `http.url`: 请求 URL
- `http.client_ip`: 客户端 IP
- `http.user_agent`: User-Agent
- `method.name`: Java 方法名
- `class.name`: Java 类名
- `execution.time.ms`: 方法执行时间(毫秒)
2. SQL 执行信息(DAO 层)
- `db.system`: 数据库类型(mysql)
- `db.operation`: SQL 操作类型(SELECT、INSERT、UPDATE、DELETE)
- `db.statement`: 原始 SQL 语句
- `db.statement.formatted`: 格式化的 SQL(包含参数值)
- `db.sql.id`: MyBatis Mapper 方法全限定名
- `db.execution.time.ms`: SQL 执行时间(毫秒)
- `db.slow.query`: 是否为慢查询(执行时间 > 1000ms)
3. 方法调用信息(Service 层)
- `method.name`: 方法名
- `class.name`: 类名
- `span.kind`: Span 类型(service、controller 等)
- `execution.time.ms`: 执行时间
- `arg.0, arg.1, ...`: 方法参数(仅基本类型)
4. 异常信息(所有层)
- 异常堆栈信息
- 异常消息
- Span 状态设置为 ERROR
生成的追踪链路结构
Trace ID: 1234567890abcdef
├─ Span 1: Controller.getMemberDetail (150ms)
│ ├─ http.method: GET
│ ├─ http.url: http://localhost:8080/member/123
│ └─ http.client_ip: 192.168.1.100
│
│ └─ Span 2: Service.findMemberWithOrders (140ms)
│ ├─ method.name: findMemberWithOrders
│ └─ span.kind: service
│
│ ├─ Span 3: sql.query (50ms)
│ │ ├─ db.operation: SELECT
│ │ ├─ db.statement: SELECT * FROM member WHERE id = ?
│ │ └─ db.statement.formatted: SELECT * FROM member WHERE id = 123
│ │
│ └─ Span 4: sql.query (80ms)
│ ├─ db.operation: SELECT
│ ├─ db.statement: SELECT * FROM orders WHERE member_id = ?
│ └─ db.statement.formatted: SELECT * FROM orders WHERE member_id = 123
展开全部链路如下:
错误异常上报:
空指针等异常详细信息:
## 注意事项
1. **性能影响**:链路追踪会有轻微的性能开销(一般 < 5%),生产环境建议配置采样率
2. **SQL 敏感信息**:SQL 参数可能包含敏感信息,注意数据安全
3. **注解使用**:
- Controller 和 Service 层建议使用 `@Trace` 或 `@WithSpan`
- Mapper 层 SQL 会自动追踪,通常不需要额外注解
4. **异常处理**:所有异常都会被自动记录到 Span 中
五、常见问题
### Q1: 为什么看不到追踪数据?
A: 检查以下几点:
- OpenTelemetry 配置是否正确
- OTLP 接收器(Jaeger/Tempo)是否正常运行
- 应用配置 `opentelemetry.enabled` 是否为 `true`
### Q2: SQL 信息没有被采集?
A: 确认 `SqlTraceInterceptor` 已注册为 Spring Bean,检查 `MybatisPlusConfigure` 配置
六、总结
通过 OpenTelemetry 集成,项目可以实现:
1. Controller 层自动采集 HTTP 请求信息
2. Service 层自动追踪业务方法调用
3. DAO 层自动采集 SQL 执行信息和参数
4. 完整的调用链路追踪
5. 慢查询自动检测
6. 异常信息自动记录
只需在关键方法上添加 `@Trace` 或 `@WithSpan` 注解,即可实现全链路监控!