Convert kubernetes-client-7.0 unit tests to Java (#9163)

This commit is contained in:
pg.yang 2023-08-09 23:03:03 +08:00 committed by GitHub
parent 0c79f14c79
commit a332eb2887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 483 additions and 309 deletions

View File

@ -1,87 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesRequestDigest
import io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesResource
import io.opentelemetry.javaagent.instrumentation.kubernetesclient.KubernetesVerb
import spock.lang.Specification
class KubernetesRequestUtilsTest extends Specification {
def "asserting non-resource requests should work"() {
expect:
!KubernetesRequestDigest.isResourceRequest("/api")
!KubernetesRequestDigest.isResourceRequest("/apis")
!KubernetesRequestDigest.isResourceRequest("/apis/v1")
!KubernetesRequestDigest.isResourceRequest("/healthz")
!KubernetesRequestDigest.isResourceRequest("/swagger.json")
!KubernetesRequestDigest.isResourceRequest("/api/v1")
!KubernetesRequestDigest.isResourceRequest("/api/v1/")
!KubernetesRequestDigest.isResourceRequest("/apis/apps/v1")
!KubernetesRequestDigest.isResourceRequest("/apis/apps/v1/")
}
def "asserting resource requests should work"() {
expect:
KubernetesRequestDigest.isResourceRequest("/apis/example.io/v1/foos")
KubernetesRequestDigest.isResourceRequest("/apis/example.io/v1/namespaces/default/foos")
KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces")
KubernetesRequestDigest.isResourceRequest("/api/v1/pods")
KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces/default/pods")
}
def "parsing core resource from url-path should work"(String urlPath, String apiGroup, String apiVersion, String resource, String subResource, String namespace, String name) {
expect:
KubernetesResource.parseCoreResource(urlPath).apiGroup == apiGroup
KubernetesResource.parseCoreResource(urlPath).apiVersion == apiVersion
KubernetesResource.parseCoreResource(urlPath).resource == resource
KubernetesResource.parseCoreResource(urlPath).subResource == subResource
KubernetesResource.parseCoreResource(urlPath).namespace == namespace
KubernetesResource.parseCoreResource(urlPath).name == name
where:
urlPath | apiGroup | apiVersion | resource | subResource | namespace | name
"/api/v1/pods" | "" | "v1" | "pods" | null | null | null
"/api/v1/namespaces/default/pods" | "" | "v1" | "pods" | null | "default" | null
"/api/v1/namespaces/default/pods/foo" | "" | "v1" | "pods" | null | "default" | "foo"
"/api/v1/namespaces/default/pods/foo/exec" | "" | "v1" | "pods" | "exec" | "default" | "foo"
}
def "parsing regular non-core resource from url-path should work"(String urlPath, String apiGroup, String apiVersion, String resource, String subResource, String namespace, String name) {
expect:
KubernetesResource.parseRegularResource(urlPath).apiGroup == apiGroup
KubernetesResource.parseRegularResource(urlPath).apiVersion == apiVersion
KubernetesResource.parseRegularResource(urlPath).resource == resource
KubernetesResource.parseRegularResource(urlPath).subResource == subResource
KubernetesResource.parseRegularResource(urlPath).namespace == namespace
KubernetesResource.parseRegularResource(urlPath).name == name
where:
urlPath | apiGroup | apiVersion | resource | subResource | namespace | name
"/apis/apps/v1/deployments" | "apps" | "v1" | "deployments" | null | null | null
"/apis/apps/v1/namespaces/default/deployments" | "apps" | "v1" | "deployments" | null | "default" | null
"/apis/apps/v1/namespaces/default/deployments/foo" | "apps" | "v1" | "deployments" | null | "default" | "foo"
"/apis/apps/v1/namespaces/default/deployments/foo/status" | "apps" | "v1" | "deployments" | "status" | "default" | "foo"
"/apis/example.io/v1alpha1/foos" | "example.io" | "v1alpha1" | "foos" | null | null | null
"/apis/example.io/v1alpha1/namespaces/default/foos" | "example.io" | "v1alpha1" | "foos" | null | "default" | null
"/apis/example.io/v1alpha1/namespaces/default/foos/foo" | "example.io" | "v1alpha1" | "foos" | null | "default" | "foo"
"/apis/example.io/v1alpha1/namespaces/default/foos/foo/status" | "example.io" | "v1alpha1" | "foos" | "status" | "default" | "foo"
}
def "parsing kubernetes request verbs should work"(String httpVerb, boolean hasNamePathParam, boolean hasWatchParam, KubernetesVerb kubernetesVerb) {
expect:
KubernetesVerb.of(httpVerb, hasNamePathParam, hasWatchParam) == kubernetesVerb
where:
httpVerb | hasNamePathParam | hasWatchParam | kubernetesVerb
"GET" | true | false | KubernetesVerb.GET
"GET" | false | true | KubernetesVerb.WATCH
"GET" | false | false | KubernetesVerb.LIST
"POST" | false | false | KubernetesVerb.CREATE
"PUT" | false | false | KubernetesVerb.UPDATE
"PATCH" | false | false | KubernetesVerb.PATCH
"DELETE" | true | false | KubernetesVerb.DELETE
"DELETE" | false | false | KubernetesVerb.DELETE_COLLECTION
}
}

View File

@ -0,0 +1,192 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.kubernetesclient;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
class KubernetesRequestUtilsTest {
@Test
void isResourceRequest() {
assertThat(KubernetesRequestDigest.isResourceRequest("/api")).isFalse();
assertThat(KubernetesRequestDigest.isResourceRequest("/apis")).isFalse();
assertThat(KubernetesRequestDigest.isResourceRequest("/apis/v1")).isFalse();
assertThat(KubernetesRequestDigest.isResourceRequest("/healthz")).isFalse();
assertThat(KubernetesRequestDigest.isResourceRequest("/swagger.json")).isFalse();
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1")).isFalse();
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/")).isFalse();
assertThat(KubernetesRequestDigest.isResourceRequest("/apis/apps/v1")).isFalse();
assertThat(KubernetesRequestDigest.isResourceRequest("/apis/apps/v1/")).isFalse();
assertThat(KubernetesRequestDigest.isResourceRequest("/apis/example.io/v1/foos")).isTrue();
assertThat(
KubernetesRequestDigest.isResourceRequest(
"/apis/example.io/v1/namespaces/default/foos"))
.isTrue();
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces")).isTrue();
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/pods")).isTrue();
assertThat(KubernetesRequestDigest.isResourceRequest("/api/v1/namespaces/default/pods"))
.isTrue();
}
@ParameterizedTest
@ArgumentsSource(ParseCoreResourceArgumentsProvider.class)
void parseCoreResource(
String urlPath,
String apiGroup,
String apiVersion,
String resource,
String subResource,
String namespace,
String name)
throws ParseKubernetesResourceException {
assertThat(KubernetesResource.parseCoreResource(urlPath).getApiGroup()).isEqualTo(apiGroup);
assertThat(KubernetesResource.parseCoreResource(urlPath).getApiVersion()).isEqualTo(apiVersion);
assertThat(KubernetesResource.parseCoreResource(urlPath).getResource()).isEqualTo(resource);
assertThat(KubernetesResource.parseCoreResource(urlPath).getSubResource())
.isEqualTo(subResource);
assertThat(KubernetesResource.parseCoreResource(urlPath).getNamespace()).isEqualTo(namespace);
assertThat(KubernetesResource.parseCoreResource(urlPath).getName()).isEqualTo(name);
}
@ParameterizedTest
@ArgumentsSource(ParseRegularResourceArgumentsProvider.class)
void parseRegularResource(
String urlPath,
String apiGroup,
String apiVersion,
String resource,
String subResource,
String namespace,
String name)
throws ParseKubernetesResourceException {
assertThat(KubernetesResource.parseRegularResource(urlPath).getApiGroup()).isEqualTo(apiGroup);
assertThat(KubernetesResource.parseRegularResource(urlPath).getApiVersion())
.isEqualTo(apiVersion);
assertThat(KubernetesResource.parseRegularResource(urlPath).getResource()).isEqualTo(resource);
assertThat(KubernetesResource.parseRegularResource(urlPath).getSubResource())
.isEqualTo(subResource);
assertThat(KubernetesResource.parseRegularResource(urlPath).getNamespace())
.isEqualTo(namespace);
assertThat(KubernetesResource.parseRegularResource(urlPath).getName()).isEqualTo(name);
}
@ParameterizedTest
@ArgumentsSource(K8sRequestVerbsArgumentsProvider.class)
void k8sRequestVerbs(
String httpVerb,
boolean hasNamePathParam,
boolean hasWatchParam,
KubernetesVerb kubernetesVerb) {
assertThat(KubernetesVerb.of(httpVerb, hasNamePathParam, hasWatchParam))
.isEqualTo(kubernetesVerb);
}
private static class K8sRequestVerbsArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext)
throws Exception {
return Stream.of(
Arguments.of("GET", true, false, KubernetesVerb.GET),
Arguments.of("GET", false, true, KubernetesVerb.WATCH),
Arguments.of("GET", false, false, KubernetesVerb.LIST),
Arguments.of("POST", false, false, KubernetesVerb.CREATE),
Arguments.of("PUT", false, false, KubernetesVerb.UPDATE),
Arguments.of("PATCH", false, false, KubernetesVerb.PATCH),
Arguments.of("DELETE", true, false, KubernetesVerb.DELETE),
Arguments.of("DELETE", false, false, KubernetesVerb.DELETE_COLLECTION));
}
}
private static class ParseRegularResourceArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext)
throws Exception {
return Stream.of(
Arguments.of("/apis/apps/v1/deployments", "apps", "v1", "deployments", null, null, null),
Arguments.of(
"/apis/apps/v1/namespaces/default/deployments",
"apps",
"v1",
"deployments",
null,
"default",
null),
Arguments.of(
"/apis/apps/v1/namespaces/default/deployments/foo",
"apps",
"v1",
"deployments",
null,
"default",
"foo"),
Arguments.of(
"/apis/apps/v1/namespaces/default/deployments/foo/status",
"apps",
"v1",
"deployments",
"status",
"default",
"foo"),
Arguments.of(
"/apis/example.io/v1alpha1/foos", "example.io", "v1alpha1", "foos", null, null, null),
Arguments.of(
"/apis/example.io/v1alpha1/namespaces/default/foos",
"example.io",
"v1alpha1",
"foos",
null,
"default",
null),
Arguments.of(
"/apis/example.io/v1alpha1/namespaces/default/foos/foo",
"example.io",
"v1alpha1",
"foos",
null,
"default",
"foo"),
Arguments.of(
"/apis/example.io/v1alpha1/namespaces/default/foos/foo/status",
"example.io",
"v1alpha1",
"foos",
"status",
"default",
"foo"));
}
}
private static class ParseCoreResourceArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(
Arguments.of("/api/v1/pods", "", "v1", "pods", null, null, null),
Arguments.of("/api/v1/namespaces/default/pods", "", "v1", "pods", null, "default", null),
Arguments.of(
"/api/v1/namespaces/default/pods/foo", "", "v1", "pods", null, "default", "foo"),
Arguments.of(
"/api/v1/namespaces/default/pods/foo/exec",
"",
"v1",
"pods",
"exec",
"default",
"foo"));
}
}
}

