|
@@ -0,0 +1,276 @@
|
|
|
+/*
|
|
|
+ * Copyright The OpenTelemetry Authors
|
|
|
+ * SPDX-License-Identifier: Apache-2.0
|
|
|
+ */
|
|
|
+
|
|
|
+package io.opentelemetry.javaagent.instrumentation.kubernetesclient;
|
|
|
+
|
|
|
+import static io.opentelemetry.instrumentation.testing.util.TelemetryDataUtil.orderByRootSpanName;
|
|
|
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
|
|
|
+import static org.assertj.core.api.Assertions.assertThat;
|
|
|
+
|
|
|
+import io.kubernetes.client.openapi.ApiCallback;
|
|
|
+import io.kubernetes.client.openapi.ApiClient;
|
|
|
+import io.kubernetes.client.openapi.ApiException;
|
|
|
+import io.kubernetes.client.openapi.apis.CoreV1Api;
|
|
|
+import io.opentelemetry.api.common.AttributeKey;
|
|
|
+import io.opentelemetry.api.trace.SpanKind;
|
|
|
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
|
|
|
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
|
|
|
+import io.opentelemetry.sdk.trace.data.StatusData;
|
|
|
+import io.opentelemetry.semconv.SemanticAttributes;
|
|
|
+import io.opentelemetry.testing.internal.armeria.common.HttpResponse;
|
|
|
+import io.opentelemetry.testing.internal.armeria.common.HttpStatus;
|
|
|
+import io.opentelemetry.testing.internal.armeria.common.MediaType;
|
|
|
+import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.CountDownLatch;
|
|
|
+import java.util.concurrent.atomic.AtomicReference;
|
|
|
+import org.junit.jupiter.api.AfterEach;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.api.extension.RegisterExtension;
|
|
|
+
|
|
|
+class KubernetesClientVer20Test {
|
|
|
+
|
|
|
+ private static final String TEST_USER_AGENT = "test-user-agent";
|
|
|
+
|
|
|
+ @RegisterExtension
|
|
|
+ private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
|
|
|
+
|
|
|
+ private final MockWebServerExtension mockWebServer = new MockWebServerExtension();
|
|
|
+
|
|
|
+ private CoreV1Api coreV1Api;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void beforeEach() {
|
|
|
+ mockWebServer.start();
|
|
|
+ ApiClient apiClient = new ApiClient();
|
|
|
+ apiClient.setUserAgent(TEST_USER_AGENT);
|
|
|
+ apiClient.setBasePath(mockWebServer.httpUri().toString());
|
|
|
+ coreV1Api = new CoreV1Api(apiClient);
|
|
|
+ }
|
|
|
+
|
|
|
+ @AfterEach
|
|
|
+ void afterEach() {
|
|
|
+ mockWebServer.stop();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void synchronousCall() throws ApiException {
|
|
|
+ mockWebServer.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42"));
|
|
|
+ String response =
|
|
|
+ testing.runWithSpan(
|
|
|
+ "parent",
|
|
|
+ () ->
|
|
|
+ coreV1Api
|
|
|
+ .connectGetNamespacedPodProxyWithPath("name", "namespace", "path")
|
|
|
+ .execute());
|
|
|
+
|
|
|
+ assertThat(response).isEqualTo("42");
|
|
|
+ assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank();
|
|
|
+
|
|
|
+ testing.waitAndAssertTraces(
|
|
|
+ trace ->
|
|
|
+ trace.hasSpansSatisfyingExactly(
|
|
|
+ span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(),
|
|
|
+ span ->
|
|
|
+ span.hasName("get pods/proxy")
|
|
|
+ .hasKind(SpanKind.CLIENT)
|
|
|
+ .hasParent(trace.getSpan(0))
|
|
|
+ .hasAttributesSatisfyingExactly(
|
|
|
+ equalTo(
|
|
|
+ SemanticAttributes.URL_FULL,
|
|
|
+ mockWebServer.httpUri()
|
|
|
+ + "/api/v1/namespaces/namespace/pods/name/proxy/path"),
|
|
|
+ equalTo(SemanticAttributes.HTTP_REQUEST_METHOD, "GET"),
|
|
|
+ equalTo(SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, 200),
|
|
|
+ equalTo(SemanticAttributes.SERVER_ADDRESS, "127.0.0.1"),
|
|
|
+ equalTo(SemanticAttributes.SERVER_PORT, mockWebServer.httpPort()),
|
|
|
+ equalTo(
|
|
|
+ AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"),
|
|
|
+ equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name"))));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void handleErrorsInSyncCall() {
|
|
|
+ mockWebServer.enqueue(
|
|
|
+ HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42"));
|
|
|
+ ApiException exception = null;
|
|
|
+ try {
|
|
|
+ testing.runWithSpan(
|
|
|
+ "parent",
|
|
|
+ () -> {
|
|
|
+ coreV1Api.connectGetNamespacedPodProxyWithPath("name", "namespace", "path").execute();
|
|
|
+ });
|
|
|
+ } catch (ApiException e) {
|
|
|
+ exception = e;
|
|
|
+ }
|
|
|
+ ApiException apiException = exception;
|
|
|
+ assertThat(apiException).isNotNull();
|
|
|
+ assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank();
|
|
|
+
|
|
|
+ testing.waitAndAssertTraces(
|
|
|
+ trace ->
|
|
|
+ trace.hasSpansSatisfyingExactly(
|
|
|
+ span ->
|
|
|
+ span.hasName("parent")
|
|
|
+ .hasKind(SpanKind.INTERNAL)
|
|
|
+ .hasNoParent()
|
|
|
+ .hasStatus(StatusData.error())
|
|
|
+ .hasException(apiException),
|
|
|
+ span ->
|
|
|
+ span.hasName("get pods/proxy")
|
|
|
+ .hasKind(SpanKind.CLIENT)
|
|
|
+ .hasParent(trace.getSpan(0))
|
|
|
+ .hasStatus(StatusData.error())
|
|
|
+ .hasException(apiException)
|
|
|
+ .hasAttributesSatisfyingExactly(
|
|
|
+ equalTo(
|
|
|
+ SemanticAttributes.URL_FULL,
|
|
|
+ mockWebServer.httpUri()
|
|
|
+ + "/api/v1/namespaces/namespace/pods/name/proxy/path"),
|
|
|
+ equalTo(SemanticAttributes.HTTP_REQUEST_METHOD, "GET"),
|
|
|
+ equalTo(SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, 451),
|
|
|
+ equalTo(SemanticAttributes.SERVER_ADDRESS, "127.0.0.1"),
|
|
|
+ equalTo(SemanticAttributes.SERVER_PORT, mockWebServer.httpPort()),
|
|
|
+ equalTo(SemanticAttributes.ERROR_TYPE, "451"),
|
|
|
+ equalTo(
|
|
|
+ AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"),
|
|
|
+ equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name"))));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void asynchronousCall() throws ApiException, InterruptedException {
|
|
|
+ mockWebServer.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42"));
|
|
|
+
|
|
|
+ AtomicReference<String> responseBodyReference = new AtomicReference<>();
|
|
|
+ CountDownLatch countDownLatch = new CountDownLatch(1);
|
|
|
+
|
|
|
+ testing.runWithSpan(
|
|
|
+ "parent",
|
|
|
+ () -> {
|
|
|
+ coreV1Api
|
|
|
+ .connectGetNamespacedPodProxyWithPath("name", "namespace", "path")
|
|
|
+ .executeAsync(
|
|
|
+ new ApiCallbackTemplate() {
|
|
|
+ @Override
|
|
|
+ public void onSuccess(
|
|
|
+ String result, int statusCode, Map<String, List<String>> responseHeaders) {
|
|
|
+ responseBodyReference.set(result);
|
|
|
+ countDownLatch.countDown();
|
|
|
+ testing.runWithSpan("callback", () -> {});
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ countDownLatch.await();
|
|
|
+
|
|
|
+ assertThat(responseBodyReference.get()).isEqualTo("42");
|
|
|
+ assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank();
|
|
|
+
|
|
|
+ testing.waitAndAssertSortedTraces(
|
|
|
+ orderByRootSpanName("parent", "get pods/proxy", "callback"),
|
|
|
+ trace ->
|
|
|
+ trace.hasSpansSatisfyingExactly(
|
|
|
+ span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(),
|
|
|
+ span ->
|
|
|
+ span.hasName("get pods/proxy")
|
|
|
+ .hasKind(SpanKind.CLIENT)
|
|
|
+ .hasParent(trace.getSpan(0))
|
|
|
+ .hasAttributesSatisfyingExactly(
|
|
|
+ equalTo(
|
|
|
+ SemanticAttributes.URL_FULL,
|
|
|
+ mockWebServer.httpUri()
|
|
|
+ + "/api/v1/namespaces/namespace/pods/name/proxy/path"),
|
|
|
+ equalTo(SemanticAttributes.HTTP_REQUEST_METHOD, "GET"),
|
|
|
+ equalTo(SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, 200),
|
|
|
+ equalTo(SemanticAttributes.SERVER_ADDRESS, "127.0.0.1"),
|
|
|
+ equalTo(SemanticAttributes.SERVER_PORT, mockWebServer.httpPort()),
|
|
|
+ equalTo(
|
|
|
+ AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"),
|
|
|
+ equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")),
|
|
|
+ span ->
|
|
|
+ span.hasName("callback")
|
|
|
+ .hasKind(SpanKind.INTERNAL)
|
|
|
+ .hasParent(trace.getSpan(0))));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void handleErrorsInAsynchronousCall() throws ApiException, InterruptedException {
|
|
|
+
|
|
|
+ mockWebServer.enqueue(
|
|
|
+ HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42"));
|
|
|
+
|
|
|
+ AtomicReference<ApiException> exceptionReference = new AtomicReference<>();
|
|
|
+ CountDownLatch countDownLatch = new CountDownLatch(1);
|
|
|
+
|
|
|
+ testing.runWithSpan(
|
|
|
+ "parent",
|
|
|
+ () -> {
|
|
|
+ coreV1Api
|
|
|
+ .connectGetNamespacedPodProxyWithPath("name", "namespace", "path")
|
|
|
+ .executeAsync(
|
|
|
+ new ApiCallbackTemplate() {
|
|
|
+ @Override
|
|
|
+ public void onFailure(
|
|
|
+ ApiException e, int statusCode, Map<String, List<String>> responseHeaders) {
|
|
|
+ exceptionReference.set(e);
|
|
|
+ countDownLatch.countDown();
|
|
|
+ testing.runWithSpan("callback", () -> {});
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ countDownLatch.await();
|
|
|
+
|
|
|
+ assertThat(exceptionReference.get()).isNotNull();
|
|
|
+ assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank();
|
|
|
+
|
|
|
+ testing.waitAndAssertSortedTraces(
|
|
|
+ orderByRootSpanName("parent", "get pods/proxy", "callback"),
|
|
|
+ trace ->
|
|
|
+ trace.hasSpansSatisfyingExactly(
|
|
|
+ span -> span.hasName("parent").hasKind(SpanKind.INTERNAL).hasNoParent(),
|
|
|
+ span ->
|
|
|
+ span.hasName("get pods/proxy")
|
|
|
+ .hasKind(SpanKind.CLIENT)
|
|
|
+ .hasParent(trace.getSpan(0))
|
|
|
+ .hasStatus(StatusData.error())
|
|
|
+ .hasException(exceptionReference.get())
|
|
|
+ .hasAttributesSatisfyingExactly(
|
|
|
+ equalTo(
|
|
|
+ SemanticAttributes.URL_FULL,
|
|
|
+ mockWebServer.httpUri()
|
|
|
+ + "/api/v1/namespaces/namespace/pods/name/proxy/path"),
|
|
|
+ equalTo(SemanticAttributes.HTTP_REQUEST_METHOD, "GET"),
|
|
|
+ equalTo(SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, 451),
|
|
|
+ equalTo(SemanticAttributes.SERVER_ADDRESS, "127.0.0.1"),
|
|
|
+ equalTo(SemanticAttributes.SERVER_PORT, mockWebServer.httpPort()),
|
|
|
+ equalTo(SemanticAttributes.ERROR_TYPE, "451"),
|
|
|
+ equalTo(
|
|
|
+ AttributeKey.stringKey("kubernetes-client.namespace"), "namespace"),
|
|
|
+ equalTo(AttributeKey.stringKey("kubernetes-client.name"), "name")),
|
|
|
+ span ->
|
|
|
+ span.hasName("callback")
|
|
|
+ .hasKind(SpanKind.INTERNAL)
|
|
|
+ .hasParent(trace.getSpan(0))));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ApiCallbackTemplate implements ApiCallback<String> {
|
|
|
+ @Override
|
|
|
+ public void onFailure(
|
|
|
+ ApiException e, int statusCode, Map<String, List<String>> responseHeaders) {}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onSuccess(
|
|
|
+ String result, int statusCode, Map<String, List<String>> responseHeaders) {}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onUploadProgress(long bytesWritten, long contentLength, boolean done) {}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onDownloadProgress(long bytesRead, long contentLength, boolean done) {}
|
|
|
+ }
|
|
|
+}
|