前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Java 应用通过 OpenTelemetry API 实现手动埋点

Java 应用通过 OpenTelemetry API 实现手动埋点

作者头像
我是阳明
发布于 2023-09-11 11:47:10
发布于 2023-09-11 11:47:10
1.3K00
代码可运行
举报
文章被收录于专栏:k8s技术圈k8s技术圈
运行总次数:0
代码可运行

我们知道对于 Java 应用可以通过 OpenTelemetry 提供的 Java agent 来实现自动埋点功能,在大多数场景下也完全足够了,但是有时候我们需要更加精细的控制,这时候我们就需要使用手动埋点的方式来实现了。

使用注解埋点

我们可以在 Java 应用通过手动埋点的方式来实现链路追踪,但如果我们不希望进行太多的代码更改,那么可以使用注解的方式来实现,OpenTelemetry 提供了一些注解来帮助我们实现手动埋点,比如 @WithSpan@SpanAttribute

首先我们需要添加依赖库 opentelemetry-instrumentation-annotations

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<dependencies>
  <dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-instrumentation-annotations</artifactId>
    <version>1.29.0</version>
  </dependency>
</dependencies>

开发人员可以使用 @WithSpan 注解来向 OpenTelemetry 自动检测发送信号,每当标记的方法被执行时都应创建一个新的 span。

比如我们在 Order Service 中的 IndexController 中添加一个 @WithSpan 注解,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/main/java/com/youdianzhishi/orderservice/controller/IndexController.java
package com.youdianzhishi.orderservice.controller;

// ......

import io.opentelemetry.instrumentation.annotations.WithSpan;


@RestController
@RequestMapping("/")
public class IndexController {
    @GetMapping
    @WithSpan
    public ResponseEntity<String> home(HttpServletRequest request) {
        return new ResponseEntity<>("Hello OpenTelemetry!", HttpStatus.OK);
    }
}

然后我们重建镜像,重新启动容器,当我们访问首页的时候就可以看到 Jaeger UI 中多了一个 IndexController.home 的 span 了。

每次应用程序调用有注解的方法时,它都会创建一个表示其持续时间并提供任何抛出异常的 span。默认情况下,span 名称是 <className>.<methodName>,当然也可以在注解中提供了一个名称作为参数,比如可以使用 @WithSpan("indexSpan") 来指定 span 的名称,这样在 Jaeger UI 中就可以看到 indexSpan 的 span 了。

此外当为一个带注解的方法创建一个 span 时,可以通过使用 @SpanAttribute 注解来自动将方法调用的参数值添加为创建 span 的属性。

比如我们在 IndexController 中添加一个 fetchId 函数,并接收一个 id 参数,我们就可以使用 @SpanAttribute 注解来将接收的 id 参数添加为 indexSpanWithAttr 这个 span 的属性,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/main/java/com/youdianzhishi/orderservice/controller/IndexController.java
package com.youdianzhishi.orderservice.controller;

// ......

import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.instrumentation.annotations.SpanAttribute;


@RestController
@RequestMapping("/")
public class IndexController {
    @GetMapping
    @WithSpan("indexSpan")
    public ResponseEntity<String> home(HttpServletRequest request) {
        return new ResponseEntity<>("Hello OpenTelemetry!", HttpStatus.OK);
    }

    @GetMapping("/{id}")
    @WithSpan("indexSpanWithAttr")
    public ResponseEntity<String> fetchId(@SpanAttribute("id") @PathVariable Long id) {
        return new ResponseEntity<>("Hello OpenTelemetry:" + id, HttpStatus.OK);
    }
}

然后我们重建镜像,重新启动容器,当我们访问 http://localhost:8081/123 的时候就可以看到 Jaeger UI 中多了一个 indexSpanWithAttr 的 span 了,并且该 span 的属性中包含了我们传递的 id 参数。

使用 API 手动埋点

除了使用注解的方式来实现埋点之外,我们还可以使用 OpenTelemetry 提供的 API 来实现手动埋点,这样我们就可以更加精细的控制我们的 span 了,当然这样也会增加我们的代码量,但就不需要使用 java agent 了。

