精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Java 應用通過 OpenTelemetry API 實現手動埋點

開發 后端
我們可以在 Java 應用通過手動埋點的方式來實現鏈路追蹤,但如果我們不希望進行太多的代碼更改,那么可以使用注解的方式來實現,OpenTelemetry 提供了一些注解來幫助我們實現手動埋點,比如 @WithSpan、@SpanAttribute。

我們知道對于 Java 應用可以通過 OpenTelemetry 提供的 Java agent 來實現自動埋點功能,在大多數場景下也完全足夠了,但是有時候我們需要更加精細的控制,這時候我們就需要使用手動埋點的方式來實現了。

使用注解埋點

我們可以在 Java 應用通過手動埋點的方式來實現鏈路追蹤,但如果我們不希望進行太多的代碼更改,那么可以使用注解的方式來實現,OpenTelemetry 提供了一些注解來幫助我們實現手動埋點,比如 @WithSpan、@SpanAttribute。

首先我們需要添加依賴庫 opentelemetry-instrumentation-annotations。

<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 注解,代碼如下所示:

// 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 的屬性,代碼如下所示:

// 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 屬性是最重要的遙測源標識信息的一部分。

當然我們需要先在應用中添加相關依賴庫,代碼如下所示:

<!-- 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-api、opentelemetry-sdk、opentelemetry-exporter-otlp、opentelemetry-semconv 這幾個依賴庫,其中 opentelemetry-semconv 是用來定義一些常用的屬性的,比如 service.name、http.method、http.status_code 等,當然現在我們就不需要 opentelemetry-instrumentation-annotations 這個依賴庫了。

在 Spring Boot 項目中,初始化 OpenTelemetry 的一種常見方法是使用 @Configuration 類。這樣的類會在 Spring Boot 應用啟動時自動運行,使得初始化工作更加集中和組織化。

我們這里創建一個如下所示的 OpenTelemetryConfig 類,代碼如下所示:

// 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 處理器中來手動埋點,代碼如下所示:

// 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 中的啟動命令,代碼如下所示:

# ......
# 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,代碼如下所示:

// 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,代碼如下所示:

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

這樣我們才能去解析 TraceContext 和 Baggage 兩種上下文傳播機制。而其中的 getter 就是用來從 HTTP 請求頭中獲取 span context 的方式。

當然最后我們還需要在 WebMvcConfig 中注冊該攔截器,代碼如下所示:

// 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 處理器,代碼如下所示:

@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,代碼如下所示:

// 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 服務請求其他服務的時候就可以形成鏈路。

// 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,代碼如下所示:

// 新建一個 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,代碼如下所示:

// 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。

責任編輯:姜華 來源: k8s技術圈
相關推薦

2024-09-04 08:09:51

2023-12-13 18:46:50

FlutterAOP業務層

2024-08-21 08:09:17

2024-08-28 08:09:13

contextmetrics類型

2020-04-29 16:24:55

開發iOS技術

2017-12-28 14:54:04

Android代碼埋點全埋點

2022-08-31 07:54:08

采集sdk埋點數據

2025-02-17 07:45:29

2023-12-25 11:18:12

OpenTeleme應用日志Loki

2009-07-15 10:47:32

Java多態

2025-07-11 09:09:00

2023-04-19 09:05:44

2021-02-19 07:59:21

數據埋點數據分析大數據

2021-08-10 13:50:24

iOS

2023-01-10 09:08:53

埋點數據數據處理

2025-11-06 01:45:00

2025-07-03 03:20:00

2024-11-01 12:39:04

2021-08-31 19:14:38

技術埋點運營

2023-03-01 15:52:30

點贊
收藏

51CTO技術棧公眾號

