From 0090eda19d7e2dee312d1facdb9b4a3c814c8564 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Fri, 13 Dec 2024 19:37:11 +0000 Subject: [PATCH 01/17] Add gcp-auth-extension for OTLP exporters --- javaagent-extensions/gcp-auth/build.gradle | 48 ++++++ .../extension/auth/ConfigurableOption.java | 95 +++++++++++ ...thAutoConfigurationCustomizerProvider.java | 147 ++++++++++++++++++ .../extension/auth/GoogleAuthException.java | 62 ++++++++ settings.gradle | 4 + 5 files changed, 356 insertions(+) create mode 100644 javaagent-extensions/gcp-auth/build.gradle create mode 100644 javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java create mode 100644 javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java create mode 100644 javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GoogleAuthException.java diff --git a/javaagent-extensions/gcp-auth/build.gradle b/javaagent-extensions/gcp-auth/build.gradle new file mode 100644 index 00000000..7e5cbf41 --- /dev/null +++ b/javaagent-extensions/gcp-auth/build.gradle @@ -0,0 +1,48 @@ +/* + * 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. + */ +plugins { + id 'java-library' + id 'com.github.johnrengelman.shadow' +} + +description = 'OpenTelemetry Java Agent Extension that enables authentication support for OTLP exporters' + +tasks { + assemble.dependsOn(shadowJar) +} + +shadowJar { + archiveClassifier.set("") +} + +jar { + // Disable standard jar + enabled = false +} + +dependencies { + api(libraries.auto_service_annotations) + annotationProcessor(libraries.auto_service) + implementation(libraries.opentelemetry_api) + implementation(libraries.opentelemetry_otlp_exporter) + implementation(libraries.opentelemetry_sdk_autoconf) + implementation(libraries.opentelemetry_autoconfigure_spi) + implementation(libraries.google_auth) +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java new file mode 100644 index 00000000..04d3bc2a --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java @@ -0,0 +1,95 @@ +/* + * 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 io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import java.util.Locale; + +/** + * An enum representing configurable options for a GCP Authentication Extension. Each option has a + * user-readable name and can be configured using environment variables or system properties. + */ +public enum ConfigurableOption { + /** + * Represents the Google Cloud Project ID option. Can be configured using the environment variable + * `GOOGLE_CLOUD_PROJECT` or the system property `google.cloud.project`. + */ + GOOGLE_CLOUD_PROJECT("Google Cloud Project ID"), + + /** + * Represents the Google Cloud Region option. Can be configured using the environment variable + * `GOOGLE_CLOUD_REGION` or the system property `google.cloud.region`. + */ + GOOGLE_CLOUD_REGION("Google Cloud Region"); + + private static final String OPTION_NOT_CONFIGURED_MSG = + "GCP Authentication Extension not configured properly: %s not configured. Configure it by exporting environment variable %s or system property %s"; + + private final String userReadableName; + private final String environmentVariableName; + private final String systemPropertyName; + + ConfigurableOption(String userReadableName) { + this.userReadableName = userReadableName; + this.environmentVariableName = this.name(); + this.systemPropertyName = + this.environmentVariableName.toLowerCase(Locale.ENGLISH).replace('_', '.'); + } + + /** + * Returns the environment variable name associated with this option. + * + * @return the environment variable name (e.g., GOOGLE_CLOUD_PROJECT) + */ + String getEnvironmentVariable() { + return this.environmentVariableName; + } + + /** + * Returns the system property name associated with this option. + * + * @return the system property name (e.g., google.cloud.project) + */ + String getSystemProperty() { + return this.systemPropertyName; + } + + /** + * Retrieves the configured value for this option. This method checks the environment variable + * first and then the system property. + * + * @return the configured value as a string, or throws an exception if not configured. + * @throws ConfigurationException if neither the environment variable nor the system property is + * set. + */ + String getConfiguredValue() throws ConfigurationException { + String envVar = System.getenv(this.getEnvironmentVariable()); + String sysProp = System.getProperty(this.getSystemProperty()); + + if (envVar != null && !envVar.isEmpty()) { + return envVar; + } else if (sysProp != null && !sysProp.isEmpty()) { + return sysProp; + } else { + throw new ConfigurationException( + String.format( + OPTION_NOT_CONFIGURED_MSG, + this.userReadableName, + this.getEnvironmentVariable(), + this.getSystemProperty())); + } + } +} diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java new file mode 100644 index 00000000..aa8269b8 --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -0,0 +1,147 @@ +/* + * 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 com.google.auth.oauth2.GoogleCredentials; +import com.google.auto.service.AutoService; +import com.google.cloud.opentelemetry.extension.auth.GoogleAuthException.Reason; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * An AutoConfigurationCustomizerProvider for Google Cloud Platform (GCP) OpenTelemetry (OTLP) + * integration. + * + *

This class is registered as a service provider using {@link AutoService} and is responsible + * for customizing the OpenTelemetry configuration for GCP specific behavior. It retrieves Google + * Application Default Credentials (ADC) and adds them as authorization headers to the configured + * {@link SpanExporter}. It also sets default properties and resource attributes for GCP + * integration. + * + * @see AutoConfigurationCustomizerProvider + * @see GoogleCredentials + */ +@AutoService(AutoConfigurationCustomizerProvider.class) +public class GcpAuthAutoConfigurationCustomizerProvider + implements AutoConfigurationCustomizerProvider { + + private static final String GCP_OTLP_ENDPOINT_STUB = + "https://staging-%s-telemetry.sandbox.googleapis.com:443"; + private static final String QUOTA_USER_PROJECT_HEADER = "X-Goog-User-Project"; + private static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id"; + + /** + * Customizes the provided {@link AutoConfigurationCustomizer}. + * + *

This method attempts to retrieve Google Application Default Credentials (ADC) and performs + * the following: - Adds authorization headers to the configured {@link SpanExporter} based on the + * retrieved credentials. - Adds default properties for OTLP endpoint and resource attributes for + * GCP integration. + * + * @param autoConfiguration the AutoConfigurationCustomizer to customize. + * @throws GoogleAuthException if there's an error retrieving Google Application Default + * Credentials. + * @throws io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException if required options are + * not configured through environment variables or system properties. + */ + @Override + public void customize(AutoConfigurationCustomizer autoConfiguration) { + try { + GoogleCredentials credentials = GoogleCredentials.getApplicationDefault(); + autoConfiguration + .addSpanExporterCustomizer( + (exporter, configProperties) -> addAuthorizationHeaders(exporter, credentials)) + .addPropertiesSupplier(this::getRequiredProperties) + .addResourceCustomizer(this::customizeResource); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); + } + } + + @Override + public int order() { + return Integer.MAX_VALUE - 1; + } + + // Adds authorization headers to the calls made by the OtlpGrpcSpanExporter and + // OtlpHttpSpanExporter. + private SpanExporter addAuthorizationHeaders( + SpanExporter exporter, GoogleCredentials credentials) { + if (exporter instanceof OtlpHttpSpanExporter) { + try { + credentials.refreshIfExpired(); + OtlpHttpSpanExporterBuilder builder = + ((OtlpHttpSpanExporter) exporter) + .toBuilder() + .addHeader(QUOTA_USER_PROJECT_HEADER, credentials.getQuotaProjectId()) + .addHeader( + "Authorization", "Bearer " + credentials.getAccessToken().getTokenValue()); + + return builder.build(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e); + } + } else if (exporter instanceof OtlpGrpcSpanExporter) { + try { + credentials.refreshIfExpired(); + OtlpGrpcSpanExporterBuilder builder = + ((OtlpGrpcSpanExporter) exporter) + .toBuilder() + .addHeader(QUOTA_USER_PROJECT_HEADER, credentials.getQuotaProjectId()) + .addHeader( + "Authorization", "Bearer " + credentials.getAccessToken().getTokenValue()); + return builder.build(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e); + } + } + return exporter; + } + + // Sets the required properties that are essential for exporting OTLP data to GCP. + private Map getRequiredProperties() { + Map properties = new HashMap<>(); + properties.put( + "otel.exporter.otlp.endpoint", + String.format( + GCP_OTLP_ENDPOINT_STUB, ConfigurableOption.GOOGLE_CLOUD_REGION.getConfiguredValue())); + properties.put("otel.exporter.otlp.insecure", "false"); + properties.put("otel.resource.providers.gcp.enabled", "true"); + return properties; + } + + // Updates the current resource with the attributes required for ingesting OTLP data on GCP. + private Resource customizeResource(Resource resource, ConfigProperties configProperties) { + Resource res = + Resource.create( + Attributes.of( + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue())); + return resource.merge(res); + } +} diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GoogleAuthException.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GoogleAuthException.java new file mode 100644 index 00000000..5e15fc60 --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GoogleAuthException.java @@ -0,0 +1,62 @@ +/* + * 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; + +/** + * An unchecked exception indicating a failure during Google authentication. This exception is + * thrown when there are issues with retrieving or refreshing Google Application Default Credentials + * (ADC). + */ +public class GoogleAuthException extends RuntimeException { + + /** + * Constructs a new {@code GoogleAuthException} with the specified reason and cause. + * + * @param reason the reason for the authentication failure. + * @param cause the underlying cause of the exception (e.g., an IOException). + */ + GoogleAuthException(Reason reason, Throwable cause) { + super(reason.message, cause); + } + + /** Enumerates the possible reasons for a Google authentication failure. */ + enum Reason { + /** Indicates a failure to retrieve Google Application Default Credentials. */ + FAILED_ADC_RETRIEVAL("Unable to retrieve Google Application Default Credentials."), + /** Indicates a failure to retrieve Google Application Default Credentials. */ + FAILED_ADC_REFRESH("Unable to refresh Google Application Default Credentials."); + + private final String message; + + /** + * Constructs a new {@code Reason} with the specified message. + * + * @param message the message describing the reason. + */ + Reason(String message) { + this.message = message; + } + + /** + * Returns the message associated with this reason. + * + * @return the message describing the reason. + */ + public String getMessage() { + return message; + } + } +} diff --git a/settings.gradle b/settings.gradle index 8cc35cdc..2b283d02 100644 --- a/settings.gradle +++ b/settings.gradle @@ -43,6 +43,7 @@ include ":examples-spring-sleuth" include ":examples-spring" include ":propagators-gcp" include ":shared-resourcemapping" +include ":gcp-auth-extension" def javaVersion = Jvm.current().javaVersion @@ -107,3 +108,6 @@ project(':examples-spring').projectDir = project(':examples-otlpmetrics-function').projectDir = "$rootDir/examples/otlpmetrics-function" as File + +project(':gcp-auth-extension').projectDir = + "$rootDir/javaagent-extensions/gcp-auth" as File From 34ef8b862d733f0ae9a9dd4ae858b4e972cf5662 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sun, 15 Dec 2024 19:11:44 +0000 Subject: [PATCH 02/17] Add README for auth extension --- javaagent-extensions/gcp-auth/README.md | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 javaagent-extensions/gcp-auth/README.md diff --git a/javaagent-extensions/gcp-auth/README.md b/javaagent-extensions/gcp-auth/README.md new file mode 100644 index 00000000..5c79346d --- /dev/null +++ b/javaagent-extensions/gcp-auth/README.md @@ -0,0 +1,69 @@ +# Google Cloud Authentication Extension for OpenTelemetry Java Agent + +The Google Cloud Auth Extension allows the users to export telemetry from their applications auto-instrumented using the OpenTelemetry Java Agent to Google Cloud using the built-in OTLP exporters. +The extension takes care of the necessary configuration required to authenticate to GCP to successfully export telemetry. + +## Prerequisites + +### Ensure the presence of Google Cloud Credentials on your machine/environment + +```shell +gcloud auth application-default login +``` +Executing this command will save your application credentials to default path which will depend on the type of machine - +- Linux, macOS: `$HOME/.config/gcloud/application_default_credentials.json` +- Windows: `%APPDATA%\gcloud\application_default_credentials.json` + +**NOTE: This method of authentication is not recommended for production environments.** + +Next, export the credentials to `GOOGLE_APPLICATION_CREDENTIALS` environment variable - + +For Linux & MacOS: +```shell +export GOOGLE_APPLICATION_CREDENTIALS=$HOME/.config/gcloud/application_default_credentials.json +``` + +These credentials are built-in running in a Google App Engine, Google Cloud Shell or Google Compute Engine environment. + +### Configuring the extension + +The extension can be configured either by environment variables or system properties. + +Here is a list of configurable options for the extension: + + - `GOOGLE_CLOUD_PROJECT`: Environment variable that represents the Google Cloud Project ID to which the telemetry needs to be exported. + - Can also be configured using `google.cloud.project` system property. + - `GOOGLE_CLOUD_REGION`: Environment variable that represents your preferred Google Cloud Region to which the telemetry needs to be exported. + - Can also be configured using `google.cloud.region` system property. + +It is **mandatory** to configure all options for the extension, not doing so would lead to a `ConfigurationException`. + +## Usage + +The OpenTelemetry Java Agent Extension can be easily added to any Java application by modifying the startup command to the application. +For more information on Extensions, see the [documentation here](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/examples/extension/README.md). + +Below is a snippet showing how to add the extension to a Java application using the Gradle build system. + +```gradle +// Specify OpenTelemetry Autoinstrumentation Java Agent Path. +def otelAgentPath = +// Specify the path for Google Cloud Authentication Extension for the Java Agent. +def extensionPath = +def googleCloudProjectId = +def googleCloudRegion = + +application { + ... + "-javaagent:${otelAgentPath}", + "-Dotel.javaagent.extensions=${extensionPath}", + // Configure the GCP Auth extension using system properties. + // This can also be configured using environment variables. + "-Dgoogle.cloud.project=${googleCloudProjectId}", + "-Dgoogle.cloud.region=${googleCloudRegion}", + // Configure auto instrumentation. + '-Dotel.java.global-autoconfigure.enabled=true', + '-Dotel.traces.exporter=otlp', + '-Dotel.metrics.exporter=otlp', +} +``` From 867147e1cc44fd4418cf39367c75d4c089404d55 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Sun, 15 Dec 2024 20:42:36 +0000 Subject: [PATCH 03/17] Udpate build.gradle --- javaagent-extensions/gcp-auth/build.gradle | 14 +++++++++----- javaagent-extensions/gcp-auth/gradle.properties | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 javaagent-extensions/gcp-auth/gradle.properties diff --git a/javaagent-extensions/gcp-auth/build.gradle b/javaagent-extensions/gcp-auth/build.gradle index 7e5cbf41..061591e7 100644 --- a/javaagent-extensions/gcp-auth/build.gradle +++ b/javaagent-extensions/gcp-auth/build.gradle @@ -34,12 +34,16 @@ jar { } dependencies { - api(libraries.auto_service_annotations) annotationProcessor(libraries.auto_service) - implementation(libraries.opentelemetry_api) - implementation(libraries.opentelemetry_otlp_exporter) - implementation(libraries.opentelemetry_sdk_autoconf) - implementation(libraries.opentelemetry_autoconfigure_spi) + // We use `compileOnly` dependency because during runtime all necessary classes are provided by javaagent itself. + compileOnly(libraries.auto_service_annotations) + compileOnly(libraries.opentelemetry_api) + compileOnly(libraries.opentelemetry_otlp_exporter) + compileOnly(libraries.opentelemetry_sdk_autoconf) + compileOnly(libraries.opentelemetry_autoconfigure_spi) + + // Only dependencies added to `implementation` configuration will be picked up by Shadow plugin + // and added to the resulting jar for our extension's distribution. implementation(libraries.google_auth) } diff --git a/javaagent-extensions/gcp-auth/gradle.properties b/javaagent-extensions/gcp-auth/gradle.properties new file mode 100644 index 00000000..a88514b4 --- /dev/null +++ b/javaagent-extensions/gcp-auth/gradle.properties @@ -0,0 +1 @@ +release.enabled=false From aa7e503ac15f28691381e7029c464805657998c4 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Mon, 16 Dec 2024 20:13:36 +0000 Subject: [PATCH 04/17] Add end-to-end smoketest --- javaagent-extensions/gcp-auth/build.gradle | 49 +++++++++- .../auth/ExtensionIntegrationTest.java | 91 +++++++++++++++++++ .../auth/testapp/InstrumentedServer.java | 55 +++++++++++ .../extension/auth/testapp/TestHandler.java | 43 +++++++++ 4 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java create mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java create mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java diff --git a/javaagent-extensions/gcp-auth/build.gradle b/javaagent-extensions/gcp-auth/build.gradle index 061591e7..3f6c9f7b 100644 --- a/javaagent-extensions/gcp-auth/build.gradle +++ b/javaagent-extensions/gcp-auth/build.gradle @@ -14,6 +14,7 @@ * limitations under the License. */ plugins { + id 'java' id 'java-library' id 'com.github.johnrengelman.shadow' } @@ -25,7 +26,7 @@ tasks { } shadowJar { - archiveClassifier.set("") + archiveFileName.set("gcp-auth-extension.jar") } jar { @@ -33,6 +34,12 @@ jar { enabled = false } +// For auto-instrumentation agent, used for running integration test +configurations { + agent + includedDependencies +} + dependencies { annotationProcessor(libraries.auto_service) // We use `compileOnly` dependency because during runtime all necessary classes are provided by javaagent itself. @@ -45,8 +52,48 @@ dependencies { // Only dependencies added to `implementation` configuration will be picked up by Shadow plugin // and added to the resulting jar for our extension's distribution. implementation(libraries.google_auth) + + // test dependencies + // OTel instrumentation used in the sample app + testImplementation platform(libraries.opentelemetry_instrumetation_bom) + testImplementation('io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations') + // logging libs - need to be included in the test app + includedDependencies libraries.slf4j + includedDependencies testLibraries.slf4j_simple + // testing library and tools + testImplementation("org.testcontainers:testcontainers:1.20.4") + testImplementation(testLibraries.junit5) + testRuntimeOnly(testLibraries.junit5_runtime) + + agent agentLibraries.agent +} + +// task to copy and rename the Java Auto-Instrumentation Agent into 'libs' folder +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 'shadowJar' + archiveFileName.set("auto-instrumented-test-server.jar") + from(sourceSets.test.output) + // include dependencies in the JAR which are required during runtime + from { + configurations.includedDependencies.collect { + it.isDirectory() ? it : zipTree(it) + } + } + manifest { + attributes.put('Main-Class', 'com.google.cloud.opentelemetry.extension.auth.testapp.InstrumentedServer') + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE } test { + dependsOn 'BuildTestApp' useJUnitPlatform() } \ No newline at end of file diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java new file mode 100644 index 00000000..eb03915a --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java @@ -0,0 +1,91 @@ +/* + * 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 org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class ExtensionIntegrationTest { + + private static final String TEST_SERVER_PATH = "/doWork"; + private static GenericContainer applicationContainer; + private static Integer applicationPort; + + @BeforeAll + static void setup() { + String testAppJarPath = + new File("./build/libs/auto-instrumented-test-server.jar").getAbsolutePath(); + String javaAgentJarPath = new File("./build/libs/otel-agent.jar").getAbsolutePath(); + String authExtensionJarPath = new File("./build/libs/gcp-auth-extension.jar").getAbsolutePath(); + DockerImageName dockerBaseImage = DockerImageName.parse("openjdk:17-jdk-slim"); + int preferredApplicationPort = 8000; + + applicationContainer = + new GenericContainer<>(dockerBaseImage) + .withExposedPorts(preferredApplicationPort) + .withCopyFileToContainer(MountableFile.forHostPath(testAppJarPath), "/test-app.jar") + .withCopyFileToContainer(MountableFile.forHostPath(javaAgentJarPath), "/agent.jar") + .withCopyFileToContainer( + MountableFile.forHostPath(authExtensionJarPath), "/auth-ext.jar") + .withCommand( + "java", + "-javaagent:/agent.jar", + "-Dotel.javaagent.extensions=/auth-ext.jar", + "-Dotel.java.global-autoconfigure.enabled=true", + "-Dgoogle.cloud.project=dummy-test-project", + "-Dgoogle.cloud.region=us-central1", + "-Dotel.traces.exporter=otlp,logging", + "-Dotel.metrics.exporter=none", + "-jar", + "/test-app.jar", + Integer.toString(preferredApplicationPort)) + .waitingFor(Wait.forLogMessage(".*Waiting for requests.*", 1)); + applicationContainer.start(); + applicationPort = applicationContainer.getMappedPort(preferredApplicationPort); + } + + @AfterAll + static void tearDown() { + if (applicationContainer != null) { + applicationContainer.stop(); + } + } + + @Test + public void testServerResponding() throws IOException, InterruptedException { + String url = + "http://" + applicationContainer.getHost() + ":" + applicationPort + TEST_SERVER_PATH; + System.out.println("URL: " + url); + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + } +} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java new file mode 100644 index 00000000..4e90e6ae --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java @@ -0,0 +1,55 @@ +/* + * 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.testapp; + +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.net.InetSocketAddress; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InstrumentedServer { + private static final Logger logger = LoggerFactory.getLogger(InstrumentedServer.class); + private static final int defaultPort = 8080; + // to run this from command line, execute `gradle run` + public static void main(String[] args) throws InterruptedException, IOException { + int port = parsePort(args); + logger.info("Starting the test server on {}", port); + + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/doWork", new TestHandler()); + server.setExecutor(null); // creates a default executor + server.start(); + logger.info("Waiting for requests"); + } + + private static int parsePort(String[] args) { + int port; + if (args.length > 0) { + try { + port = Integer.parseInt(args[0]); + if (port < 0 || port > 65535) { + throw new NumberFormatException("Port number must be between 0 and 65535"); + } + return port; + } catch (NumberFormatException e) { + logger.warn("Invalid port number provided: {}", args[0]); + logger.warn("Using default port: {}", defaultPort); + } + } + return defaultPort; + } +} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java new file mode 100644 index 00000000..63fcd88d --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java @@ -0,0 +1,43 @@ +/* + * 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.testapp; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Random; + +public class TestHandler implements HttpHandler { + private final Random random = new Random(); + + @Override + @WithSpan + public void handle(HttpExchange exchange) throws IOException { + int n = random.nextInt(100); + String response = n % 2 == 0 ? "Send Response A" : "Send Response B"; + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } +} From b767d98a2fcee43ef802748dd9823f48a5305ab8 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Mon, 16 Dec 2024 22:15:40 +0000 Subject: [PATCH 05/17] Set header suppliers to enable dynamic token refresh --- ...thAutoConfigurationCustomizerProvider.java | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java index aa8269b8..4c0d9ab2 100644 --- a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -93,36 +93,31 @@ public int order() { private SpanExporter addAuthorizationHeaders( SpanExporter exporter, GoogleCredentials credentials) { if (exporter instanceof OtlpHttpSpanExporter) { - try { - credentials.refreshIfExpired(); - OtlpHttpSpanExporterBuilder builder = - ((OtlpHttpSpanExporter) exporter) - .toBuilder() - .addHeader(QUOTA_USER_PROJECT_HEADER, credentials.getQuotaProjectId()) - .addHeader( - "Authorization", "Bearer " + credentials.getAccessToken().getTokenValue()); - - return builder.build(); - } catch (IOException e) { - throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e); - } + OtlpHttpSpanExporterBuilder builder = + ((OtlpHttpSpanExporter) exporter) + .toBuilder().setHeaders(() -> getRequiredHeaderMap(credentials)); + return builder.build(); } else if (exporter instanceof OtlpGrpcSpanExporter) { - try { - credentials.refreshIfExpired(); - OtlpGrpcSpanExporterBuilder builder = - ((OtlpGrpcSpanExporter) exporter) - .toBuilder() - .addHeader(QUOTA_USER_PROJECT_HEADER, credentials.getQuotaProjectId()) - .addHeader( - "Authorization", "Bearer " + credentials.getAccessToken().getTokenValue()); - return builder.build(); - } catch (IOException e) { - throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e); - } + OtlpGrpcSpanExporterBuilder builder = + ((OtlpGrpcSpanExporter) exporter) + .toBuilder().setHeaders(() -> getRequiredHeaderMap(credentials)); + return builder.build(); } return exporter; } + private Map getRequiredHeaderMap(GoogleCredentials credentials) { + Map gcpHeaders = new HashMap<>(); + try { + credentials.refreshIfExpired(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e); + } + gcpHeaders.put(QUOTA_USER_PROJECT_HEADER, credentials.getQuotaProjectId()); + gcpHeaders.put("Authorization", "Bearer " + credentials.getAccessToken().getTokenValue()); + return gcpHeaders; + } + // Sets the required properties that are essential for exporting OTLP data to GCP. private Map getRequiredProperties() { Map properties = new HashMap<>(); From efd47efcb78796c565467bd2e1281e18bfbf5523 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Mon, 16 Dec 2024 22:52:10 +0000 Subject: [PATCH 06/17] Remove hardcoded OTLP endpoint from extension --- javaagent-extensions/gcp-auth/README.md | 8 ++++---- .../opentelemetry/extension/auth/ConfigurableOption.java | 8 +------- .../auth/GcpAuthAutoConfigurationCustomizerProvider.java | 7 ------- 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/javaagent-extensions/gcp-auth/README.md b/javaagent-extensions/gcp-auth/README.md index 5c79346d..c441a9e8 100644 --- a/javaagent-extensions/gcp-auth/README.md +++ b/javaagent-extensions/gcp-auth/README.md @@ -33,10 +33,8 @@ Here is a list of configurable options for the extension: - `GOOGLE_CLOUD_PROJECT`: Environment variable that represents the Google Cloud Project ID to which the telemetry needs to be exported. - Can also be configured using `google.cloud.project` system property. - - `GOOGLE_CLOUD_REGION`: Environment variable that represents your preferred Google Cloud Region to which the telemetry needs to be exported. - - Can also be configured using `google.cloud.region` system property. -It is **mandatory** to configure all options for the extension, not doing so would lead to a `ConfigurationException`. +It is **mandatory** to configure this option for the extension, not doing so would lead to a `ConfigurationException`. ## Usage @@ -52,6 +50,7 @@ def otelAgentPath = def extensionPath = def googleCloudProjectId = def googleCloudRegion = +def googleOtlpEndpoint = application { ... @@ -62,8 +61,9 @@ application { "-Dgoogle.cloud.project=${googleCloudProjectId}", "-Dgoogle.cloud.region=${googleCloudRegion}", // Configure auto instrumentation. + "-Dotel.exporter.otlp.traces.endpoint=${googleOtlpEndpoint}", '-Dotel.java.global-autoconfigure.enabled=true', '-Dotel.traces.exporter=otlp', - '-Dotel.metrics.exporter=otlp', + '-Dotel.metrics.exporter=logging', } ``` diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java index 04d3bc2a..544f9d22 100644 --- a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java @@ -27,13 +27,7 @@ public enum ConfigurableOption { * Represents the Google Cloud Project ID option. Can be configured using the environment variable * `GOOGLE_CLOUD_PROJECT` or the system property `google.cloud.project`. */ - GOOGLE_CLOUD_PROJECT("Google Cloud Project ID"), - - /** - * Represents the Google Cloud Region option. Can be configured using the environment variable - * `GOOGLE_CLOUD_REGION` or the system property `google.cloud.region`. - */ - GOOGLE_CLOUD_REGION("Google Cloud Region"); + GOOGLE_CLOUD_PROJECT("Google Cloud Project ID"); private static final String OPTION_NOT_CONFIGURED_MSG = "GCP Authentication Extension not configured properly: %s not configured. Configure it by exporting environment variable %s or system property %s"; diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java index 4c0d9ab2..3e8c0589 100644 --- a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -50,8 +50,6 @@ public class GcpAuthAutoConfigurationCustomizerProvider implements AutoConfigurationCustomizerProvider { - private static final String GCP_OTLP_ENDPOINT_STUB = - "https://staging-%s-telemetry.sandbox.googleapis.com:443"; private static final String QUOTA_USER_PROJECT_HEADER = "X-Goog-User-Project"; private static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id"; @@ -121,11 +119,6 @@ private Map getRequiredHeaderMap(GoogleCredentials credentials) // Sets the required properties that are essential for exporting OTLP data to GCP. private Map getRequiredProperties() { Map properties = new HashMap<>(); - properties.put( - "otel.exporter.otlp.endpoint", - String.format( - GCP_OTLP_ENDPOINT_STUB, ConfigurableOption.GOOGLE_CLOUD_REGION.getConfiguredValue())); - properties.put("otel.exporter.otlp.insecure", "false"); properties.put("otel.resource.providers.gcp.enabled", "true"); return properties; } From 00d4dab80cc2a7c6e75209ac8c4649c39e821f7f Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Tue, 17 Dec 2024 00:48:53 +0000 Subject: [PATCH 07/17] Enhance Integration Test - WIP --- javaagent-extensions/gcp-auth/build.gradle | 21 ++++- .../auth/ExtensionIntegrationTest.java | 56 +++++++---- .../auth/testapp/InstrumentedServer.java | 5 +- .../testbackend/DummyOTelHttpEndpoint.java | 93 +++++++++++++++++++ 4 files changed, 157 insertions(+), 18 deletions(-) create mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java diff --git a/javaagent-extensions/gcp-auth/build.gradle b/javaagent-extensions/gcp-auth/build.gradle index 3f6c9f7b..2cea4a7d 100644 --- a/javaagent-extensions/gcp-auth/build.gradle +++ b/javaagent-extensions/gcp-auth/build.gradle @@ -58,6 +58,8 @@ dependencies { testImplementation platform(libraries.opentelemetry_instrumetation_bom) testImplementation('io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations') // logging libs - need to be included in the test app + testImplementation libraries.slf4j + testRuntimeOnly testLibraries.slf4j_simple includedDependencies libraries.slf4j includedDependencies testLibraries.slf4j_simple // testing library and tools @@ -79,6 +81,7 @@ tasks.register('CopyAgent', Copy) { tasks.register('BuildTestApp', org.gradle.jvm.tasks.Jar) { dependsOn 'CopyAgent' dependsOn 'shadowJar' + dependsOn 'BuildDummyBackend' archiveFileName.set("auto-instrumented-test-server.jar") from(sourceSets.test.output) // include dependencies in the JAR which are required during runtime @@ -93,7 +96,23 @@ tasks.register('BuildTestApp', org.gradle.jvm.tasks.Jar) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } +tasks.register('BuildDummyBackend', org.gradle.jvm.tasks.Jar) { + archiveFileName.set("dummy-otlp-backend.jar") + from(sourceSets.test.output) + // include dependencies in the JAR which are required during runtime + from { + configurations.includedDependencies.collect { + it.isDirectory() ? it : zipTree(it) + } + } + manifest { + attributes.put('Main-Class', 'com.google.cloud.opentelemetry.extension.auth.testbackend.DummyOTelHttpEndpoint') + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + test { dependsOn 'BuildTestApp' + dependsOn 'BuildDummyBackend' useJUnitPlatform() -} \ No newline at end of file +} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java index eb03915a..3551a026 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java @@ -15,7 +15,10 @@ */ package com.google.cloud.opentelemetry.extension.auth; +import static com.google.cloud.opentelemetry.extension.auth.testbackend.DummyOTelHttpEndpoint.HEADER_VERIFIED; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.io.IOException; @@ -23,10 +26,15 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; @@ -37,34 +45,35 @@ public class ExtensionIntegrationTest { private static GenericContainer applicationContainer; private static Integer applicationPort; + private static final Logger logger = LoggerFactory.getLogger(ExtensionIntegrationTest.class); + @BeforeAll static void setup() { String testAppJarPath = new File("./build/libs/auto-instrumented-test-server.jar").getAbsolutePath(); + String dummyBackendJarPath = new File("./build/libs/dummy-otlp-backend.jar").getAbsolutePath(); String javaAgentJarPath = new File("./build/libs/otel-agent.jar").getAbsolutePath(); String authExtensionJarPath = new File("./build/libs/gcp-auth-extension.jar").getAbsolutePath(); + DockerImageName dockerBaseImage = DockerImageName.parse("openjdk:17-jdk-slim"); int preferredApplicationPort = 8000; + String runJavaTestApp = + "java -javaagent:/agent.jar -Dotel.javaagent.extensions=/auth-ext.jar -Dotel.java.global-autoconfigure.enabled=true -Dgoogle.cloud.project=dummy-test-project -Dotel.exporter.otlp.endpoint=http://localhost:4318/ -Dotel.traces.exporter=otlp,logging -Dotel.metrics.exporter=none -jar /test-app.jar " + + preferredApplicationPort; + String runOtelBackend = "java -jar /test-backend.jar &"; + applicationContainer = new GenericContainer<>(dockerBaseImage) .withExposedPorts(preferredApplicationPort) + .withCopyFileToContainer( + MountableFile.forHostPath(dummyBackendJarPath), "/test-backend.jar") .withCopyFileToContainer(MountableFile.forHostPath(testAppJarPath), "/test-app.jar") .withCopyFileToContainer(MountableFile.forHostPath(javaAgentJarPath), "/agent.jar") .withCopyFileToContainer( MountableFile.forHostPath(authExtensionJarPath), "/auth-ext.jar") - .withCommand( - "java", - "-javaagent:/agent.jar", - "-Dotel.javaagent.extensions=/auth-ext.jar", - "-Dotel.java.global-autoconfigure.enabled=true", - "-Dgoogle.cloud.project=dummy-test-project", - "-Dgoogle.cloud.region=us-central1", - "-Dotel.traces.exporter=otlp,logging", - "-Dotel.metrics.exporter=none", - "-jar", - "/test-app.jar", - Integer.toString(preferredApplicationPort)) + .withCommand("sh", "-c", runOtelBackend + runJavaTestApp) + .withLogConsumer(new Slf4jLogConsumer(logger)) .waitingFor(Wait.forLogMessage(".*Waiting for requests.*", 1)); applicationContainer.start(); applicationPort = applicationContainer.getMappedPort(preferredApplicationPort); @@ -78,14 +87,29 @@ static void tearDown() { } @Test - public void testServerResponding() throws IOException, InterruptedException { + public void testSampleAppResponding() { + AtomicReference> response = new AtomicReference<>(); + assertDoesNotThrow(() -> response.set(sendRequestToSampleApp())); + assertEquals(200, response.get().statusCode()); + } + + @Test + public void testExtensionAttachesCorrectHeaders() throws IOException, InterruptedException { + HttpResponse response = sendRequestToSampleApp(); + assertEquals(200, response.statusCode()); + + applicationContainer.waitingFor(Wait.forLogMessage(".*Received trace data.*", 1)); + + String logs = applicationContainer.getLogs(); + assertTrue(logs.contains(HEADER_VERIFIED)); + } + + private HttpResponse sendRequestToSampleApp() throws IOException, InterruptedException { String url = "http://" + applicationContainer.getHost() + ":" + applicationPort + TEST_SERVER_PATH; - System.out.println("URL: " + url); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - assertEquals(200, response.statusCode()); + return client.send(request, BodyHandlers.ofString()); } } diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java index 4e90e6ae..b023b619 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java @@ -15,6 +15,8 @@ */ package com.google.cloud.opentelemetry.extension.auth.testapp; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.net.InetSocketAddress; @@ -25,12 +27,13 @@ public class InstrumentedServer { private static final Logger logger = LoggerFactory.getLogger(InstrumentedServer.class); private static final int defaultPort = 8080; // to run this from command line, execute `gradle run` - public static void main(String[] args) throws InterruptedException, IOException { + public static void main(String[] args) throws IOException { int port = parsePort(args); logger.info("Starting the test server on {}", port); HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); server.createContext("/doWork", new TestHandler()); + server.createContext("/stop", exchange -> server.stop(0)); server.setExecutor(null); // creates a default executor server.start(); logger.info("Waiting for requests"); diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java new file mode 100644 index 00000000..9da0fa3f --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java @@ -0,0 +1,93 @@ +/* + * 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.testbackend; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.Executors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DummyOTelHttpEndpoint { + public static final String HEADER_VERIFIED = "OTLP Export Headers verified"; + public static final String HEADER_NOT_VERIFIED = "OTLP Export Headers not verified"; + private static final Logger logger = LoggerFactory.getLogger(DummyOTelHttpEndpoint.class); + + public static void main(String[] args) throws IOException { + int port = 4318; // Use a different port than gRPC (e.g., 4318) + + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/v1/traces", new TraceHandler()); // Handle traces + server.setExecutor(Executors.newFixedThreadPool(4)); // Thread pool for handling requests + server.start(); + + logger.info("Dummy OpenTelemetry HTTP endpoint started on port {}", port); + logger.info("Send POST requests to /v1/traces"); + } + + static class TraceHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if ("POST".equals(exchange.getRequestMethod())) { + String requestBody = + new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + logger.info("Received trace data:\n" + requestBody); + String response = "Trace data received"; + if (verifyRequestHeaders(exchange)) { + exchange.sendResponseHeaders(200, response.getBytes().length); + logger.info(HEADER_VERIFIED); + } else { + exchange.sendResponseHeaders(400, response.getBytes().length); + logger.info(HEADER_NOT_VERIFIED); + } + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } else { + sendMethodNotAllowed(exchange); + } + } + + private boolean verifyRequestHeaders(HttpExchange exchange) { + Headers requestHeaders = exchange.getRequestHeaders(); + List quotaUserProjectHeader = requestHeaders.get("X-Goog-User-Project"); + List authHeader = requestHeaders.get("Authorization"); + if (quotaUserProjectHeader.size() != 1 + || quotaUserProjectHeader.get(0).equals("dummy-test-project")) { + return false; + } + if (authHeader.size() != 1 || !authHeader.get(0).startsWith("Bearer")) { + return false; + } + return true; + } + } + + private static void sendMethodNotAllowed(HttpExchange exchange) throws IOException { + String response = "Method Not Allowed"; + exchange.sendResponseHeaders(405, response.getBytes().length); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } +} From c6049e35802b4d47554521031c4aab60b9f02ec6 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Tue, 17 Dec 2024 19:50:30 +0000 Subject: [PATCH 08/17] Fix integration test The com.sun.net.httpserver does not seem to be auto-instrumented by the Java Agent and @WithSpan was not producing a trace span for the handler. This commit changes the test app to use Apache HttpServer client which is auto-instrumented by the agent. --- javaagent-extensions/gcp-auth/build.gradle | 43 ++---- .../auth/ExtensionIntegrationTest.java | 126 +++++++----------- .../auth/testapp/InstrumentedServer.java | 58 -------- .../auth/testapp/InstrumentedTestApp.java | 92 +++++++++++++ .../extension/auth/testapp/TestHandler.java | 3 +- .../testbackend/DummyOTelHttpEndpoint.java | 65 ++++----- 6 files changed, 172 insertions(+), 215 deletions(-) delete mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java create mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java diff --git a/javaagent-extensions/gcp-auth/build.gradle b/javaagent-extensions/gcp-auth/build.gradle index 2cea4a7d..f7e5d36e 100644 --- a/javaagent-extensions/gcp-auth/build.gradle +++ b/javaagent-extensions/gcp-auth/build.gradle @@ -34,10 +34,10 @@ jar { enabled = false } -// For auto-instrumentation agent, used for running integration test +// Custom configurations used to facilitate running the integration test configurations { agent - includedDependencies + includeDeps } dependencies { @@ -54,20 +54,12 @@ dependencies { implementation(libraries.google_auth) // test dependencies - // OTel instrumentation used in the sample app - testImplementation platform(libraries.opentelemetry_instrumetation_bom) - testImplementation('io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations') - // logging libs - need to be included in the test app - testImplementation libraries.slf4j - testRuntimeOnly testLibraries.slf4j_simple - includedDependencies libraries.slf4j - includedDependencies testLibraries.slf4j_simple - // testing library and tools - testImplementation("org.testcontainers:testcontainers:1.20.4") testImplementation(testLibraries.junit5) testRuntimeOnly(testLibraries.junit5_runtime) - + // OTel instrumentation used in the sample app to facilitate integration testing agent agentLibraries.agent + testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' + includeDeps 'org.apache.httpcomponents:httpclient:4.5.14' } // task to copy and rename the Java Auto-Instrumentation Agent into 'libs' folder @@ -81,38 +73,23 @@ tasks.register('CopyAgent', Copy) { tasks.register('BuildTestApp', org.gradle.jvm.tasks.Jar) { dependsOn 'CopyAgent' dependsOn 'shadowJar' - dependsOn 'BuildDummyBackend' + archiveFileName.set("auto-instrumented-test-server.jar") - from(sourceSets.test.output) - // include dependencies in the JAR which are required during runtime - from { - configurations.includedDependencies.collect { - it.isDirectory() ? it : zipTree(it) - } + from(sourceSets.test.output) { + include 'com/google/cloud/opentelemetry/extension/auth/testapp/**' } - manifest { - attributes.put('Main-Class', 'com.google.cloud.opentelemetry.extension.auth.testapp.InstrumentedServer') - } - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} - -tasks.register('BuildDummyBackend', org.gradle.jvm.tasks.Jar) { - archiveFileName.set("dummy-otlp-backend.jar") - from(sourceSets.test.output) - // include dependencies in the JAR which are required during runtime from { - configurations.includedDependencies.collect { + configurations.includeDeps.collect { it.isDirectory() ? it : zipTree(it) } } manifest { - attributes.put('Main-Class', 'com.google.cloud.opentelemetry.extension.auth.testbackend.DummyOTelHttpEndpoint') + attributes.put('Main-Class', 'com.google.cloud.opentelemetry.extension.auth.testapp.InstrumentedTestApp') } duplicatesStrategy = DuplicatesStrategy.EXCLUDE } test { dependsOn 'BuildTestApp' - dependsOn 'BuildDummyBackend' useJUnitPlatform() } diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java index 3551a026..9a5d07fa 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java @@ -15,101 +15,65 @@ */ package com.google.cloud.opentelemetry.extension.auth; -import static com.google.cloud.opentelemetry.extension.auth.testbackend.DummyOTelHttpEndpoint.HEADER_VERIFIED; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.cloud.opentelemetry.extension.auth.testbackend.DummyOTelHttpEndpoint; +import com.sun.net.httpserver.HttpServer; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandlers; -import java.util.concurrent.atomic.AtomicReference; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import java.io.InputStreamReader; +import java.lang.ProcessBuilder.Redirect; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.DockerImageName; -import org.testcontainers.utility.MountableFile; public class ExtensionIntegrationTest { - private static final String TEST_SERVER_PATH = "/doWork"; - private static GenericContainer applicationContainer; - private static Integer applicationPort; - - private static final Logger logger = LoggerFactory.getLogger(ExtensionIntegrationTest.class); - - @BeforeAll - static void setup() { + @Test + public void smokeTest() throws IOException, InterruptedException { String testAppJarPath = new File("./build/libs/auto-instrumented-test-server.jar").getAbsolutePath(); - String dummyBackendJarPath = new File("./build/libs/dummy-otlp-backend.jar").getAbsolutePath(); + String javaAgentJarPath = new File("./build/libs/otel-agent.jar").getAbsolutePath(); String authExtensionJarPath = new File("./build/libs/gcp-auth-extension.jar").getAbsolutePath(); - DockerImageName dockerBaseImage = DockerImageName.parse("openjdk:17-jdk-slim"); - int preferredApplicationPort = 8000; - - String runJavaTestApp = - "java -javaagent:/agent.jar -Dotel.javaagent.extensions=/auth-ext.jar -Dotel.java.global-autoconfigure.enabled=true -Dgoogle.cloud.project=dummy-test-project -Dotel.exporter.otlp.endpoint=http://localhost:4318/ -Dotel.traces.exporter=otlp,logging -Dotel.metrics.exporter=none -jar /test-app.jar " - + preferredApplicationPort; - String runOtelBackend = "java -jar /test-backend.jar &"; - - applicationContainer = - new GenericContainer<>(dockerBaseImage) - .withExposedPorts(preferredApplicationPort) - .withCopyFileToContainer( - MountableFile.forHostPath(dummyBackendJarPath), "/test-backend.jar") - .withCopyFileToContainer(MountableFile.forHostPath(testAppJarPath), "/test-app.jar") - .withCopyFileToContainer(MountableFile.forHostPath(javaAgentJarPath), "/agent.jar") - .withCopyFileToContainer( - MountableFile.forHostPath(authExtensionJarPath), "/auth-ext.jar") - .withCommand("sh", "-c", runOtelBackend + runJavaTestApp) - .withLogConsumer(new Slf4jLogConsumer(logger)) - .waitingFor(Wait.forLogMessage(".*Waiting for requests.*", 1)); - applicationContainer.start(); - applicationPort = applicationContainer.getMappedPort(preferredApplicationPort); - } - - @AfterAll - static void tearDown() { - if (applicationContainer != null) { - applicationContainer.stop(); + HttpServer backendServer; + try { + backendServer = DummyOTelHttpEndpoint.createTestServer(); + } catch (IOException e) { + throw new RuntimeException(e); } - } - - @Test - public void testSampleAppResponding() { - AtomicReference> response = new AtomicReference<>(); - assertDoesNotThrow(() -> response.set(sendRequestToSampleApp())); - assertEquals(200, response.get().statusCode()); - } - - @Test - public void testExtensionAttachesCorrectHeaders() throws IOException, InterruptedException { - HttpResponse response = sendRequestToSampleApp(); - assertEquals(200, response.statusCode()); - - applicationContainer.waitingFor(Wait.forLogMessage(".*Received trace data.*", 1)); - - String logs = applicationContainer.getLogs(); - assertTrue(logs.contains(HEADER_VERIFIED)); - } - - private HttpResponse sendRequestToSampleApp() throws IOException, InterruptedException { - String url = - "http://" + applicationContainer.getHost() + ":" + applicationPort + TEST_SERVER_PATH; - HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build(); - - return client.send(request, BodyHandlers.ofString()); + backendServer.start(); + Process p = + new ProcessBuilder( + "java", + "-javaagent:" + javaAgentJarPath, + "-Dotel.javaagent.extensions=" + authExtensionJarPath, + "-Dotel.java.global-autoconfigure.enabled=true", + "-Dgoogle.cloud.project=dummy-test-project", + "-Dotel.exporter.otlp.endpoint=http://localhost:4318", + "-Dotel.exporter.otlp.insecure=true", + "-Dotel.traces.exporter=otlp,logging", + "-Dotel.metrics.exporter=none", + "-Dotel.javaagent.debug=false", + "-Dotel.exporter.otlp.protocol=http/protobuf", + "-jar", + testAppJarPath) + .redirectError( + Redirect.INHERIT) // Redirect stderr from this process to the current process + .start(); + p.waitFor(); // wait for the process running the test app to finish + + // flush logs from instrumented app process + BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); + reader.lines().forEach(System.out::println); + System.out.println("Instrumented app process finished"); + + backendServer.stop(0); // stop the mock HTTP server + + // assert on number of requests + assertEquals(1, DummyOTelHttpEndpoint.receivedRequests.size()); + // ensure headers were verified for all requests + DummyOTelHttpEndpoint.receivedRequests.forEach((request, verified) -> assertTrue(verified)); } } diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java deleted file mode 100644 index b023b619..00000000 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedServer.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.testapp; - -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; -import java.io.IOException; -import java.net.InetSocketAddress; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class InstrumentedServer { - private static final Logger logger = LoggerFactory.getLogger(InstrumentedServer.class); - private static final int defaultPort = 8080; - // to run this from command line, execute `gradle run` - public static void main(String[] args) throws IOException { - int port = parsePort(args); - logger.info("Starting the test server on {}", port); - - HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); - server.createContext("/doWork", new TestHandler()); - server.createContext("/stop", exchange -> server.stop(0)); - server.setExecutor(null); // creates a default executor - server.start(); - logger.info("Waiting for requests"); - } - - private static int parsePort(String[] args) { - int port; - if (args.length > 0) { - try { - port = Integer.parseInt(args[0]); - if (port < 0 || port > 65535) { - throw new NumberFormatException("Port number must be between 0 and 65535"); - } - return port; - } catch (NumberFormatException e) { - logger.warn("Invalid port number provided: {}", args[0]); - logger.warn("Using default port: {}", defaultPort); - } - } - return defaultPort; - } -} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java new file mode 100644 index 00000000..c926eb3f --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java @@ -0,0 +1,92 @@ +/* + * 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.testapp; + +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +public class InstrumentedTestApp { + private static final String serverUrl = "http://localhost:%d/%s"; + private static final int defaultPort = 8080; + + public static void main(String[] args) throws IOException, InterruptedException { + int port = parsePort(args); + + HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + server.createContext("/doWork", new TestHandler()); + server.createContext( + "/stop", + exchange -> { + String response = "Stopping Server"; + System.out.println(response); + exchange.sendResponseHeaders(200, response.length()); + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); + server.stop(0); + }); + server.setExecutor(null); // creates a default executor + server.start(); + System.out.println("Server ready"); + System.out.println("Sending request to do work ..."); + makeCall(String.format(serverUrl, port, "doWork")); + Thread.sleep(1000); + System.out.println("Sending request to stop server ..."); + makeCall(String.format(serverUrl, port, "stop")); + } + + // Make calls to the server using Apache HttpClient + private static void makeCall(String url) { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet request = new HttpGet(url); + try (CloseableHttpResponse response = httpClient.execute(request)) { + HttpEntity entity = response.getEntity(); + if (entity != null) { + String responseBody = EntityUtils.toString(entity); + System.out.println("Response Body: " + responseBody); + EntityUtils.consume(entity); + } + } + } catch (IOException e) { + System.err.println("Error making request: " + e.getMessage()); + } + } + + private static int parsePort(String[] args) { + int port; + if (args.length > 0) { + try { + port = Integer.parseInt(args[0]); + if (port < 0 || port > 65535) { + throw new NumberFormatException("Port number must be between 0 and 65535"); + } + return port; + } catch (NumberFormatException e) { + System.err.println("Invalid port number provided: " + args[0]); + System.err.println("Using default port: " + defaultPort); + } + } + return defaultPort; + } +} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java index 63fcd88d..e6918bd8 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java @@ -17,7 +17,6 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; -import io.opentelemetry.instrumentation.annotations.WithSpan; import java.io.IOException; import java.io.OutputStream; import java.util.Random; @@ -26,9 +25,9 @@ public class TestHandler implements HttpHandler { private final Random random = new Random(); @Override - @WithSpan public void handle(HttpExchange exchange) throws IOException { int n = random.nextInt(100); + System.out.println("handling"); String response = n % 2 == 0 ? "Send Response A" : "Send Response B"; try { Thread.sleep(1000); diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java index 9da0fa3f..af9f93f6 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java @@ -23,49 +23,43 @@ import java.io.OutputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.util.HashMap; import java.util.List; -import java.util.concurrent.Executors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.Map; public class DummyOTelHttpEndpoint { - public static final String HEADER_VERIFIED = "OTLP Export Headers verified"; - public static final String HEADER_NOT_VERIFIED = "OTLP Export Headers not verified"; - private static final Logger logger = LoggerFactory.getLogger(DummyOTelHttpEndpoint.class); + // Map to keep track of incoming requests and if the headers were verified for the request + public static Map receivedRequests = new HashMap<>(); - public static void main(String[] args) throws IOException { + public static HttpServer createTestServer() throws IOException { int port = 4318; // Use a different port than gRPC (e.g., 4318) HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); server.createContext("/v1/traces", new TraceHandler()); // Handle traces - server.setExecutor(Executors.newFixedThreadPool(4)); // Thread pool for handling requests - server.start(); - - logger.info("Dummy OpenTelemetry HTTP endpoint started on port {}", port); - logger.info("Send POST requests to /v1/traces"); + server.setExecutor(null); // Use default thread pool + return server; } - static class TraceHandler implements HttpHandler { + // Dummy trace handler + private static class TraceHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { - if ("POST".equals(exchange.getRequestMethod())) { - String requestBody = - new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); - logger.info("Received trace data:\n" + requestBody); - String response = "Trace data received"; - if (verifyRequestHeaders(exchange)) { - exchange.sendResponseHeaders(200, response.getBytes().length); - logger.info(HEADER_VERIFIED); - } else { - exchange.sendResponseHeaders(400, response.getBytes().length); - logger.info(HEADER_NOT_VERIFIED); - } - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); + System.out.println("handling exchange from " + exchange.getRemoteAddress()); + String requestBody = + new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + System.out.println("handling trace export " + requestBody); + + String response = "Trace data received"; + if (verifyRequestHeaders(exchange)) { + receivedRequests.put(exchange.toString(), true); + exchange.sendResponseHeaders(200, response.getBytes().length); } else { - sendMethodNotAllowed(exchange); + receivedRequests.put(exchange.toString(), false); + exchange.sendResponseHeaders(400, response.getBytes().length); } + OutputStream os = exchange.getResponseBody(); + os.write(response.getBytes()); + os.close(); } private boolean verifyRequestHeaders(HttpExchange exchange) { @@ -76,18 +70,7 @@ private boolean verifyRequestHeaders(HttpExchange exchange) { || quotaUserProjectHeader.get(0).equals("dummy-test-project")) { return false; } - if (authHeader.size() != 1 || !authHeader.get(0).startsWith("Bearer")) { - return false; - } - return true; + return authHeader.size() == 1 && authHeader.get(0).startsWith("Bearer"); } } - - private static void sendMethodNotAllowed(HttpExchange exchange) throws IOException { - String response = "Method Not Allowed"; - exchange.sendResponseHeaders(405, response.getBytes().length); - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); - } } From eef7cb7ed0ccf37011091afbf17519d3753458b7 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Wed, 18 Dec 2024 23:51:06 +0000 Subject: [PATCH 09/17] Remove the use of fixed ports --- .../auth/ExtensionIntegrationTest.java | 10 ++++--- .../auth/testapp/InstrumentedTestApp.java | 26 +++---------------- .../testbackend/DummyOTelHttpEndpoint.java | 4 +-- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java index 9a5d07fa..f2d82693 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java @@ -44,17 +44,21 @@ public void smokeTest() throws IOException, InterruptedException { throw new RuntimeException(e); } backendServer.start(); + int backendServerPort = backendServer.getAddress().getPort(); + System.out.println("Started OTLP HTTP Endpoint on localhost:" + backendServerPort); Process p = new ProcessBuilder( "java", "-javaagent:" + javaAgentJarPath, "-Dotel.javaagent.extensions=" + authExtensionJarPath, - "-Dotel.java.global-autoconfigure.enabled=true", "-Dgoogle.cloud.project=dummy-test-project", - "-Dotel.exporter.otlp.endpoint=http://localhost:4318", + "-Dotel.java.global-autoconfigure.enabled=true", + "-Dotel.javaagent.logging=none", + "-Dotel.exporter.otlp.endpoint=http://localhost:" + backendServerPort, "-Dotel.exporter.otlp.insecure=true", - "-Dotel.traces.exporter=otlp,logging", + "-Dotel.traces.exporter=otlp", "-Dotel.metrics.exporter=none", + "-Dotel.logs.exporter=none", "-Dotel.javaagent.debug=false", "-Dotel.exporter.otlp.protocol=http/protobuf", "-jar", diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java index c926eb3f..4d5e430f 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java @@ -28,12 +28,11 @@ public class InstrumentedTestApp { private static final String serverUrl = "http://localhost:%d/%s"; - private static final int defaultPort = 8080; public static void main(String[] args) throws IOException, InterruptedException { - int port = parsePort(args); - - HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + int port; + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + port = server.getAddress().getPort(); server.createContext("/doWork", new TestHandler()); server.createContext( "/stop", @@ -48,7 +47,7 @@ public static void main(String[] args) throws IOException, InterruptedException }); server.setExecutor(null); // creates a default executor server.start(); - System.out.println("Server ready"); + System.out.println("Starting Server at port " + port); System.out.println("Sending request to do work ..."); makeCall(String.format(serverUrl, port, "doWork")); Thread.sleep(1000); @@ -72,21 +71,4 @@ private static void makeCall(String url) { System.err.println("Error making request: " + e.getMessage()); } } - - private static int parsePort(String[] args) { - int port; - if (args.length > 0) { - try { - port = Integer.parseInt(args[0]); - if (port < 0 || port > 65535) { - throw new NumberFormatException("Port number must be between 0 and 65535"); - } - return port; - } catch (NumberFormatException e) { - System.err.println("Invalid port number provided: " + args[0]); - System.err.println("Using default port: " + defaultPort); - } - } - return defaultPort; - } } diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java index af9f93f6..90eedea2 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java @@ -32,9 +32,7 @@ public class DummyOTelHttpEndpoint { public static Map receivedRequests = new HashMap<>(); public static HttpServer createTestServer() throws IOException { - int port = 4318; // Use a different port than gRPC (e.g., 4318) - - HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); server.createContext("/v1/traces", new TraceHandler()); // Handle traces server.setExecutor(null); // Use default thread pool return server; From aeda53ff10cde155411c4b5ff9a249aed50ce813 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Thu, 19 Dec 2024 16:28:30 +0000 Subject: [PATCH 10/17] Do not enable GCP resource detector by default --- javaagent-extensions/gcp-auth/README.md | 3 ++- .../auth/GcpAuthAutoConfigurationCustomizerProvider.java | 8 -------- .../extension/auth/ExtensionIntegrationTest.java | 1 + 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/javaagent-extensions/gcp-auth/README.md b/javaagent-extensions/gcp-auth/README.md index c441a9e8..c617b4e9 100644 --- a/javaagent-extensions/gcp-auth/README.md +++ b/javaagent-extensions/gcp-auth/README.md @@ -59,10 +59,11 @@ application { // Configure the GCP Auth extension using system properties. // This can also be configured using environment variables. "-Dgoogle.cloud.project=${googleCloudProjectId}", - "-Dgoogle.cloud.region=${googleCloudRegion}", // Configure auto instrumentation. "-Dotel.exporter.otlp.traces.endpoint=${googleOtlpEndpoint}", '-Dotel.java.global-autoconfigure.enabled=true', + // Optionally enable the built-in GCP resource detector + '-Dotel.resource.providers.gcp.enabled=true' '-Dotel.traces.exporter=otlp', '-Dotel.metrics.exporter=logging', } diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java index 3e8c0589..6bf4c55f 100644 --- a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -74,7 +74,6 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { autoConfiguration .addSpanExporterCustomizer( (exporter, configProperties) -> addAuthorizationHeaders(exporter, credentials)) - .addPropertiesSupplier(this::getRequiredProperties) .addResourceCustomizer(this::customizeResource); } catch (IOException e) { throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); @@ -116,13 +115,6 @@ private Map getRequiredHeaderMap(GoogleCredentials credentials) return gcpHeaders; } - // Sets the required properties that are essential for exporting OTLP data to GCP. - private Map getRequiredProperties() { - Map properties = new HashMap<>(); - properties.put("otel.resource.providers.gcp.enabled", "true"); - return properties; - } - // Updates the current resource with the attributes required for ingesting OTLP data on GCP. private Resource customizeResource(Resource resource, ConfigProperties configProperties) { Resource res = diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java index f2d82693..755d19f4 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java @@ -56,6 +56,7 @@ public void smokeTest() throws IOException, InterruptedException { "-Dotel.javaagent.logging=none", "-Dotel.exporter.otlp.endpoint=http://localhost:" + backendServerPort, "-Dotel.exporter.otlp.insecure=true", + "-Dotel.resource.providers.gcp.enabled=true", "-Dotel.traces.exporter=otlp", "-Dotel.metrics.exporter=none", "-Dotel.logs.exporter=none", From 58d4bad5f889d721664f9f9f1eaebb8e7f236eec Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Thu, 19 Dec 2024 17:08:15 +0000 Subject: [PATCH 11/17] Provide methods to support fallback value for config options --- .../extension/auth/ConfigurableOption.java | 21 ++++++++++++++++++- ...thAutoConfigurationCustomizerProvider.java | 15 ++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java index 544f9d22..1ed27772 100644 --- a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/ConfigurableOption.java @@ -17,6 +17,7 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; import java.util.Locale; +import java.util.function.Supplier; /** * An enum representing configurable options for a GCP Authentication Extension. Each option has a @@ -65,7 +66,7 @@ String getSystemProperty() { * Retrieves the configured value for this option. This method checks the environment variable * first and then the system property. * - * @return the configured value as a string, or throws an exception if not configured. + * @return The configured value as a string, or throws an exception if not configured. * @throws ConfigurationException if neither the environment variable nor the system property is * set. */ @@ -86,4 +87,22 @@ String getConfiguredValue() throws ConfigurationException { this.getSystemProperty())); } } + + /** + * Retrieves the value for this option, prioritizing environment variables and system properties. + * If neither an environment variable nor a system property is set for this option, the provided + * fallback function is used to determine the value. + * + * @param fallback A {@link Supplier} that provides the default value for the option when it is + * not explicitly configured via an environment variable or system property. + * @return The configured value for the option, obtained from the environment variable, system + * property, or the fallback function, in that order of precedence. + */ + String getConfiguredValueWithFallback(Supplier fallback) { + try { + return this.getConfiguredValue(); + } catch (ConfigurationException e) { + return fallback.get(); + } + } } diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java index 6bf4c55f..6c1de559 100644 --- a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -117,11 +117,20 @@ private Map getRequiredHeaderMap(GoogleCredentials credentials) // Updates the current resource with the attributes required for ingesting OTLP data on GCP. private Resource customizeResource(Resource resource, ConfigProperties configProperties) { + String gcpProjectId = + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValueWithFallback( + () -> { + try { + GoogleCredentials googleCredentials = GoogleCredentials.getApplicationDefault(); + return googleCredentials.getQuotaProjectId(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); + } + }); + Resource res = Resource.create( - Attributes.of( - AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), - ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue())); + Attributes.of(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), gcpProjectId)); return resource.merge(res); } } From 250b10837cc9950133dd95c58618ddcc6ba3401e Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Thu, 19 Dec 2024 17:15:12 +0000 Subject: [PATCH 12/17] Update README to reflect effect of fallback option --- javaagent-extensions/gcp-auth/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/javaagent-extensions/gcp-auth/README.md b/javaagent-extensions/gcp-auth/README.md index c617b4e9..dabf5a6b 100644 --- a/javaagent-extensions/gcp-auth/README.md +++ b/javaagent-extensions/gcp-auth/README.md @@ -33,8 +33,7 @@ Here is a list of configurable options for the extension: - `GOOGLE_CLOUD_PROJECT`: Environment variable that represents the Google Cloud Project ID to which the telemetry needs to be exported. - Can also be configured using `google.cloud.project` system property. - -It is **mandatory** to configure this option for the extension, not doing so would lead to a `ConfigurationException`. + - If this option is not configured, the extension would infer GCP Project ID from the application default credentials. For more information on application default credentials, see [here](https://cloud.google.com/docs/authentication/application-default-credentials). ## Usage From 122c7d79a01a074ec9b0f943a69d9cbd1515dd18 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Fri, 20 Dec 2024 20:44:17 +0000 Subject: [PATCH 13/17] Add unit test for extension --- build.gradle | 1 + javaagent-extensions/gcp-auth/build.gradle | 7 + ...thAutoConfigurationCustomizerProvider.java | 4 +- ...toConfigurationCustomizerProviderTest.java | 257 ++++++++++++++++++ 4 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java diff --git a/build.gradle b/build.gradle index e72b5df9..b571e1df 100644 --- a/build.gradle +++ b/build.gradle @@ -225,6 +225,7 @@ subprojects { junit5_runtime : "org.junit.jupiter:junit-jupiter-engine:${junit5Version}", junit5_params : "org.junit.jupiter:junit-jupiter-params:${junit5Version}", mockito : "org.mockito:mockito-inline:${mockitoVersion}", + mockito_jupiter: "org.mockito:mockito-junit-jupiter:${mockitoVersion}", slf4j_simple: "org.slf4j:slf4j-simple:${slf4jVersion}", opentelemetry_sdk_testing: "io.opentelemetry:opentelemetry-sdk-testing:${openTelemetryVersion}", test_containers: "org.testcontainers:testcontainers:${testContainersVersion}", diff --git a/javaagent-extensions/gcp-auth/build.gradle b/javaagent-extensions/gcp-auth/build.gradle index f7e5d36e..248c3702 100644 --- a/javaagent-extensions/gcp-auth/build.gradle +++ b/javaagent-extensions/gcp-auth/build.gradle @@ -55,9 +55,16 @@ dependencies { // test dependencies testImplementation(testLibraries.junit5) + testImplementation(libraries.opentelemetry_api) + testImplementation(libraries.opentelemetry_otlp_exporter) testRuntimeOnly(testLibraries.junit5_runtime) + testImplementation(testLibraries.opentelemetry_sdk_testing) + testImplementation(testLibraries.mockito) + testImplementation(testLibraries.mockito_jupiter) + testImplementation(libraries.opentelemetry_sdk_autoconf) // OTel instrumentation used in the sample app to facilitate integration testing agent agentLibraries.agent + testImplementation 'org.awaitility:awaitility:4.2.2' testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' includeDeps 'org.apache.httpcomponents:httpclient:4.5.14' } diff --git a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java index 6c1de559..f2309564 100644 --- a/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java +++ b/javaagent-extensions/gcp-auth/src/main/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -50,8 +50,8 @@ public class GcpAuthAutoConfigurationCustomizerProvider implements AutoConfigurationCustomizerProvider { - private static final String QUOTA_USER_PROJECT_HEADER = "X-Goog-User-Project"; - private static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id"; + static final String QUOTA_USER_PROJECT_HEADER = "X-Goog-User-Project"; + static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id"; /** * Customizes the provided {@link AutoConfigurationCustomizer}. diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java new file mode 100644 index 00000000..dc74c123 --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java @@ -0,0 +1,257 @@ +/* + * 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.*; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.internal.ComponentLoader; +import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.time.Duration; +import java.time.Instant; +import java.util.AbstractMap.SimpleEntry; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Supplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class GcpAuthAutoConfigurationCustomizerProviderTest { + + @Mock private GoogleCredentials mockedGoogleCredentials; + + @Captor private ArgumentCaptor> spanDataCollectionCaptor; + @Captor private ArgumentCaptor>> headerSupplierCaptor; + + private final Map otelProperties = + ImmutableMap.of( + "otel.bsp.schedule.delay", // span exporter + "10", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "none", + "otel.logs.exporter", + "none"); + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + Mockito.when(mockedGoogleCredentials.getQuotaProjectId()).thenReturn("test-project"); + Mockito.when(mockedGoogleCredentials.getAccessToken()) + .thenReturn(new AccessToken("fake", new Date())); + } + + @Test + public void testCustomizerOtlpHttp() { + OtlpHttpSpanExporter mockOtlpHttpSpanExporter = Mockito.mock(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporterBuilder otlpSpanExporterBuilder = OtlpHttpSpanExporter.builder(); + OtlpHttpSpanExporterBuilder spyOtlpHttpSpanExporterBuilder = + Mockito.spy(otlpSpanExporterBuilder); + Mockito.when(spyOtlpHttpSpanExporterBuilder.build()).thenReturn(mockOtlpHttpSpanExporter); + + Mockito.when(mockOtlpHttpSpanExporter.export(Mockito.anyCollection())) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.when(mockOtlpHttpSpanExporter.toBuilder()).thenReturn(spyOtlpHttpSpanExporterBuilder); + + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpHttpSpanExporter); + generateTestSpan(sdk); + + await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted( + () -> { + Mockito.verify(mockOtlpHttpSpanExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpHttpSpanExporterBuilder, Mockito.times(1)) + .setHeaders(headerSupplierCaptor.capture()); + assertEquals(2, headerSupplierCaptor.getValue().get().size()); + verifyAuthHeaders(headerSupplierCaptor.getValue().get()); + + Mockito.verify(mockOtlpHttpSpanExporter, Mockito.atLeast(1)) + .export(spanDataCollectionCaptor.capture()); + spanDataCollectionCaptor + .getValue() + .forEach( + spanData -> { + assertEquals( + "test-project", + spanData + .getAttributes() + .get(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY))); + assertTrue( + spanData + .getAttributes() + .asMap() + .containsKey(AttributeKey.stringKey("work_loop"))); + }); + }); + } + } + + @Test + public void testCustomizerOtlpGrpc() { + OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); + OtlpGrpcSpanExporterBuilder otlpSpanExporterBuilder = OtlpGrpcSpanExporter.builder(); + OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = + Mockito.spy(otlpSpanExporterBuilder); + Mockito.when(spyOtlpGrpcSpanExporterBuilder.build()).thenReturn(mockOtlpGrpcSpanExporter); + + Mockito.when(mockOtlpGrpcSpanExporter.export(Mockito.anyCollection())) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.when(mockOtlpGrpcSpanExporter.toBuilder()).thenReturn(spyOtlpGrpcSpanExporterBuilder); + + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter); + generateTestSpan(sdk); + + await() + .atMost(Duration.ofSeconds(10)) + .untilAsserted( + () -> { + Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1)) + .setHeaders(headerSupplierCaptor.capture()); + assertEquals(2, headerSupplierCaptor.getValue().get().size()); + verifyAuthHeaders(headerSupplierCaptor.getValue().get()); + + Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.atLeast(1)) + .export(spanDataCollectionCaptor.capture()); + spanDataCollectionCaptor + .getValue() + .forEach( + spanData -> { + assertEquals( + "test-project", + spanData + .getAttributes() + .get(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY))); + assertTrue( + spanData + .getAttributes() + .asMap() + .containsKey(AttributeKey.stringKey("work_loop"))); + }); + }); + } + } + + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(SpanExporter spanExporter) { + SpiHelper spiHelper = + SpiHelper.create(GcpAuthAutoConfigurationCustomizerProviderTest.class.getClassLoader()); + AutoConfiguredOpenTelemetrySdkBuilder builder = + AutoConfiguredOpenTelemetrySdk.builder().addPropertiesSupplier(() -> otelProperties); + AutoConfigureUtil.setComponentLoader( + builder, + new ComponentLoader() { + @SuppressWarnings("unchecked") + @Override + public Iterable load(Class spiClass) { + if (spiClass == ConfigurableSpanExporterProvider.class) { + return Collections.singletonList( + (T) + new ConfigurableSpanExporterProvider() { + @Override + public SpanExporter createExporter(ConfigProperties configProperties) { + return spanExporter; + } + + @Override + public String getName() { + return "otlp"; + } + }); + } + return spiHelper.load(spiClass); + } + }); + return builder.build().getOpenTelemetrySdk(); + } + + private void verifyAuthHeaders(Map headers) { + Set> headerEntrySet = headers.entrySet(); + assertTrue( + headerEntrySet.contains(new SimpleEntry<>(QUOTA_USER_PROJECT_HEADER, "test-project"))); + assertTrue(headerEntrySet.contains(new SimpleEntry<>("Authorization", "Bearer fake"))); + } + + private void generateTestSpan(OpenTelemetrySdk openTelemetrySdk) { + Span span = openTelemetrySdk.getTracer("test").spanBuilder("sample").startSpan(); + try (Scope ignored = span.makeCurrent()) { + long workOutput = busyloop(); + span.setAttribute("work_loop", workOutput); + } finally { + span.end(); + } + } + + // loop to simulate work done + private long busyloop() { + Instant start = Instant.now(); + Instant end; + long counter = 0; + do { + counter++; + end = Instant.now(); + } while (Duration.between(start, end).toMillis() < 1000); + System.out.println("Busy work done, counted " + counter + " times in one second."); + return counter; + } +} From dacebad0183712b6643233ecc2d96d5ee4952f5b Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Mon, 23 Dec 2024 23:51:17 +0000 Subject: [PATCH 14/17] Simplify the integration smoke test --- build.gradle | 1 + javaagent-extensions/gcp-auth/build.gradle | 64 +++++- .../auth/GcpAuthExtensionSmokeTest.java | 190 ++++++++++++++++++ .../extension/auth/springapp/Application.java | 26 +++ .../extension/auth/springapp/Controller.java | 49 +++++ 5 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java create mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/springapp/Application.java create mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/springapp/Controller.java diff --git a/build.gradle b/build.gradle index b571e1df..76c69e9e 100644 --- a/build.gradle +++ b/build.gradle @@ -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}", diff --git a/javaagent-extensions/gcp-auth/build.gradle b/javaagent-extensions/gcp-auth/build.gradle index 248c3702..ee555fb5 100644 --- a/javaagent-extensions/gcp-auth/build.gradle +++ b/javaagent-extensions/gcp-auth/build.gradle @@ -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 * @@ -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' @@ -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' @@ -70,7 +94,7 @@ 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" @@ -78,7 +102,7 @@ tasks.register('CopyAgent', Copy) { } tasks.register('BuildTestApp', org.gradle.jvm.tasks.Jar) { - dependsOn 'CopyAgent' + dependsOn 'copyAgent' dependsOn 'shadowJar' archiveFileName.set("auto-instrumented-test-server.jar") @@ -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" + ] } diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java new file mode 100644 index 00000000..86243dc4 --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java @@ -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 extractedHeaders = extractHeadersFromRequests(requests); + verifyRequestHeaders(extractedHeaders); + + List extractedResourceSpans = + extractResourceSpansFromRequests(requests); + verifyResourceAttributes(extractedResourceSpans); + }); + } + + // Helper methods + + private void verifyResourceAttributes(List 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 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 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 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 getExportTraceServiceRequest(Body body) { + try { + return Optional.ofNullable(ExportTraceServiceRequest.parseFrom(body.getRawBytes())); + } catch (InvalidProtocolBufferException e) { + return Optional.empty(); + } + } +} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/springapp/Application.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/springapp/Application.java new file mode 100644 index 00000000..e4455c4f --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/springapp/Application.java @@ -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); + } +} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/springapp/Controller.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/springapp/Controller.java new file mode 100644 index 00000000..cb8fe88b --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/springapp/Controller.java @@ -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; + } +} From 2cd373e0153115b9a414983c034aa2bad9c53f33 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Tue, 24 Dec 2024 00:31:09 +0000 Subject: [PATCH 15/17] Test cleanup --- javaagent-extensions/gcp-auth/build.gradle | 45 +--------- .../auth/ExtensionIntegrationTest.java | 84 ------------------- .../auth/testapp/InstrumentedTestApp.java | 74 ---------------- .../extension/auth/testapp/TestHandler.java | 42 ---------- .../testbackend/DummyOTelHttpEndpoint.java | 74 ---------------- 5 files changed, 3 insertions(+), 316 deletions(-) delete mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java delete mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java delete mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java delete mode 100644 javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java diff --git a/javaagent-extensions/gcp-auth/build.gradle b/javaagent-extensions/gcp-auth/build.gradle index ee555fb5..be1ee750 100644 --- a/javaagent-extensions/gcp-auth/build.gradle +++ b/javaagent-extensions/gcp-auth/build.gradle @@ -1,20 +1,3 @@ -/* - * 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 * @@ -34,7 +17,7 @@ plugins { id 'java' id 'java-library' id 'com.github.johnrengelman.shadow' - id 'org.springframework.boot' version '3.4.1' + id 'org.springframework.boot' version '2.7.18' } description = 'OpenTelemetry Java Agent Extension that enables authentication support for OTLP exporters' @@ -55,7 +38,6 @@ jar { // Custom configurations used to facilitate running the integration test configurations { agent - includeDeps } dependencies { @@ -72,10 +54,10 @@ dependencies { implementation(libraries.google_auth) // test dependencies + testRuntimeOnly(testLibraries.junit5_runtime) testImplementation(testLibraries.junit5) testImplementation(libraries.opentelemetry_api) testImplementation(libraries.opentelemetry_otlp_exporter) - testRuntimeOnly(testLibraries.junit5_runtime) testImplementation(testLibraries.opentelemetry_sdk_testing) testImplementation(testLibraries.mockito) testImplementation(testLibraries.mockito_jupiter) @@ -84,13 +66,11 @@ dependencies { testImplementation(libraries.spring_boot_starter_web) testImplementation(testLibraries.spring_boot_starter_test) testImplementation("org.mock-server:mockserver-netty:5.15.0") + testImplementation("org.awaitility:awaitility:4.2.2") 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' - testImplementation 'org.apache.httpcomponents:httpclient:4.5.14' - includeDeps 'org.apache.httpcomponents:httpclient:4.5.14' } // task to copy and rename the Java Auto-Instrumentation Agent into 'libs' folder @@ -101,25 +81,6 @@ tasks.register('copyAgent', Copy) { } } -tasks.register('BuildTestApp', org.gradle.jvm.tasks.Jar) { - dependsOn 'copyAgent' - dependsOn 'shadowJar' - - archiveFileName.set("auto-instrumented-test-server.jar") - from(sourceSets.test.output) { - include 'com/google/cloud/opentelemetry/extension/auth/testapp/**' - } - from { - configurations.includeDeps.collect { - it.isDirectory() ? it : zipTree(it) - } - } - manifest { - attributes.put('Main-Class', 'com.google.cloud.opentelemetry.extension.auth.testapp.InstrumentedTestApp') - } - duplicatesStrategy = DuplicatesStrategy.EXCLUDE -} - def builtLibsDir = layout.buildDirectory.dir("libs").get().toString() def javaAgentJarPath = builtLibsDir + "/otel-agent.jar" def authExtensionJarPath = builtLibsDir + "/gcp-auth-extension.jar" diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java deleted file mode 100644 index 755d19f4..00000000 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/ExtensionIntegrationTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.google.cloud.opentelemetry.extension.auth.testbackend.DummyOTelHttpEndpoint; -import com.sun.net.httpserver.HttpServer; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.lang.ProcessBuilder.Redirect; -import org.junit.jupiter.api.Test; - -public class ExtensionIntegrationTest { - - @Test - public void smokeTest() throws IOException, InterruptedException { - String testAppJarPath = - new File("./build/libs/auto-instrumented-test-server.jar").getAbsolutePath(); - - String javaAgentJarPath = new File("./build/libs/otel-agent.jar").getAbsolutePath(); - String authExtensionJarPath = new File("./build/libs/gcp-auth-extension.jar").getAbsolutePath(); - - HttpServer backendServer; - try { - backendServer = DummyOTelHttpEndpoint.createTestServer(); - } catch (IOException e) { - throw new RuntimeException(e); - } - backendServer.start(); - int backendServerPort = backendServer.getAddress().getPort(); - System.out.println("Started OTLP HTTP Endpoint on localhost:" + backendServerPort); - Process p = - new ProcessBuilder( - "java", - "-javaagent:" + javaAgentJarPath, - "-Dotel.javaagent.extensions=" + authExtensionJarPath, - "-Dgoogle.cloud.project=dummy-test-project", - "-Dotel.java.global-autoconfigure.enabled=true", - "-Dotel.javaagent.logging=none", - "-Dotel.exporter.otlp.endpoint=http://localhost:" + backendServerPort, - "-Dotel.exporter.otlp.insecure=true", - "-Dotel.resource.providers.gcp.enabled=true", - "-Dotel.traces.exporter=otlp", - "-Dotel.metrics.exporter=none", - "-Dotel.logs.exporter=none", - "-Dotel.javaagent.debug=false", - "-Dotel.exporter.otlp.protocol=http/protobuf", - "-jar", - testAppJarPath) - .redirectError( - Redirect.INHERIT) // Redirect stderr from this process to the current process - .start(); - p.waitFor(); // wait for the process running the test app to finish - - // flush logs from instrumented app process - BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); - reader.lines().forEach(System.out::println); - System.out.println("Instrumented app process finished"); - - backendServer.stop(0); // stop the mock HTTP server - - // assert on number of requests - assertEquals(1, DummyOTelHttpEndpoint.receivedRequests.size()); - // ensure headers were verified for all requests - DummyOTelHttpEndpoint.receivedRequests.forEach((request, verified) -> assertTrue(verified)); - } -} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java deleted file mode 100644 index 4d5e430f..00000000 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/InstrumentedTestApp.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.testapp; - -import com.sun.net.httpserver.HttpServer; -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import org.apache.http.HttpEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.util.EntityUtils; - -public class InstrumentedTestApp { - private static final String serverUrl = "http://localhost:%d/%s"; - - public static void main(String[] args) throws IOException, InterruptedException { - int port; - HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); - port = server.getAddress().getPort(); - server.createContext("/doWork", new TestHandler()); - server.createContext( - "/stop", - exchange -> { - String response = "Stopping Server"; - System.out.println(response); - exchange.sendResponseHeaders(200, response.length()); - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); - server.stop(0); - }); - server.setExecutor(null); // creates a default executor - server.start(); - System.out.println("Starting Server at port " + port); - System.out.println("Sending request to do work ..."); - makeCall(String.format(serverUrl, port, "doWork")); - Thread.sleep(1000); - System.out.println("Sending request to stop server ..."); - makeCall(String.format(serverUrl, port, "stop")); - } - - // Make calls to the server using Apache HttpClient - private static void makeCall(String url) { - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpGet request = new HttpGet(url); - try (CloseableHttpResponse response = httpClient.execute(request)) { - HttpEntity entity = response.getEntity(); - if (entity != null) { - String responseBody = EntityUtils.toString(entity); - System.out.println("Response Body: " + responseBody); - EntityUtils.consume(entity); - } - } - } catch (IOException e) { - System.err.println("Error making request: " + e.getMessage()); - } - } -} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java deleted file mode 100644 index e6918bd8..00000000 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testapp/TestHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.testapp; - -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Random; - -public class TestHandler implements HttpHandler { - private final Random random = new Random(); - - @Override - public void handle(HttpExchange exchange) throws IOException { - int n = random.nextInt(100); - System.out.println("handling"); - String response = n % 2 == 0 ? "Send Response A" : "Send Response B"; - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - exchange.sendResponseHeaders(200, response.length()); - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); - } -} diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java deleted file mode 100644 index 90eedea2..00000000 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/testbackend/DummyOTelHttpEndpoint.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.testbackend; - -import com.sun.net.httpserver.Headers; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class DummyOTelHttpEndpoint { - // Map to keep track of incoming requests and if the headers were verified for the request - public static Map receivedRequests = new HashMap<>(); - - public static HttpServer createTestServer() throws IOException { - HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); - server.createContext("/v1/traces", new TraceHandler()); // Handle traces - server.setExecutor(null); // Use default thread pool - return server; - } - - // Dummy trace handler - private static class TraceHandler implements HttpHandler { - @Override - public void handle(HttpExchange exchange) throws IOException { - System.out.println("handling exchange from " + exchange.getRemoteAddress()); - String requestBody = - new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); - System.out.println("handling trace export " + requestBody); - - String response = "Trace data received"; - if (verifyRequestHeaders(exchange)) { - receivedRequests.put(exchange.toString(), true); - exchange.sendResponseHeaders(200, response.getBytes().length); - } else { - receivedRequests.put(exchange.toString(), false); - exchange.sendResponseHeaders(400, response.getBytes().length); - } - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); - } - - private boolean verifyRequestHeaders(HttpExchange exchange) { - Headers requestHeaders = exchange.getRequestHeaders(); - List quotaUserProjectHeader = requestHeaders.get("X-Goog-User-Project"); - List authHeader = requestHeaders.get("Authorization"); - if (quotaUserProjectHeader.size() != 1 - || quotaUserProjectHeader.get(0).equals("dummy-test-project")) { - return false; - } - return authHeader.size() == 1 && authHeader.get(0).startsWith("Bearer"); - } - } -} From a267bbba2f75c70cec7fc4d75b5ccde583f6b05a Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Fri, 27 Dec 2024 03:46:27 +0000 Subject: [PATCH 16/17] Make integration tests environment agnostic --- javaagent-extensions/gcp-auth/build.gradle | 5 +- .../auth/GcpAuthExtensionSmokeTest.java | 59 ++++++++++++++++++- .../src/test/resources/fakecreds.json | 13 ++++ 3 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 javaagent-extensions/gcp-auth/src/test/resources/fakecreds.json diff --git a/javaagent-extensions/gcp-auth/build.gradle b/javaagent-extensions/gcp-auth/build.gradle index be1ee750..0f6b96d7 100644 --- a/javaagent-extensions/gcp-auth/build.gradle +++ b/javaagent-extensions/gcp-auth/build.gradle @@ -103,7 +103,10 @@ test { dependsOn 'copyAgent' useJUnitPlatform() - environment("GOOGLE_CLOUD_QUOTA_PROJECT", "test-project-id") + def fakeCredsFilePath = project.file("src/test/resources/fakecreds.json").getAbsolutePath() + + environment("GOOGLE_CLOUD_QUOTA_PROJECT", "quota-project-id") + environment("GOOGLE_APPLICATION_CREDENTIALS", fakeCredsFilePath.toString()) jvmArgs = [ "-javaagent:${javaAgentJarPath}", "-Dotel.javaagent.extensions=${authExtensionJarPath}", diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java index 86243dc4..be3ff0ea 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java @@ -31,11 +31,18 @@ import io.opentelemetry.proto.common.v1.KeyValue; import io.opentelemetry.proto.trace.v1.ResourceSpans; import java.net.URI; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -61,7 +68,7 @@ public class GcpAuthExtensionSmokeTest { @Autowired private TestRestTemplate template; - // The port at which the backend server will recieve telemetry + // The port at which the backend server will receive 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; @@ -78,12 +85,19 @@ public class GcpAuthExtensionSmokeTest { private static final String DUMMY_GCP_PROJECT = System.getProperty("google.cloud.project"); @BeforeAll - public static void setup() { + public static void setup() throws NoSuchAlgorithmException, KeyManagementException { // 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("https.proxyHost", "localhost"); + System.setProperty("https.proxyPort", MOCK_GCP_METADATA_PORT + ""); System.setProperty("http.nonProxyHost", "localhost"); + System.setProperty("https.nonProxyHost", "localhost"); + + // Disable SSL validation for integration test + // The OAuth2 token validation requires SSL validation + disableSSLValidation(); // Set up mock OTLP backend server to which traces will be exported backendServer = ClientAndServer.startClientAndServer(EXPORTER_ENDPOINT_PORT); @@ -93,7 +107,19 @@ public static void setup() { 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 mockServerClient = + new MockServerClient("localhost", MOCK_GCP_METADATA_PORT).withSecure(true); + + // mock the token refresh + mockServerClient + .when(request().withMethod("POST").withPath("/token")) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "application/json") + .withBody(new JsonBody(accessTokenResponse))); + // mock the gcp metadata server mockServerClient .when( request() @@ -138,6 +164,33 @@ public void authExtensionSmokeTest() { // Helper methods + private static void disableSSLValidation() + throws NoSuchAlgorithmException, KeyManagementException { + TrustManager[] trustAllCerts = + new TrustManager[] { + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + System.out.println("Reached checkClientTrusted"); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + System.out.println("Reached checkServerTrusted"); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + System.out.println("No acceptedIssuers"); + return null; + } + } + }; + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + } + private void verifyResourceAttributes(List extractedResourceSpans) { extractedResourceSpans.forEach( resourceSpan -> diff --git a/javaagent-extensions/gcp-auth/src/test/resources/fakecreds.json b/javaagent-extensions/gcp-auth/src/test/resources/fakecreds.json new file mode 100644 index 00000000..28e89e5a --- /dev/null +++ b/javaagent-extensions/gcp-auth/src/test/resources/fakecreds.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "quota-project-id", + "private_key_id": "aljmafmlamlmmasma", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKNwapOQ6rQJHetP\nHRlJBIh1OsOsUBiXb3rXXE3xpWAxAha0MH+UPRblOko+5T2JqIb+xKf9Vi3oTM3t\nKvffaOPtzKXZauscjq6NGzA3LgeiMy6q19pvkUUOlGYK6+Xfl+B7Xw6+hBMkQuGE\nnUS8nkpR5mK4ne7djIyfHFfMu4ptAgMBAAECgYA+s0PPtMq1osG9oi4xoxeAGikf\nJB3eMUptP+2DYW7mRibc+ueYKhB9lhcUoKhlQUhL8bUUFVZYakP8xD21thmQqnC4\nf63asad0ycteJMLb3r+z26LHuCyOdPg1pyLk3oQ32lVQHBCYathRMcVznxOG16VK\nI8BFfstJTaJu0lK/wQJBANYFGusBiZsJQ3utrQMVPpKmloO2++4q1v6ZR4puDQHx\nTjLjAIgrkYfwTJBLBRZxec0E7TmuVQ9uJ+wMu/+7zaUCQQDDf2xMnQqYknJoKGq+\noAnyC66UqWC5xAnQS32mlnJ632JXA0pf9pb1SXAYExB1p9Dfqd3VAwQDwBsDDgP6\nHD8pAkEA0lscNQZC2TaGtKZk2hXkdcH1SKru/g3vWTkRHxfCAznJUaza1fx0wzdG\nGcES1Bdez0tbW4llI5By/skZc2eE3QJAFl6fOskBbGHde3Oce0F+wdZ6XIJhEgCP\niukIcKZoZQzoiMJUoVRrA5gqnmaYDI5uRRl/y57zt6YksR3KcLUIuQJAd242M/WF\n6YAZat3q/wEeETeQq1wrooew+8lHl05/Nt0cCpV48RGEhJ83pzBm3mnwHf8lTBJH\nx6XroMXsmbnsEw==\n-----END PRIVATE KEY-----\n", + "client_email": "sample@appspot.gserviceaccount.com", + "client_id": "100000000000000000221", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/quota-project-id%40appspot.gserviceaccount.com", + "universe_domain": "googleapis.com" +} From 60368334b489738e6fa49a28fcbcb6289e2fb2dd Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Fri, 27 Dec 2024 04:13:17 +0000 Subject: [PATCH 17/17] Test code cleanup and doc fix --- javaagent-extensions/gcp-auth/README.md | 1 - ...java => GcpAuthExtensionEndToEndTest.java} | 40 ++++++------------- .../src/test/resources/fakecreds.json | 2 +- 3 files changed, 13 insertions(+), 30 deletions(-) rename javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/{GcpAuthExtensionSmokeTest.java => GcpAuthExtensionEndToEndTest.java} (85%) diff --git a/javaagent-extensions/gcp-auth/README.md b/javaagent-extensions/gcp-auth/README.md index dabf5a6b..9a2c2071 100644 --- a/javaagent-extensions/gcp-auth/README.md +++ b/javaagent-extensions/gcp-auth/README.md @@ -48,7 +48,6 @@ def otelAgentPath = // Specify the path for Google Cloud Authentication Extension for the Java Agent. def extensionPath = def googleCloudProjectId = -def googleCloudRegion = def googleOtlpEndpoint = application { diff --git a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionEndToEndTest.java similarity index 85% rename from javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java rename to javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionEndToEndTest.java index be3ff0ea..35b63cf7 100644 --- a/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionSmokeTest.java +++ b/javaagent-extensions/gcp-auth/src/test/java/com/google/cloud/opentelemetry/extension/auth/GcpAuthExtensionEndToEndTest.java @@ -62,7 +62,7 @@ @SpringBootTest( classes = {Application.class}, webEnvironment = WebEnvironment.RANDOM_PORT) -public class GcpAuthExtensionSmokeTest { +public class GcpAuthExtensionEndToEndTest { @LocalServerPort private int testApplicationPort; // port at which the spring app is running @@ -70,28 +70,26 @@ public class GcpAuthExtensionSmokeTest { // The port at which the backend server will receive 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; + // The port at which the mock GCP OAuth 2.0 server will run + private static final int MOCK_GCP_OAUTH2_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; + // Mock server to intercept calls to the GCP OAuth 2.0 server and provide fake credentials + private static ClientAndServer mockGcpOAuth2Server; - 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() throws NoSuchAlgorithmException, KeyManagementException { - // Set up the mock server to always respond with 200 - // Setup proxy host + // Setup proxy host(s) System.setProperty("http.proxyHost", "localhost"); - System.setProperty("http.proxyPort", MOCK_GCP_METADATA_PORT + ""); + System.setProperty("http.proxyPort", MOCK_GCP_OAUTH2_PORT + ""); System.setProperty("https.proxyHost", "localhost"); - System.setProperty("https.proxyPort", MOCK_GCP_METADATA_PORT + ""); + System.setProperty("https.proxyPort", MOCK_GCP_OAUTH2_PORT + ""); System.setProperty("http.nonProxyHost", "localhost"); System.setProperty("https.nonProxyHost", "localhost"); @@ -106,12 +104,12 @@ public static void setup() throws NoSuchAlgorithmException, KeyManagementExcepti // 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); + mockGcpOAuth2Server = ClientAndServer.startClientAndServer(MOCK_GCP_OAUTH2_PORT); MockServerClient mockServerClient = - new MockServerClient("localhost", MOCK_GCP_METADATA_PORT).withSecure(true); + new MockServerClient("localhost", MOCK_GCP_OAUTH2_PORT).withSecure(true); - // mock the token refresh + // mock the token refresh - always respond with 200 mockServerClient .when(request().withMethod("POST").withPath("/token")) .respond( @@ -119,26 +117,13 @@ public static void setup() throws NoSuchAlgorithmException, KeyManagementExcepti .withStatusCode(200) .withHeader("Content-Type", "application/json") .withBody(new JsonBody(accessTokenResponse))); - // mock the gcp metadata server - 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); + stopQuietly(mockGcpOAuth2Server); } @Test @@ -146,7 +131,6 @@ 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) diff --git a/javaagent-extensions/gcp-auth/src/test/resources/fakecreds.json b/javaagent-extensions/gcp-auth/src/test/resources/fakecreds.json index 28e89e5a..b52122ca 100644 --- a/javaagent-extensions/gcp-auth/src/test/resources/fakecreds.json +++ b/javaagent-extensions/gcp-auth/src/test/resources/fakecreds.json @@ -2,7 +2,7 @@ "type": "service_account", "project_id": "quota-project-id", "private_key_id": "aljmafmlamlmmasma", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKNwapOQ6rQJHetP\nHRlJBIh1OsOsUBiXb3rXXE3xpWAxAha0MH+UPRblOko+5T2JqIb+xKf9Vi3oTM3t\nKvffaOPtzKXZauscjq6NGzA3LgeiMy6q19pvkUUOlGYK6+Xfl+B7Xw6+hBMkQuGE\nnUS8nkpR5mK4ne7djIyfHFfMu4ptAgMBAAECgYA+s0PPtMq1osG9oi4xoxeAGikf\nJB3eMUptP+2DYW7mRibc+ueYKhB9lhcUoKhlQUhL8bUUFVZYakP8xD21thmQqnC4\nf63asad0ycteJMLb3r+z26LHuCyOdPg1pyLk3oQ32lVQHBCYathRMcVznxOG16VK\nI8BFfstJTaJu0lK/wQJBANYFGusBiZsJQ3utrQMVPpKmloO2++4q1v6ZR4puDQHx\nTjLjAIgrkYfwTJBLBRZxec0E7TmuVQ9uJ+wMu/+7zaUCQQDDf2xMnQqYknJoKGq+\noAnyC66UqWC5xAnQS32mlnJ632JXA0pf9pb1SXAYExB1p9Dfqd3VAwQDwBsDDgP6\nHD8pAkEA0lscNQZC2TaGtKZk2hXkdcH1SKru/g3vWTkRHxfCAznJUaza1fx0wzdG\nGcES1Bdez0tbW4llI5By/skZc2eE3QJAFl6fOskBbGHde3Oce0F+wdZ6XIJhEgCP\niukIcKZoZQzoiMJUoVRrA5gqnmaYDI5uRRl/y57zt6YksR3KcLUIuQJAd242M/WF\n6YAZat3q/wEeETeQq1wrooew+8lHl05/Nt0cCpV48RGEhJ83pzBm3mnwHf8lTBJH\nx6XroMXsmbnsEw==\n-----END PRIVATE KEY-----\n", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12ikv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/GrCtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrPSXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAutLPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEAgidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ==\n-----END PRIVATE KEY-----\n", "client_email": "sample@appspot.gserviceaccount.com", "client_id": "100000000000000000221", "auth_uri": "https://accounts.google.com/o/oauth2/auth",