在 Java 应用中,要实现手动埋点,首先第一步是获取 OpenTelemetry 接口的实例,我们需要尽早在应用程序中配置一个 OpenTelemetrySdk 的实例,我们可以使用 OpenTelemetrySdk.builder() 方法来完成这个操作。然后可以通过返回的 OpenTelemetrySdkBuilder 实例获取与信号、跟踪和指标相关的提供程序,以构建 OpenTelemetry 实例。我们可以使用 SdkTracerProvider.builder()SdkMeterProvider.builder() 方法来构建 Provider。此外还强烈建议将 Resource 实例定义为生成遥测数据的实体的表示;特别是 service.name 属性是最重要的遥测源标识信息的一部分。

当然我们需要先在应用中添加相关依赖库,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!-- pom.xml -->
<project>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.opentelemetry</groupId>
                <artifactId>opentelemetry-bom</artifactId>
                <version>1.29.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-exporter-otlp</artifactId>
        </dependency>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-semconv</artifactId>
            <version>1.29.0-alpha</version>
        </dependency>
    </dependencies>
</project>

pom.xml 文件中添加了 opentelemetry-apiopentelemetry-sdkopentelemetry-exporter-otlpopentelemetry-semconv 这几个依赖库,其中 opentelemetry-semconv 是用来定义一些常用的属性的,比如 service.namehttp.methodhttp.status_code 等,当然现在我们就不需要 opentelemetry-instrumentation-annotations 这个依赖库了。

Spring Boot 项目中,初始化 OpenTelemetry 的一种常见方法是使用 @Configuration 类。这样的类会在 Spring Boot 应用启动时自动运行,使得初始化工作更加集中和组织化。

我们这里创建一个如下所示的 OpenTelemetryConfig 类,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/main/java/com/youdianzhishi/orderservice/config/OpenTelemetryConfig.java
package com.youdianzhishi.orderservice.config;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

@Configuration
@Order(2)
public class OpenTelemetryConfig {

    @Bean
    public OpenTelemetry openTelemetry() {
        GlobalOpenTelemetry.resetForTest(); // 初始化之前先重置 GlobalOpenTelemetry

        // 从环境变量中获取 OTLP Exporter 的地址
        String exporterEndpointFromEnv = System.getenv("OTLP_EXPORTER_ENDPOINT");
        String exporterEndpoint = exporterEndpointFromEnv != null ? exporterEndpointFromEnv
                : "http://otel-collector:4317";

        String serviceNameFromEnv = System.getenv("SERVICE_NAME");
        String serviceName = serviceNameFromEnv != null ? serviceNameFromEnv : "order-service";

        // 初始化 OTLP Exporter
        OtlpGrpcSpanExporter exporter = OtlpGrpcSpanExporter.builder()
                .setEndpoint(exporterEndpoint)
                .build();

        Resource resource = Resource.getDefault()
                .merge(Resource.create(Attributes.of(
                        ResourceAttributes.SERVICE_NAME, serviceName,
                        ResourceAttributes.TELEMETRY_SDK_LANGUAGE, "java")));

        // 初始化 TracerProvider
        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
                .addSpanProcessor(SimpleSpanProcessor.create(exporter))
                .setResource(resource)
                .build();

        // 初始化 ContextPropagators,这里我们配置包含 W3C Trace Context 和 W3C Baggage
        ContextPropagators propagators = ContextPropagators.create(
                TextMapPropagator.composite(
                        W3CTraceContextPropagator.getInstance(),
                        W3CBaggagePropagator.getInstance()));

        // 初始化并返回 OpenTelemetry SDK
        return OpenTelemetrySdk.builder()
                .setPropagators(propagators)
                .setTracerProvider(tracerProvider)
                .buildAndRegisterGlobal();
    }

    @Bean
    public Tracer tracer() {
        return openTelemetry().getTracer(OrderserviceApplication.class.getName());
    }
}

在上述代码中,我们定义了一个 @Configuration 类,并使用 @Bean 注解为 OpenTelemetry 创建了一个 Bean,Spring 会管理这个 Bean 的生命周期,并在需要时自动注入。

