diff --git a/instrumentation/kubernetes-client-7.0/javaagent/build.gradle.kts b/instrumentation/kubernetes-client-7.0/javaagent/build.gradle.kts index 67cf51b7e2..d3d085e6df 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/build.gradle.kts +++ b/instrumentation/kubernetes-client-7.0/javaagent/build.gradle.kts @@ -14,9 +14,29 @@ muzzle { dependencies { library("io.kubernetes:client-java-api:7.0.0") - implementation(project(":instrumentation:okhttp:okhttp-3.0:javaagent")) - testInstrumentation(project(":instrumentation:okhttp:okhttp-3.0:javaagent")) + + latestDepTestLibrary("io.kubernetes:client-java-api:19.+") +} + +testing { + suites { + val version20Test by registering(JvmTestSuite::class) { + dependencies { + if (findProperty("testLatestDeps") as Boolean) { + implementation("io.kubernetes:client-java-api:+") + } else { + implementation("io.kubernetes:client-java-api:20.0.0") + } + } + } + } +} + +tasks { + check { + dependsOn(testing.suites) + } } tasks.withType().configureEach { diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java index 7b95f6a958..1800e32aac 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/ApiClientInstrumentation.java @@ -35,7 +35,7 @@ public class ApiClientInstrumentation implements TypeInstrumentation { @Override public void transform(TypeTransformer transformer) { transformer.applyAdviceToMethod( - isPublic().and(named("buildRequest")).and(takesArguments(10)), + isPublic().and(named("buildRequest")).and(takesArguments(10).or(takesArguments(11))), this.getClass().getName() + "$BuildRequestAdvice"); transformer.applyAdviceToMethod( isPublic() diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java index e53504ff1a..a3365c3422 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesRequestDigest.java @@ -89,7 +89,7 @@ class KubernetesRequestDigest { @Override public String toString() { if (isNonResourceRequest) { - return verb.value() + ' ' + urlPath; + return (verb != null ? verb.value() + ' ' : "") + urlPath; } String groupVersion; diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java index 57b10b3db5..31413a790c 100644 --- a/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesResource.java @@ -12,7 +12,7 @@ class KubernetesResource { public static final Pattern CORE_RESOURCE_URL_PATH_PATTERN = Pattern.compile( - "^/api/v1(/namespaces/(?[\\w-]+))?/(?[\\w-]+)(/(?[\\w-]+))?(/(?[\\w-]+))?"); + "^/api/v1(/namespaces/(?[\\w-]+))?/(?[\\w-]+)(/(?[\\w-]+))?(/(?[\\w-]+))?(/.*)?"); public static final Pattern REGULAR_RESOURCE_URL_PATH_PATTERN = Pattern.compile( diff --git a/instrumentation/kubernetes-client-7.0/javaagent/src/version20Test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientVer20Test.java b/instrumentation/kubernetes-client-7.0/javaagent/src/version20Test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientVer20Test.java new file mode 100644 index 0000000000..b521d05a27 --- /dev/null +++ b/instrumentation/kubernetes-client-7.0/javaagent/src/version20Test/java/io/opentelemetry/javaagent/instrumentation/kubernetesclient/KubernetesClientVer20Test.java @@ -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 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> 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 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> 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 { + @Override + public void onFailure( + ApiException e, int statusCode, Map> responseHeaders) {} + + @Override + public void onSuccess( + String result, int statusCode, Map> responseHeaders) {} + + @Override + public void onUploadProgress(long bytesWritten, long contentLength, boolean done) {} + + @Override + public void onDownloadProgress(long bytesRead, long contentLength, boolean done) {} + } +}