View File

@ -1,222 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
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.trace.SpanKind
import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
import io.opentelemetry.instrumentation.test.asserts.TraceAssert
import io.opentelemetry.semconv.trace.attributes.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 spock.lang.Shared
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicReference
import static io.opentelemetry.api.trace.SpanKind.CLIENT
import static io.opentelemetry.api.trace.StatusCode.ERROR
class KubernetesClientTest extends AgentInstrumentationSpecification {
private static final String TEST_USER_AGENT = "test-user-agent"
@Shared
def server = new MockWebServerExtension()
@Shared
CoreV1Api api
def setupSpec() {
server.start()
def apiClient = new ApiClient()
apiClient.setUserAgent(TEST_USER_AGENT)
apiClient.basePath = server.httpUri().toString()
api = new CoreV1Api(apiClient)
}
def cleanupSpec() {
server.stop()
}
def setup() {
server.beforeTestExecution(null)
}
def "Kubernetes span is registered on a synchronous call"() {
given:
server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42"))
when:
def response = runWithSpan("parent") {
api.connectGetNamespacedPodProxy("name", "namespace", "path")
}
then:
response == "42"
server.takeRequest().request().headers().get("traceparent") != null
assertTraces(1) {
trace(0, 2) {
span(0) {
name "parent"
kind SpanKind.INTERNAL
hasNoParent()
}
apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 200)
}
}
}
def "Kubernetes instrumentation handles errors on a synchronous call"() {
given:
server.enqueue(HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42"))
when:
runWithSpan("parent") {
api.connectGetNamespacedPodProxy("name", "namespace", "path")
}
then:
def exception = thrown(ApiException)
server.takeRequest().request().headers().get("traceparent") != null
assertTraces(1) {
trace(0, 2) {
span(0) {
name "parent"
kind SpanKind.INTERNAL
hasNoParent()
status ERROR
errorEvent(exception.class, exception.message)
}
apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 451, exception)
}
}
}
def "Kubernetes span is registered on an asynchronous call"() {
given:
server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "42"))
when:
def responseBody = new AtomicReference<String>()
def latch = new CountDownLatch(1)
runWithSpan("parent") {
api.connectGetNamespacedPodProxyAsync("name", "namespace", "path", new ApiCallbackTemplate() {
@Override
void onSuccess(String result, int statusCode, Map<String, List<String>> responseHeaders) {
responseBody.set(result)
latch.countDown()
runWithSpan("callback") {}
}
})
}
then:
latch.await()
responseBody.get() == "42"
server.takeRequest().request().headers().get("traceparent") != null
assertTraces(1) {
trace(0, 3) {
span(0) {
name "parent"
kind SpanKind.INTERNAL
hasNoParent()
}
apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 200)
span(2) {
name "callback"
kind SpanKind.INTERNAL
childOf span(0)
}
}
}
}
def "Kubernetes instrumentation handles errors on an asynchronous call"() {
given:
server.enqueue(HttpResponse.of(HttpStatus.valueOf(451), MediaType.PLAIN_TEXT_UTF_8, "42"))
when:
def exception = new AtomicReference<Exception>()
def latch = new CountDownLatch(1)
runWithSpan("parent") {
api.connectGetNamespacedPodProxyAsync("name", "namespace", "path", new ApiCallbackTemplate() {
@Override
void onFailure(ApiException e, int statusCode, Map<String, List<String>> responseHeaders) {
exception.set(e)
latch.countDown()
runWithSpan("callback") {}
}
})
}
then:
latch.await()
exception.get() != null
server.takeRequest().request().headers().get("traceparent") != null
assertTraces(1) {
trace(0, 3) {
span(0) {
name "parent"
kind SpanKind.INTERNAL
hasNoParent()
}
apiClientSpan(it, 1, "get pods/proxy", "${server.httpUri()}/api/v1/namespaces/namespace/pods/name/proxy?path=path", 451, exception.get())
span(2) {
name "callback"
kind SpanKind.INTERNAL
childOf span(0)
}
}
}
}
private void apiClientSpan(TraceAssert trace, int index, String spanName, String url, int statusCode, Throwable exception = null) {
boolean hasFailed = exception != null
trace.span(index) {
name spanName
kind CLIENT
childOf trace.span(0)
if (hasFailed) {
status ERROR
errorEvent exception.class, exception.message
}
attributes {
"$SemanticAttributes.HTTP_URL" url
"$SemanticAttributes.HTTP_METHOD" "GET"
"$SemanticAttributes.USER_AGENT_ORIGINAL" TEST_USER_AGENT
"$SemanticAttributes.HTTP_STATUS_CODE" statusCode
"$SemanticAttributes.NET_PEER_NAME" "127.0.0.1"
"$SemanticAttributes.NET_PEER_PORT" server.httpPort()
"$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long
"kubernetes-client.namespace" "namespace"
"kubernetes-client.name" "name"
}
}
}
static class ApiCallbackTemplate implements ApiCallback<String> {
@Override
void onFailure(ApiException e, int statusCode, Map<String, List<String>> responseHeaders) {}
@Override
void onSuccess(String result, int statusCode, Map<String, List<String>> responseHeaders) {}
@Override
void onUploadProgress(long bytesWritten, long contentLength, boolean done) {}
@Override
void onDownloadProgress(long bytesRead, long contentLength, boolean done) {}
}
}