这样,你的 Spring Boot 应用每次启动时,都会执行这些初始化代码,从而确保了 OpenTelemetry 的正确配置。

在真正初始化的代码中,我们首先从环境变量中获取 OTLP Exporter 的地址,然后初始化 OTLP Exporter,接着初始化 TracerProvider,最后初始化并返回 OpenTelemetry SDK

比如现在我们在 OrderController 中的 getAllOrders 处理器中来手动埋点,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/main/java/com/youdianzhishi/orderservice/controller/OrderController.java
package com.youdianzhishi.orderservice.controller;

// ......

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

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private static final Logger logger = LoggerFactory.getLogger(OrderserviceApplication.class);

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private WebClient webClient;

    @Autowired
    private Tracer tracer;  // 注入 Tracer

    @GetMapping
    public ResponseEntity<List<OrderDto>> getAllOrders(HttpServletRequest request) {
        // 创建一个新的 Span 并设置 Span 名称为 "GET /api/orders"
        var span = tracer.spanBuilder("GET /api/orders").startSpan();

        // 将 Span 注入到上下文中
        try (var scope = span.makeCurrent()) {
            // 从拦截器中获取用户信息
            User user = (User) request.getAttribute("user");

            // 要根据 orderDate 倒序排列
            List<Order> orders = orderRepository.findByUserIdOrderByOrderDateDesc(user.getId());

            // 将Order转换为OrderDto
            List<OrderDto> orderDtos = orders.stream().map(order -> {
                try {
                    return order.toOrderDto(webClient);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }).collect(Collectors.toList());

            span.setAttribute("user_id", user.getId());
            span.setAttribute("order_count", orders.size());

            return new ResponseEntity<>(orderDtos, HttpStatus.OK);
        } catch (Exception e) {
            // 记录 Span 错误
            span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        } finally {
            // 记录 Span 结束时间
            span.end();
        }
    }

    // 忽略其他......
}

上面代码中我们首先通过 openTelemetry.getTracer(OrderController.class.getName()) 方法来初始化 Tracer,然后通过 tracer.spanBuilder("getAllOrders").startSpan() 方法来创建一个新的 Span,接着通过 span.makeCurrent() 方法将 Span 注入到上下文中,然后就可以在 try 代码块中执行我们的业务逻辑了,这里我们添加了两个属性,如果出现了异常则会记录异常信息,最后在 finally 代码块中结束 Span。

我们还需要修改 Dockerfile 中的启动命令,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# ......
# CMD ["mvn", "-Pdev", "spring-boot:run"]
CMD ["mvn", "spring-boot:run"]

因为现在我们不需要使用 java agent 了,所以去掉 -Pdev 参数(该 profile 中定义了 java agent 启动参数),然后重新构建镜像,重新启动容器,当我们访问订单列表后就可以看到 Jaeger UI 中多了一个 getAllOrders 的 span 了。

很明显我们可以看到现在的 span 非常简单,没有和前端 frontend 服务的 span 关联起来。

由于前端 frontend 在请求后端接口的时候我们已经注入了 W3CTraceContext,所以我们只需要在 Java 应用中通过 propagation api 来获取到 span context,然后将其作为父级 span,这样就可以将前端的 span 和后端的 span 关联起来了。

这里我们可以添加一个拦截器来使用 propagation 接口解析 span context,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/main/java/com/youdianzhishi/orderservice/interceptor/OpenTelemetryInterceptor.java
package com.youdianzhishi.orderservice.interceptor;

// ......

@Component
public class OpenTelemetryInterceptor implements HandlerInterceptor {
    @Autowired
    private OpenTelemetry openTelemetry;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        TextMapGetter<HttpServletRequest> getter = new TextMapGetter<>() {
            @Override
            public Iterable<String> keys(HttpServletRequest carrier) {
                return Collections.list(carrier.getHeaderNames());
            }

            @Override
            public String get(HttpServletRequest carrier, String key) {
                return carrier.getHeader(key);
            }
        };

        // 提取传入的Trace Context
        Context extractedContext = openTelemetry.getPropagators().getTextMapPropagator()
                                       .extract(Context.current(), request, getter);

