Skip to content

Commit

Permalink
Add GCP authentication extension (#1631)
Browse files Browse the repository at this point in the history
  • Loading branch information
psx95 authored Jan 10, 2025
1 parent deb9746 commit 7480d43
Show file tree
Hide file tree
Showing 16 changed files with 1,046 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ components:
gcp-resources:
- jsuereth
- psx95
gcp-auth-extension:
- jsuereth
- psx95
jfr-connection:
- breedx-splk
- jeanbisutti
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/draft-change-log-entries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ component_names["compressors/"]="Compressors"
component_names["consistent-sampling/"]="Consistent sampling"
component_names["disk-buffering/"]="Disk buffering"
component_names["gcp-resources/"]="GCP Resources"
component_names["gcp-auth-extension/"]="GCP authentication extension"
component_names["inferred-spans/"]="Inferred spans"
component_names["jfr-connection/"]="JFR connection"
component_names["jfr-events/"]="JFR events"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ feature or via instrumentation, this project is hopefully for you.
| alpha | [zstd Compressor](./compressors/compressor-zstd/README.md) |
| alpha | [Consistent Sampling](./consistent-sampling/README.md) |
| alpha | [Disk Buffering](./disk-buffering/README.md) |
| alpha | [GCP Authentication Extension](./gcp-auth-extension/README.md) |
| beta | [GCP Resources](./gcp-resources/README.md) |
| beta | [Inferred Spans](./inferred-spans/README.md) |
| alpha | [JFR Connection](./jfr-connection/README.md) |
Expand Down
124 changes: 124 additions & 0 deletions gcp-auth-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Google Cloud Authentication Extension

The Google Cloud Auth Extension allows the users to export telemetry from their applications 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.
- 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

### With OpenTelemetry Java agent

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 = <OpenTelemetry Java Agent location>
// Specify the path for Google Cloud Authentication Extension for the Java Agent.
def extensionPath = <Google Cloud Authentication Extension location>
def googleCloudProjectId = <Your Google Cloud Project ID>
def googleOtlpEndpoint = <Google Cloud OTLP endpoint>
val autoconf_config = listOf(
"-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}",
// 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'
)
application {
...
applicationDefaultJvmArgs = autoconf_config
...
}
```

### Without OpenTelemetry Java agent

This extension can be used without the OpenTelemetry Java agent by leveraging the [OpenTelemetry SDK Autoconfigure](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure/README.md) module.\
When using the autoconfigured SDK, simply adding this extension as a dependency automatically configures authentication headers and resource attributes for spans, enabling export to Google Cloud.

Below is a snippet showing how to use this extension as a dependency when the application is not instrumented using the OpenTelemetry Java agent.

```gradle
dependencies {
implementation("io.opentelemetry:opentelemetry-api")
implementation("io.opentelemetry:opentelemetry-sdk")
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
// include the auth extension dependency
implementation("io.opentelemetry.contrib:opentelemetry-gcp-auth-extension")
// other dependencies
...
}
val autoconf_config = listOf(
'-Dgoogle.cloud.project=your-gcp-project-id',
'-Dotel.exporter.otlp.endpoint=https://your.otlp.endpoint:1234',
'-Dotel.traces.exporter=otlp',
'-Dotel.java.global-autoconfigure.enabled=true'
// any additional args
...
)
application {
applicationDefaultJvmArgs = autoconf_config
// additional configuration
...
}
```

## Component Owners

- [Josh Suereth](https://github.com/jsuereth), Google
- [Pranav Sharma](https://github.com/psx95), Google

Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).
114 changes: 114 additions & 0 deletions gcp-auth-extension/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
plugins {
id("otel.java-conventions")
id("otel.publish-conventions")
id("com.github.johnrengelman.shadow")
id("org.springframework.boot") version "2.7.18"
}

description = "OpenTelemetry extension that provides GCP authentication support for OTLP exporters"
otelJava.moduleName.set("io.opentelemetry.contrib.gcp.auth")

val agent: Configuration by configurations.creating {
isCanBeResolved = true
isCanBeConsumed = false
}

dependencies {
annotationProcessor("com.google.auto.service:auto-service")
// We use `compileOnly` dependency because during runtime all necessary classes are provided by
// javaagent itself.
compileOnly("com.google.auto.service:auto-service-annotations")
compileOnly("io.opentelemetry:opentelemetry-api")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
compileOnly("io.opentelemetry:opentelemetry-exporter-otlp")

// Only dependencies added to `implementation` configuration will be picked up by Shadow plugin
implementation("com.google.auth:google-auth-library-oauth2-http:1.30.1")

// Test dependencies
testCompileOnly("com.google.auto.service:auto-service-annotations")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation("org.junit.jupiter:junit-jupiter-api")

testImplementation("io.opentelemetry:opentelemetry-api")
testImplementation("io.opentelemetry:opentelemetry-exporter-otlp")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
testImplementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations")

testImplementation("org.awaitility:awaitility")
testImplementation("org.mockito:mockito-inline")
testImplementation("org.mockito:mockito-junit-jupiter")
testImplementation("org.mock-server:mockserver-netty:5.15.0")
testImplementation("io.opentelemetry.proto:opentelemetry-proto:1.4.0-alpha")
testImplementation("org.springframework.boot:spring-boot-starter-web:2.7.18")
testImplementation("org.springframework.boot:spring-boot-starter:2.7.18")
testImplementation("org.springframework.boot:spring-boot-starter-test:2.7.18")

agent("io.opentelemetry.javaagent:opentelemetry-javaagent")
}

tasks {
test {
useJUnitPlatform()
// exclude integration test
exclude("io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.class")
}

shadowJar {
archiveClassifier.set("")
}

jar {
// Disable standard jar
enabled = false
}

assemble {
dependsOn(shadowJar)
}

bootJar {
// disable bootJar in build since it only runs as part of test
enabled = false
}
}

val builtLibsDir = layout.buildDirectory.dir("libs").get().asFile.absolutePath
val javaAgentJarPath = "$builtLibsDir/otel-agent.jar"
val authExtensionJarPath = "${tasks.shadowJar.get().archiveFile.get()}"

tasks.register<Copy>("copyAgent") {
into(layout.buildDirectory.dir("libs"))
from(configurations.named("agent") {
rename("opentelemetry-javaagent(.*).jar", "otel-agent.jar")
})
}

tasks.register<Test>("IntegrationTest") {
dependsOn(tasks.shadowJar)
dependsOn(tasks.named("copyAgent"))

useJUnitPlatform()
// include only the integration test file
include("io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.class")

val fakeCredsFilePath = project.file("src/test/resources/fakecreds.json").absolutePath

environment("GOOGLE_CLOUD_QUOTA_PROJECT", "quota-project-id")
environment("GOOGLE_APPLICATION_CREDENTIALS", fakeCredsFilePath)
jvmArgs = listOf(
"-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"
)
}
2 changes: 2 additions & 0 deletions gcp-auth-extension/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# TODO: uncomment when ready to mark as stable
# otel.stable=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.gcp.auth;

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
* 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");

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() {
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(
"GCP Authentication Extension not configured properly: %s not configured. Configure it by exporting environment variable %s or system property %s",
this.userReadableName, this.getEnvironmentVariable(), 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<String> fallback) {
try {
return this.getConfiguredValue();
} catch (ConfigurationException e) {
return fallback.get();
}
}
}
Loading

0 comments on commit 7480d43

Please sign in to comment.