View File

@ -0,0 +1,291 @@
/*
* 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 io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
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.trace.attributes.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.assertj.core.api.AbstractAssert;
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 KubernetesClientTest {
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.connectGetNamespacedPodProxy("name", "namespace", "path"));
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.HTTP_URL,
mockWebServer.httpUri()
+ "/api/v1/namespaces/namespace/pods/name/proxy?path=path"),
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT),
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200),
equalTo(SemanticAttributes.NET_PEER_NAME, "127.0.0.1"),
equalTo(SemanticAttributes.NET_PEER_PORT, mockWebServer.httpPort()),
satisfies(
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
AbstractAssert::isNotNull),
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"));
AtomicReference<ApiException> apiExceptionReference = new AtomicReference<>(null);
try {
testing.runWithSpan(
"parent",
() -> {
coreV1Api.connectGetNamespacedPodProxy("name", "namespace", "path");
});
} catch (ApiException e) {
apiExceptionReference.set(e);
}
assertThat(apiExceptionReference.get()).isNotNull();
assertThat(mockWebServer.takeRequest().request().headers().get("traceparent")).isNotBlank();
ApiException apiException = apiExceptionReference.get();
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.HTTP_URL,
mockWebServer.httpUri()
+ "/api/v1/namespaces/namespace/pods/name/proxy?path=path"),
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT),
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 451),
equalTo(SemanticAttributes.NET_PEER_NAME, "127.0.0.1"),
equalTo(SemanticAttributes.NET_PEER_PORT, mockWebServer.httpPort()),
satisfies(
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
AbstractAssert::isNotNull),
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.connectGetNamespacedPodProxyAsync(
"name",
"namespace",
"path",
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.HTTP_URL,
mockWebServer.httpUri()
+ "/api/v1/namespaces/namespace/pods/name/proxy?path=path"),
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT),
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 200),
equalTo(SemanticAttributes.NET_PEER_NAME, "127.0.0.1"),
equalTo(SemanticAttributes.NET_PEER_PORT, mockWebServer.httpPort()),
satisfies(
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
AbstractAssert::isNotNull),
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.connectGetNamespacedPodProxyAsync(
"name",
"namespace",
"path",
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.HTTP_URL,
mockWebServer.httpUri()
+ "/api/v1/namespaces/namespace/pods/name/proxy?path=path"),
equalTo(SemanticAttributes.HTTP_METHOD, "GET"),
equalTo(SemanticAttributes.USER_AGENT_ORIGINAL, TEST_USER_AGENT),
equalTo(SemanticAttributes.HTTP_STATUS_CODE, 451),
equalTo(SemanticAttributes.NET_PEER_NAME, "127.0.0.1"),
equalTo(SemanticAttributes.NET_PEER_PORT, mockWebServer.httpPort()),
satisfies(
SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH,
AbstractAssert::isNotNull),
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) {}
}
}