Skip to content

Commit

Permalink
Simplify the integration smoke test
Browse files Browse the repository at this point in the history
  • Loading branch information
psx95 committed Dec 24, 2024
1 parent 122c7d7 commit dacebad
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 3 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ subprojects {
mockito : "org.mockito:mockito-inline:${mockitoVersion}",
mockito_jupiter: "org.mockito:mockito-junit-jupiter:${mockitoVersion}",
slf4j_simple: "org.slf4j:slf4j-simple:${slf4jVersion}",
spring_boot_starter_test: "org.springframework.boot:spring-boot-starter-test:${springVersion}",
opentelemetry_sdk_testing: "io.opentelemetry:opentelemetry-sdk-testing:${openTelemetryVersion}",
test_containers: "org.testcontainers:testcontainers:${testContainersVersion}",
wiremock : "com.github.tomakehurst:wiremock-jre8:${wiremockVersion}",
Expand Down
64 changes: 61 additions & 3 deletions javaagent-extensions/gcp-auth/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.springframework.boot.gradle.tasks.run.BootRun

/*
* Copyright 2024 Google LLC
*
Expand All @@ -17,6 +34,7 @@ plugins {
id 'java'
id 'java-library'
id 'com.github.johnrengelman.shadow'
id 'org.springframework.boot' version '3.4.1'
}

description = 'OpenTelemetry Java Agent Extension that enables authentication support for OTLP exporters'
Expand Down Expand Up @@ -62,6 +80,12 @@ dependencies {
testImplementation(testLibraries.mockito)
testImplementation(testLibraries.mockito_jupiter)
testImplementation(libraries.opentelemetry_sdk_autoconf)
// for implementing smoke test application
testImplementation(libraries.spring_boot_starter_web)
testImplementation(testLibraries.spring_boot_starter_test)
testImplementation("org.mock-server:mockserver-netty:5.15.0")
testImplementation("io.opentelemetry.proto:opentelemetry-proto:1.4.0-alpha")
testImplementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.8.0")
// OTel instrumentation used in the sample app to facilitate integration testing
agent agentLibraries.agent
testImplementation 'org.awaitility:awaitility:4.2.2'
Expand All @@ -70,15 +94,15 @@ dependencies {
}

// task to copy and rename the Java Auto-Instrumentation Agent into 'libs' folder
tasks.register('CopyAgent', Copy) {
tasks.register('copyAgent', Copy) {
into layout.buildDirectory.dir("libs")
from configurations.agent {
rename "opentelemetry-javaagent(.*).jar", "otel-agent.jar"
}
}

tasks.register('BuildTestApp', org.gradle.jvm.tasks.Jar) {
dependsOn 'CopyAgent'
dependsOn 'copyAgent'
dependsOn 'shadowJar'

archiveFileName.set("auto-instrumented-test-server.jar")
Expand All @@ -96,7 +120,41 @@ tasks.register('BuildTestApp', org.gradle.jvm.tasks.Jar) {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

def builtLibsDir = layout.buildDirectory.dir("libs").get().toString()
def javaAgentJarPath = builtLibsDir + "/otel-agent.jar"
def authExtensionJarPath = builtLibsDir + "/gcp-auth-extension.jar"

// this task is run as part of the integration test so it is necessary to
// configure this
tasks.named('bootJar').configure {
dependsOn('copyAgent')
}

build {
// disable bootJar in build since it only runs as part of test
tasks.named('bootJar').configure {
enabled = false
}
}

test {
dependsOn 'BuildTestApp'
dependsOn 'shadowJar'
dependsOn 'copyAgent'
useJUnitPlatform()

environment("GOOGLE_CLOUD_QUOTA_PROJECT", "test-project-id")
jvmArgs = [
"-javaagent:${javaAgentJarPath}",
"-Dotel.javaagent.extensions=${authExtensionJarPath}",
"-Dgoogle.cloud.project=my-gcp-project",
"-Dotel.java.global-autoconfigure.enabled=true",
"-Dotel.exporter.otlp.endpoint=http://localhost:4318",
"-Dotel.resource.providers.gcp.enabled=true",
"-Dotel.traces.exporter=otlp",
"-Dotel.bsp.schedule.delay=2000",
"-Dotel.metrics.exporter=none",
"-Dotel.logs.exporter=none",
"-Dotel.exporter.otlp.protocol=http/protobuf",
"-Dmockserver.logLevel=off"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.cloud.opentelemetry.extension.auth;

import static com.google.cloud.opentelemetry.extension.auth.GcpAuthAutoConfigurationCustomizerProvider.GCP_USER_PROJECT_ID_KEY;
import static com.google.cloud.opentelemetry.extension.auth.GcpAuthAutoConfigurationCustomizerProvider.QUOTA_USER_PROJECT_HEADER;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.stop.Stop.stopQuietly;

import com.google.cloud.opentelemetry.extension.auth.springapp.Application;
import com.google.protobuf.InvalidProtocolBufferException;
import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
import io.opentelemetry.proto.common.v1.AnyValue;
import io.opentelemetry.proto.common.v1.KeyValue;
import io.opentelemetry.proto.trace.v1.ResourceSpans;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.mockserver.client.MockServerClient;
import org.mockserver.integration.ClientAndServer;
import org.mockserver.model.Body;
import org.mockserver.model.Headers;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.JsonBody;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;

@SpringBootTest(
classes = {Application.class},
webEnvironment = WebEnvironment.RANDOM_PORT)
public class GcpAuthExtensionSmokeTest {

@LocalServerPort private int testApplicationPort; // port at which the spring app is running

@Autowired private TestRestTemplate template;

// The port at which the backend server will recieve telemetry
private static final int EXPORTER_ENDPOINT_PORT = 4318;
// The port at which the mock GCP metadata server will run
private static final int MOCK_GCP_METADATA_PORT = 8090;

// Backend server to which the application under test will export traces
// the export config is specified in the build.gradle file.
private static ClientAndServer backendServer;

// Mock server to intercept calls to the GCP metadata server and provide fake credentials
private static ClientAndServer mockGcpMetadataServer;

private static final String METADATA_GOOGLE_INTERNAL = "metadata.google.internal";
private static final String DUMMY_GCP_QUOTA_PROJECT = System.getenv("GOOGLE_CLOUD_QUOTA_PROJECT");
private static final String DUMMY_GCP_PROJECT = System.getProperty("google.cloud.project");

@BeforeAll
public static void setup() {
// Set up the mock server to always respond with 200
// Setup proxy host
System.setProperty("http.proxyHost", "localhost");
System.setProperty("http.proxyPort", MOCK_GCP_METADATA_PORT + "");
System.setProperty("http.nonProxyHost", "localhost");

// Set up mock OTLP backend server to which traces will be exported
backendServer = ClientAndServer.startClientAndServer(EXPORTER_ENDPOINT_PORT);
backendServer.when(request()).respond(response().withStatusCode(200));

// Set up the mock gcp metadata server to provide fake credentials
String accessTokenResponse =
"{\"access_token\": \"fake.access_token\",\"expires_in\": 3600, \"token_type\": \"Bearer\"}";
mockGcpMetadataServer = ClientAndServer.startClientAndServer(MOCK_GCP_METADATA_PORT);
MockServerClient mockServerClient = new MockServerClient("localhost", MOCK_GCP_METADATA_PORT);
mockServerClient
.when(
request()
.withMethod("GET")
.withPath("/computeMetadata/v1/instance/service-accounts/default/token")
.withHeader("Host", METADATA_GOOGLE_INTERNAL)
.withHeader("Metadata-Flavor", "Google"))
.respond(
response()
.withStatusCode(200)
.withHeader("Content-Type", "application/json")
.withBody(new JsonBody(accessTokenResponse)));
}

@AfterAll
public static void teardown() {
// Stop the backend server
stopQuietly(backendServer);
stopQuietly(mockGcpMetadataServer);
}

@Test
public void authExtensionSmokeTest() {
ResponseEntity<?> a =
template.getForEntity(
URI.create("http://localhost:" + testApplicationPort + "/ping"), String.class);
System.out.println("resp is " + a.toString());

await()
.atMost(10, TimeUnit.SECONDS)
.untilAsserted(
() -> {
HttpRequest[] requests = backendServer.retrieveRecordedRequests(request());
List<Headers> extractedHeaders = extractHeadersFromRequests(requests);
verifyRequestHeaders(extractedHeaders);

List<ResourceSpans> extractedResourceSpans =
extractResourceSpansFromRequests(requests);
verifyResourceAttributes(extractedResourceSpans);
});
}

// Helper methods

private void verifyResourceAttributes(List<ResourceSpans> extractedResourceSpans) {
extractedResourceSpans.forEach(
resourceSpan ->
assertTrue(
resourceSpan
.getResource()
.getAttributesList()
.contains(
KeyValue.newBuilder()
.setKey(GCP_USER_PROJECT_ID_KEY)
.setValue(AnyValue.newBuilder().setStringValue(DUMMY_GCP_PROJECT))
.build())));
}

private void verifyRequestHeaders(List<Headers> extractedHeaders) {
assertFalse(extractedHeaders.isEmpty());
// verify if extension added the required headers
extractedHeaders.forEach(
headers -> {
assertTrue(headers.containsEntry(QUOTA_USER_PROJECT_HEADER, DUMMY_GCP_QUOTA_PROJECT));
assertTrue(headers.containsEntry("Authorization", "Bearer fake.access_token"));
});
}

private List<Headers> extractHeadersFromRequests(HttpRequest[] requests) {
return Arrays.stream(requests).map(HttpRequest::getHeaders).collect(Collectors.toList());
}

/**
* Extract resource spans from http requests received by a telemetry collector.
*
* @param requests Request received by a http server trace collector
* @return spans extracted from the request body
*/
private List<ResourceSpans> extractResourceSpansFromRequests(HttpRequest[] requests) {
return Arrays.stream(requests)
.map(HttpRequest::getBody)
.flatMap(body -> getExportTraceServiceRequest(body).stream())
.flatMap(r -> r.getResourceSpansList().stream())
.collect(Collectors.toList());
}

private Optional<ExportTraceServiceRequest> getExportTraceServiceRequest(Body<?> body) {
try {
return Optional.ofNullable(ExportTraceServiceRequest.parseFrom(body.getRawBytes()));
} catch (InvalidProtocolBufferException e) {
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.cloud.opentelemetry.extension.auth.springapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.cloud.opentelemetry.extension.auth.springapp;

import io.opentelemetry.instrumentation.annotations.WithSpan;
import java.time.Duration;
import java.time.Instant;
import java.util.Random;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Controller {

private final Random random = new Random();

@GetMapping("/ping")
public String ping() {
int busyTime = random.nextInt(200);
long ctr = busyloop(busyTime);
System.out.println("Busy work done, counted " + ctr + " times in " + busyTime + " ms");
return "pong";
}

@WithSpan
private long busyloop(int busyMillis) {
Instant start = Instant.now();
Instant end;
long counter = 0;
do {
counter++;
end = Instant.now();
} while (Duration.between(start, end).toMillis() < busyMillis);
return counter;
}
}

0 comments on commit dacebad

Please sign in to comment.