        StringBuilder sb = new StringBuilder();
        sb.append(request.getMethod()).append(" ").append(request.getRequestURI());
        Span span = tracer.spanBuilder(sb.toString()).setParent(extractedContext)
                    .startSpan();

        // 将解析出来的SpanContext存储在请求属性中,以便后续使用
        request.setAttribute("currentSpan", span);

        return true;
    }
}

上面代码中我们首先通过 openTelemetry.getPropagators().getTextMapPropagator() 方法来获取到 TextMapPropagator,然后通过 extract 方法来解析 span context,然后将解析出来的 span context 设置为子 span 的父级 span,最后将 span context 存储在请求属性中,以便后续使用。

这里的关键是在初始化 OpenTelemetry 的时候需要配置 ContextPropagators,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 初始化 ContextPropagators,这里我们配置包含 W3C Trace Context 和 W3C Baggage
ContextPropagators propagators = ContextPropagators.create(
        TextMapPropagator.composite(
                W3CTraceContextPropagator.getInstance(),
                W3CBaggagePropagator.getInstance()));

这样我们才能去解析 TraceContext 和 Baggage 两种上下文传播机制。而其中的 getter 就是用来从 HTTP 请求头中获取 span context 的方式。

当然最后我们还需要在 WebMvcConfig 中注册该拦截器,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/main/java/com/youdianzhishi/orderservice/config/WebMvcConfig.java
package com.youdianzhishi.orderservice.config;

// ......

@Configuration
@Order(4)
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Autowired
    private OpenTelemetryInterceptor otelCtxInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(otelCtxInterceptor)
            .addPathPatterns("/api/orders/**");

        registry.addInterceptor(tokenInterceptor)
            .addPathPatterns("/api/orders/**") // 指定拦截器应该应用的路径模式
            .excludePathPatterns("/api/login", "/api/register"); // 指定应该排除的路径模式
    }

}