人妻av无码一区二区三区| 99在线视频首页| 中文字幕一区二区三区人妻| 桃子视频成人app| 亚洲天堂av一区| 国产伦精品一区二区三区在线| 亚洲黄色小说图片| 久久福利综合| 亚洲精品福利在线观看| 激情内射人妻1区2区3区| 黄色在线视频网站| 97久久久精品综合88久久| 国产在线精品成人一区二区三区| 久久精品99国产精| 欧美亚洲国产激情| 亚洲的天堂在线中文字幕| www欧美激情| av在线播放资源| 国产精品短视频| 欧美日韩综合精品| 亚洲第一视频在线| 久久99精品国产麻豆婷婷洗澡| 91tv亚洲精品香蕉国产一区7ujn| 午夜剧场免费在线观看| 国产aⅴ精品一区二区三区久久| 日韩欧美在线网站| 久久婷五月综合| 国模冰冰炮一区二区| 亚洲国产欧美一区二区三区丁香婷| 亚洲v国产v| 日韩美女一级视频| 国产aⅴ精品一区二区三区色成熟| 国产精品网址在线| 国产嫩bbwbbw高潮| 亚洲青涩在线| 欧美激情第一页xxx| 99国产精品无码| 国产在线日韩精品| 亚洲色图第三页| 在线免费观看成年人视频| 成人av综合网| 欧美大片在线观看一区二区| 欧美性受xxxxxx黑人xyx性爽| 日本欧美韩国| 欧美中文字幕一区二区三区| 国产超级av在线| 天堂av中文在线观看| 亚洲午夜三级在线| www.在线观看av| 女人黄色免费在线观看| 伊人婷婷欧美激情| 麻豆传媒网站在线观看| 中文字幕伦理免费在线视频| 亚洲少妇最新在线视频| 手机福利在线视频| 国产精品第2页| 中文字幕第二区| 国产剧情一区| 国产性猛交xxxx免费看久久| 夜夜春很很躁夜夜躁| 国产精品探花在线观看| 亚洲香蕉av在线一区二区三区| mm131美女视频| 九九免费精品视频在线观看| 亚洲日本欧美中文幕| 中文字幕免费高清| 欧美少妇性xxxx| 色老头一区二区三区| 成人自拍小视频| 欧美三区不卡| 97在线视频免费看| 91久久国产综合久久91| 蜜桃av一区二区在线观看| 国产免费久久av| 精品人妻无码一区二区色欲产成人| 国产一区二区三区久久久| 99久久久精品免费观看国产| 内射后入在线观看一区| 久久综合色8888| 天堂精品视频| 2024短剧网剧在线观看| 午夜电影网一区| 日韩欧美在线免费观看视频| 亚洲午夜国产成人| 亚洲成人网久久久| 精品无码人妻一区二区免费蜜桃| 久久精品亚洲人成影院| 久久久久国产一区二区三区| 日韩欧美亚洲天堂| 在线最新版中文在线| 欧美日韩国产综合久久| 亚洲三级在线视频| 天天躁日日躁狠狠躁欧美巨大小说 | 一区二区不卡视频| free性欧美hd另类精品| 精品国产鲁一鲁一区二区张丽| 可以在线看的av网站| 成人做爰免费视频免费看| 日韩亚洲欧美一区二区三区| 国产男男chinese网站| 亚洲老妇激情| 国产成人激情视频| 亚洲av无码国产精品久久不卡| 91免费看`日韩一区二区| 亚洲一区二区三区乱码| 激情aⅴ欧美一区二区欲海潮| 欧美亚洲国产一区二区三区va| 国产伦理在线观看| 成人精品亚洲| 8090成年在线看片午夜| 国产一区二区三区三州| 97国产精品videossex| 综合一区中文字幕| 日韩av福利| 日韩欧美国产一区二区在线播放 | 国产欧美一区二区三区沐欲 | 视频在线观看一区| 99re在线播放| 在线视频婷婷| 欧美性猛xxx| 韩国av中国字幕| 99欧美视频| 国产成人精品999| 懂色av一区二区三区四区| 国产精品乱子久久久久| 欧美极品欧美精品欧美图片| 香蕉成人app| 日韩中文字幕在线视频| 国产精品传媒在线观看| 91麻豆福利精品推荐| 日韩一级性生活片| 九九99久久精品在免费线bt| 丝袜一区二区三区| 少妇一级淫片日本| 久久先锋资源网| 国产黄色一级网站| 麻豆成人入口| 97精品伊人久久久大香线蕉 | 成年女人18级毛片毛片免费| 亚洲精品66| 日韩亚洲精品视频| 中文字幕一区二区人妻痴汉电车| 久久久亚洲国产美女国产盗摄| 免费观看美女裸体网站| 9l视频自拍蝌蚪9l视频成人| 欧美成人精品h版在线观看| 国产又大又粗又硬| 中文字幕一区av| 国产永久免费网站| 亚洲h色精品| 96国产粉嫩美女| 成人日批视频| 日韩精品一区二区三区中文精品| 日韩一级片av| 大尺度一区二区| 免费日韩在线观看| 国产精品一线| 97久久伊人激情网| 视频三区在线观看| 在线视频中文字幕一区二区| 国产精品av久久久久久无| 视频一区视频二区中文| 亚洲欧洲另类精品久久综合| 日本在线一区二区| 久久99亚洲精品| 蜜臀av中文字幕| 精品久久久久久久久久| 男人天堂av电影| 麻豆精品在线视频| 青青草综合在线| 乱亲女h秽乱长久久久| 国产91在线播放九色快色| av在线首页| 欧美一区二区精品久久911| 九九免费精品视频| 91理论电影在线观看| 不卡av免费在线| 久久久人成影片免费观看| 国产精品国产三级国产专区53| 中文字幕在线中文字幕在线中三区| 亚洲视频一区二区| 99精品视频在线播放免费| 亚洲va欧美va天堂v国产综合| 一级黄色片大全| 激情久久五月天| 亚洲熟妇av日韩熟妇在线| 色狮一区二区三区四区视频| av成人免费观看| 欧美××××黑人××性爽| 美日韩精品免费视频| 日韩欧美在线观看一区二区| 欧美精品久久99久久在免费线| 日本一二三区不卡| 中文字幕在线观看一区二区| 这里只有精品在线观看视频| 麻豆精品国产传媒mv男同| 国产不卡一区二区视频| 欧美高清视频在线观看mv| 久久精品一区二区三区不卡免费视频| 日韩成人免费av| 欧美在线不卡区| 国产鲁鲁视频在线观看特色| 亚洲男人天天操| 亚洲av综合色区无码一区爱av | 97精品一区二区视频在线观看| 1024国产在线| 精品视频在线播放色网色视频| 国产精品人人爽| 色婷婷香蕉在线一区二区| 国产女人被狂躁到高潮小说| 国产日韩欧美综合一区| 日本少妇xxxx| 国产成人精品免费在线| 天天碰免费视频| 国产一区二区三区成人欧美日韩在线观看 | 女人帮男人橹视频播放| 成人在线一区| 蜜桃精品久久久久久久免费影院| 欧美一级网址| 国产精品91久久| 国产亚洲成av人片在线观看| 欧美成人手机在线| 免费超碰在线| 在线观看国产欧美| 成全电影播放在线观看国语| 日韩黄色av网站| 亚洲欧美国产高清va在线播放| 欧美精品丝袜久久久中文字幕| aaa在线视频| 一本到不卡精品视频在线观看| 久久精品免费av| 亚洲欧美偷拍卡通变态| 激情五月激情综合| 国产精品视频在线看| 丰满少妇高潮一区二区| 91视频免费观看| 国产黑丝一区二区| 成人h动漫精品一区二| 免费观看污网站| 国产成人免费视频精品含羞草妖精| 成年人网站av| 国产精品12区| 任你躁av一区二区三区| 国产成人av网站| 免费啪视频在线观看| 成人国产免费视频| 国产婷婷在线观看| 99视频超级精品| 国产黄色网址在线观看| 久久这里只有精品6| 国产免费无遮挡吸奶头视频| 国产亚洲成年网址在线观看| 日本免费www| 国产精品久久久久久久裸模| 日本猛少妇色xxxxx免费网站| 中文子幕无线码一区tr| 影音先锋男人看片资源| 成人欧美一区二区三区黑人麻豆| 国产性生活大片| 亚洲一区在线观看免费| 国产无码精品在线播放| 狠狠操狠狠色综合网| 亚洲 欧美 日韩 在线| 欧美日韩视频在线一区二区| 国产男女裸体做爰爽爽| 精品国产一区二区三区四区四| 日韩在线观看视频网站| 亚洲精品国产精品国产自| 久草在现在线| 色妞欧美日韩在线| 国内老司机av在线| 青青久久av北条麻妃黑人| 99蜜月精品久久91| av一本久道久久波多野结衣| 牛牛精品成人免费视频| 无遮挡亚洲一区| 欧美淫片网站| 漂亮人妻被中出中文字幕| 久久精品999| 老熟女高潮一区二区三区| 久久综合精品国产一区二区三区 | 天天操天天插天天射| 亚洲最新视频在线| 99福利在线| 青草热久免费精品视频| 四虎国产精品免费久久| 国产日韩亚洲精品| 久久麻豆精品| 国产69精品久久久久久久| 奇米影视7777精品一区二区| 日批视频在线看| 久久香蕉国产线看观看99| 男人的天堂久久久| 在线亚洲人成电影网站色www| japanese国产| 亚洲天堂日韩电影| 色av手机在线| 国产精品偷伦一区二区| 欧美日韩精品一区二区三区在线观看| 亚洲最大免费| 乱人伦精品视频在线观看| 精品国产鲁一鲁一区二区三区| 2021中文字幕一区亚洲| 久久精品一级片| 欧美视频你懂的| 亚洲av毛片成人精品| 久久伊人精品天天| 2019年精品视频自拍| 国产一区视频观看| 欧美有码视频| 色国产在线视频| 久久婷婷久久一区二区三区| 久草视频中文在线| 69堂精品视频| 91在线视频免费看| 欧美一级淫片videoshd| 97久久综合区小说区图片区| 亚洲欧洲日本国产| 全部av―极品视觉盛宴亚洲| 女人被狂躁c到高潮| 亚洲综合另类小说| 精品人妻无码一区二区| 色狠狠av一区二区三区香蕉蜜桃| 国精产品一区二区三区有限公司 | 中国老熟女重囗味hdxx| 国产精品久久久久久亚洲伦| 国产美女www| 亚洲欧美变态国产另类| 国内激情视频在线观看| 超碰97人人人人人蜜桃| 欧美+日本+国产+在线a∨观看| 超碰成人在线播放| 国产精品久久久久一区二区三区| 成人av网站在线播放| 亚洲欧美精品伊人久久| 成人爱爱网址| 欧美日韩在线精品一区二区三区| 性xx色xx综合久久久xx| 国产艳俗歌舞表演hd| 婷婷夜色潮精品综合在线| 全部免费毛片在线播放一个| 欧美大片大片在线播放| 51vv免费精品视频一区二区| 91传媒免费视频| 国产成人精品免费看| 唐朝av高清盛宴| 欧美mv日韩mv亚洲| free性护士videos欧美| 国产一区国产精品| 免费永久网站黄欧美| 国产av自拍一区| 欧美性猛交xxxxxx富婆| www.亚洲资源| 成人久久久久久久| 永久亚洲成a人片777777| 久久久久99人妻一区二区三区| 亚洲一区在线免费观看| 天天干,夜夜操| 国产成人精品久久久| 日本一区二区免费高清| 日日干日日操日日射| 亚洲精品乱码久久久久久| 人妻妺妺窝人体色www聚色窝| 97国产suv精品一区二区62| 国产欧美日韩精品一区二区三区| www.99av.com| 一区二区三区国产精品| 天天综合天天色| 国产成人在线精品| 午夜av一区| 手机在线成人av| 欧美在线视频全部完| 91精品久久| 久久精品日产第一区二区三区精品版 | 欧美极品欧美精品欧美| 久久色在线观看| 正在播放亚洲精品| 欧美另类极品videosbestfree| 欧美91在线| 日韩欧美国产片| 一级中文字幕一区二区| 欧美日韩在线中文字幕| 91久久久久久久久久| 日韩视频中文| 国产精品视频在| 精品乱码亚洲一区二区不卡| 三上悠亚国产精品一区二区三区| 中文网丁香综合网| 97精品国产97久久久久久久久久久久 | 天堂中文网在线| 国产精品女主播视频| 伊人精品成人久久综合软件| 亚洲AV无码成人精品区明星换面| 7777女厕盗摄久久久| 日本蜜桃在线观看视频| 异国色恋浪漫潭| 久久精品视频在线免费观看| 99久久免费国产精精品| 奇门遁甲1982国语版免费观看高清| 亚洲不卡av不卡一区二区|