这样当我们在请求 /api/orders/** 下面的接口时,就可以从请求属性中获取父级的 span context 了。

现在我们重新修改 getAllOrders 处理器,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@GetMapping
public ResponseEntity<List<OrderDto>> getAllOrders(HttpServletRequest request) {
    // 从请求属性中获取 Span
    Span span = (Span) request.getAttribute("currentSpan");

    try {
        // 从拦截器中获取用户信息
        User user = (User) request.getAttribute("user");

        // 要根据 orderDate 倒序排列
        List<Order> orders = orderRepository.findByUserIdOrderByOrderDateDesc(user.getId());

        // 将Order转换为OrderDto
        List<OrderDto> orderDtos = orders.stream().map(order -> {
            try {
                return order.toOrderDto(webClient);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }).collect(Collectors.toList());

        span.setAttribute("user_id", user.getId());
        span.setAttribute("order_count", orders.size());

        return new ResponseEntity<>(orderDtos, HttpStatus.OK);
    } catch (Exception e) {
        // 记录 Span 错误
        span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
        return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    } finally {
        // 记录 Span 结束时间
        span.end();
    }

}

这里我们首先通过请求属性获取到 span context,这里我们添加了两个属性,如果出现了异常则会记录异常信息,最后在 finally 代码块中结束 Span。

现在我们重新启动容器,当我们访问订单列表后就可以看到 Jaeger UI 中多了一个 GET /api/orders 的 span 了,并且该 span 和前端 frontend 服务的 span 关联起来了。

当然这还不够,因为我们的订单列表接口还会去请求 user-service 服务来获取用户信息,还会去请求 catalog-service 服务获取书籍信息,所以我们还需要在这两个请求中也注入我们这里的 span,这样就可以将整个链路串联起来了。

首先针对 TokenInterceptor 拦截器我们先创建一个子 span,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/main/java/com/youdianzhishi/orderservice/interceptor/TokenInterceptor.java
package com.youdianzhishi.orderservice.interceptor;

// ......

@Component
public class TokenInterceptor implements HandlerInterceptor {

    @Autowired
    private WebClient webClient;

    @Autowired
    private Tracer tracer;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 先获取 Span
        Span currentSpan = (Span) request.getAttribute("currentSpan");
        Context context = Context.current().with(currentSpan);

        // 创建新的 Span,作为子 Span
        Span span = tracer.spanBuilder("GET /api/userinfo")
            .setParent(context).startSpan();

        // 将子 Span 设置为当前上下文,相当于切换上下文到子 Span
        try (Scope scope = span.makeCurrent()) {

            try {
                String token = request.getHeader("Authorization");
                if (token == null) {
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    span.addEvent("Token is null").setStatus(StatusCode.ERROR);
                    return false;
                }
                // 从环境变量中获取 userServiceUrl
                String userServiceEnv = System.getenv("USER_SERVICE_URL");
                String userServiceUrl = userServiceEnv != null ? userServiceEnv : "http://localhost:8080";
                User user = webClient.get()
                        .uri(userServiceUrl + "/api/userinfo")
                        .header(HttpHeaders.AUTHORIZATION, token)
                        .retrieve()
                        .onStatus(httpStatus -> httpStatus.equals(HttpStatus.UNAUTHORIZED),
                                clientResponse -> Mono.error(new RuntimeException("Unauthorized")))
                        .onStatus(
                                httpStatus -> httpStatus.is4xxClientError()
                                        && !httpStatus.equals(HttpStatus.UNAUTHORIZED),
                                clientResponse -> Mono.error(new RuntimeException("Other Client Error")))
                        .bodyToMono(User.class)
                        .block();
                if (user != null) {
                    request.setAttribute("user", user);
                    span.setAttribute("user_id", user.getId());
                    return true;
                } else {
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    span.addEvent("User is null").setStatus(StatusCode.ERROR);
                    return false;
                }
            } catch (RuntimeException e) {
                span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
                if (e.getMessage().equals("Unauthorized")) {
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                } else {
                    response.setStatus(HttpStatus.BAD_REQUEST.value());
                }
                return false;
            } catch (Exception e) {
                span.recordException(e).setStatus(StatusCode.ERROR, e.getMessage());
                response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                return false;
            } finally {
                request.setAttribute("parentSpan", span);
                span.end();
            }
        }

    }
}

在上面代码中我们首先获取当前上下文的 Span,然后创建一个名为 GET /api/userinfo 的 span,将其设置为当前上下文的子 span,并将上下文切换到当前子 span,然后执行我们的业务逻辑,最后结束子 span。

然后我们可以统一在 WebClient 中来注入 span context,这样当我们 Java 服务请求其他服务的时候就可以形成链路。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/main/java/com/youdianzhishi/orderservice/config/WebClientConfig.java
package com.youdianzhishi.orderservice.config;

// ......

@Configuration
@Order(3)
public class WebClientConfig {
    @Autowired
    private OpenTelemetry openTelemetry;

    @Bean
    public WebClient webClient() {
        return WebClient.builder().filter(traceExchangeFilterFunction()).build();
    }

    @Bean
    public ExchangeFilterFunction traceExchangeFilterFunction() {
        return (clientRequest, next) -> {
            // 获取当前上下文的 Span
            Span currentSpan = Span.current();
            Context context = Context.current().with(currentSpan);

            // 创建新的请求头并添加跟踪信息
            HttpHeaders newHeaders = new HttpHeaders();
            newHeaders.putAll(clientRequest.headers());

            TextMapSetter<HttpHeaders> setter = new TextMapSetter<HttpHeaders>() {
                @Override
                public void set(HttpHeaders carrier, String key, String value) {
                    carrier.add(key, value);
                }
            };

            // 将当前上下文的 Span 注入到请求头中
            openTelemetry.getPropagators().getTextMapPropagator().inject(context, newHeaders, setter);

            // 创建一个新的 ClientRequest 对象
            ClientRequest newRequest = ClientRequest.from(clientRequest)
                    .headers(headers -> headers.addAll(newHeaders))
                    .build();

            return next.exchange(newRequest);
        };
    }
}

在上面代码中我们为 WebClient 添加了一个名为 traceExchangeFilterFunction 的过滤器函数,在该函数中我们首先获取当前上下文的 Span,然后创建一个新的请求头并添加跟踪信息,最后将当前上下文的 Span 通过 Propagator 接口注入到请求头中,这样当我们请求其他服务的时候就可以形成链路了。

现在我们重新启动容器,当我们访问订单列表后就可以看到 Jaeger UI 中多了一个 GET /api/userinfo 的 span 了,并且该 span 和还会和 user-service 服务的 span 关联起来。

同样的方式我们还可以在 getAllOrders 处理器中添加数据库查询的 span,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 新建一个 DB 查询的 span
Span dbSpan = tracer.spanBuilder("DB findByUserIdOrderByOrderDateDesc").setParent(context).startSpan();
// 要根据 orderDate 倒序排列
List<Order> orders = orderRepository.findByUserIdOrderByOrderDateDesc(user.getId());
dbSpan.addEvent("OrderRepository findByUserIdOrderByOrderDateDesc From DB");
dbSpan.setAttribute("order_count", orders.size());
dbSpan.end();

将 Order 转换为 OrderDto 也可以添加一个 span,代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/main/java/com/youdianzhishi/orderservice/model/Order.java
package com.youdianzhishi.orderservice.model;

// ......

public OrderDto toOrderDto(WebClient webClient, Tracer tracer, Context context) throws Exception {
    // 创建新的 Span,作为子 Span
    Span span = tracer.spanBuilder("GET /api/books/batch").setParent(context).startSpan();

    try (Scope scope = span.makeCurrent()) { // 切换上下文到子 Span

        span.setAttribute("order_id", this.getId());
        span.setAttribute("status", this.getStatus());

        OrderDto orderDto = new OrderDto();
        orderDto.setId(this.getId());
        orderDto.setStatus(this.getStatus());
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strDate = formatter.format(this.getOrderDate());
        orderDto.setOrderDate(strDate);

        List<Integer> bookIds = this.getBookIds(); // 假设你有一个可以获取书籍ID的方法
        // 将 bookIds 转换为字符串,以便于传递给 WebClient
        String bookIdsStr = bookIds.stream().map(String::valueOf).collect(Collectors.joining(","));
        span.addEvent("get book ids");
        span.setAttribute("book_ids", bookIdsStr);

        // 用 WebClient 调用批量查询书籍的服务接口
        // 从环境变量中获取 bookServiceUrl
        String catalogServiceEnv = System.getenv("CATALOG_SERVICE_URL");
        String catalogServiceUrl = catalogServiceEnv != null ? catalogServiceEnv : "http://localhost:8082";
        Mono<List<BookDto>> booksMono = webClient.get() // 假设你有一个webClient实例
                .uri(catalogServiceUrl + "/api/books/batch?ids=" + bookIdsStr)
                .retrieve()
                .bodyToMono(new ParameterizedTypeReference<>() {
                });
        List<BookDto> books = booksMono.block();

        span.addEvent("get books info from catalog service");

        // 还需要将书籍数量和总价填充到 OrderDto 对象中
        int totalAmount = 0;
        int totalCount = 0;
        List<BookQuantity> bqs = this.getBookQuantities();
        for (BookDto book : books) {
            // 如果 book.id 在 bqs 中,那么就将对应的数量设置到 book.quantity 中
            int quantity = bqs.stream().filter(bq -> bq.getId() == book.getId()).findFirst().get().getQuantity();
            book.setQuantity(quantity);
            totalCount += quantity;
            totalAmount += book.getPrice() * quantity;
        }

        orderDto.setBooks(books);
        orderDto.setAmount(totalAmount);
        orderDto.setTotal(totalCount);

        span.addEvent("calculate total amount and total count");

        span.end();

        return orderDto;
    }
}

这里同样我们会为每一个转换创建一个子 span,然后将其设置为当前上下文的子 span,最后结束子 span,这样当我们通过 WebClient 去请求 catalog-service 服务的时候也就可以形成链路了。

最后我们再去查看下完整的链路,如下图所示:

完整代码请查看:https://github.com/cnych/podemo。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-09-04 17:07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 k8s技术圈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Opentelemetry——Instrumentation-Libraries
Learn how to add native instrumentation to your library. 了解如何将本地测量装置添加到您的库中。
方亮
2024/05/24
1070
Opentelemetry——Instrumentation-Libraries
OpenTelemetry 实战:从零实现分布式链路追踪
之前写过一篇 从 Dapper 到 OpenTelemetry:分布式追踪的演进之旅的文章,主要是从概念上讲解了 Trace 在 OpenTelemetry 的中的场景和使用。
crossoverJie
2024/09/29
2370
Spring Boot上的OpenTelemetry追踪:Java Agent VS Micrometer Tracing
我的 OpenTelemetry 追踪演示包含两个 Spring Boot 组件。其中一个使用 Java 代理,我最近将它从 v1.x 升级到 v2.x 后,注意到了一些不同的行为。在另一个组件中,我使用 Micrometer Tracing,因为我编译到 GraalVM 原生,它无法处理 Java 代理。
云云众生s
2024/08/07
6630
Spring Boot上的OpenTelemetry追踪:Java Agent VS Micrometer Tracing
OpenTelemetry 深度定制:跨服务追踪的实战技巧
在上一篇《从 Dapper 到 OpenTelemetry:分布式追踪的演进之旅》中在最后提到在做一些 Trace 的定制开发。
crossoverJie
2024/06/27
4110
Language APIs & SDKs-C++-Exporters
Send telemetry to the OpenTelemetry Collector to make sure it’s exported correctly. Using the Collector in production environments is a best practice. To visualize your telemetry, export it to a backend such as Jaeger , Zipkin , Prometheus , or a vendor-specific backend. 将遥测数据发送到OpenTelemetry Collector以确保其导出正确。在生产环境中使用收集器是最佳实践。要可视化您的遥测数据,请将其导出到后端,例如 Jaeger、Zipkin、 Prometheus或特定于供应商的后端。
方亮
2024/05/24
720
在Jaeger和OpenTelemetry SDK混合环境中使用W3C Trace-Context
在本文中,我们将了解如何使用带有W3C Trace-Context传播格式的Jaeger客户端。标准化的上下文传播格式,确保了不同跟踪系统和工具库之间的互操作性。在这方面,我们将探讨两个用例。首先,如何在Jaeger测仪环境中使用OpenTelemetry SDK。这个场景模拟了不同跟踪系统之间的互操作性,也模拟了从Jaeger客户端到OpenTelemetry SDK的迁移路径。在第二个用例中,我们将配置Jaeger原生客户端以使用W3C Trace-Context。
CNCF
2020/05/29
2.4K0
在Jaeger和OpenTelemetry SDK混合环境中使用W3C Trace-Context
技术阅读周刊第十一期
URL: https://medium.com/@jojoooo/exploring-a-base-spring-boot-application-with-java-21-virtual-thread-spring-security-flyway-c0fde13c1eca#551c
crossoverJie
2023/12/26
1750
技术阅读周刊第十一期
使用opentelemetry与jaeger实现flask应用的链路追踪
链路追踪是应用可观测性的重要组成部分,它可以帮助我们快速定位问题,提高应用的可用性和稳定性。 今天我们来看看如何使用 opentelemetry 与 jaeger 实现 flask 应用的链路追踪。
杜逸先
2023/04/13
1.9K0
使用opentelemetry与jaeger实现flask应用的链路追踪
基于OpenTelemetry实现Java微服务调用链跟踪
随着业务的发展,所有的系统都会走向微服务化体系,微服务进行拆分后,服务的依赖关系变得复杂,如果出现了错误和异常,定位的过程将会变得复杂,一个请求可能需要调用很多个服务,所以微服务架构中,分布式链路跟踪的实现至关重要,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见。如何快速查询整个请求链路上的信息并呈现出来是解决排查问题复杂度的根本方法。
用户1107783
2024/04/11
1910
基于OpenTelemetry实现Java微服务调用链跟踪
PHP 中使用 opentelemetry-auto-laravel 进行链路追踪时间片段不连续的问题
seth-shi
2024/03/29
3460
PHP 中使用 opentelemetry-auto-laravel 进行链路追踪时间片段不连续的问题
Opentelemetry 实践分享 - Golang篇
Opentelemetry 是一个CNCF社区下一个开源的可观测性框架,或者也可以说是一组工具、API 和 SDK 的集合,来检测、生成、收集和导出可观测性数据(指标、日志和链路),以帮助我们分析软件的性能和行为。
云原生小白
2023/08/28
4K0
Opentelemetry 实践分享 - Golang篇
OpenTelemetry:Go可观测性指南
现代应用程序通常是复杂的分布式系统。调试它们可不是一件有趣的事情:你必须跟踪跨服务的请求,日志会丢失,而且指标通常难以关联。这就像大海捞针——只不过这个草堆正在燃烧,而且针还在移动。这就是 OpenTelemetry (OTel) 可以提供帮助的地方。
云云众生s
2025/02/09
1110
OpenTelemetry:Go可观测性指南
跨服务链路追踪:一次完整的 OpenTelemetry 使用实践
在现代的微服务架构中,一个用户请求可能需要跨多个服务才能完全处理。这样的复杂性可能会给诊断问题和性能优化带来挑战。因此,跨服务链路追踪技术的应用越来越受到关注。
运维开发王义杰
2023/08/10
2.5K0
跨服务链路追踪:一次完整的 OpenTelemetry 使用实践
OpenTelemetry 实战:从零实现应用指标监控
在上一篇文章:OpenTelemetry 实战:从零实现分布式链路追踪讲解了链路相关的实战,本次我们继续跟进如何使用 OpenTelemetry 集成 metrics 监控。
crossoverJie
2024/09/30
2320
使用OpenTelemetry对React应用程序进行插桩
了解如何在 React 应用程序中使用 OpenTelemetry,包括基本和自动插桩,以及添加自定义跨度和指标。
云云众生s
2024/09/21
1860
使用OpenTelemetry对React应用程序进行插桩
Opentelemetry SDK的简单用法
Opentelemetry trace的简单架构图如下,客户端和服务端都需要启动一个traceProvider,主要用于将trace数据传输到registry(如jaeger、opencensus等)。client和server通过context将整个链路串起来。
charlieroro
2022/08/31
1.9K0
Opentelemetry SDK的简单用法
Golang 接入 OpenTelemetry 完整过程和思路(附源码)
为了更方便的查看代码, 建议直接跳转到 Github 仓库:https://github.com/tangx/opentelemetry-gin-demo
老麦
2023/09/07
2.2K0
Golang 接入 OpenTelemetry 完整过程和思路(附源码)
Elastic APM 和 OpenTelemetry 集成
OpenTelemetry是一个云原生计算基金会 (CNCF) 沙盒项目,它提供了供应商中立的、特定于语言的代理、SDK 和 API,您可以使用它们从所有受监控的应用程序中收集分布式跟踪、指标和日志数据。OpenTelemetry 代理能够自动检测应用程序代码以显示性能数据,这些数据对于帮助您了解服务的健康状况非常重要——为应用程序开发人员提供了选择和选择哪些代理监控其应用程序的灵活性。
点火三周
2022/05/30
4K0
Elastic APM 和 OpenTelemetry 集成
Opentelemetry——Signals-Trace
Understand the full path through your distributed application. 了解分布式应用程序的完整路径。
方亮
2024/05/24
980
Spring Cloud技术栈包含哪些组件?
在当今互联网时代,微服务架构已成为构建高效、灵活、可扩展系统的首选方案。而Spring Cloud作为Java生态中最流行的微服务框架之一,为开发者提供了一整套解决方案,极大地简化了微服务的开发、部署和管理。本文将带你深入了解Spring Cloud技术栈,通过通俗易懂的语言和详细的案例说明,帮助你快速掌握这项强大的技术。
coderidea
2024/06/27
3450
Spring Cloud技术栈包含哪些组件?
相关推荐
Opentelemetry——Instrumentation-Libraries
更多 >
目录
  • 使用注解埋点
  • 使用 API 手动埋点
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文