Skip to content

Commit 3a59d50

Browse files
committed
Add GCP auth extension implementation
1 parent 999122f commit 3a59d50

File tree

5 files changed

+286
-13
lines changed

5 files changed

+286
-13
lines changed

gcp-auth-extension/build.gradle.kts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
plugins {
22
id("otel.java-conventions")
3-
43
id("otel.publish-conventions")
4+
id("com.github.johnrengelman.shadow")
55
}
66

77
description = "OpenTelemetry Java Agent Extension that enables authentication support for OTLP exporters"
88
otelJava.moduleName.set("io.opentelemetry.contrib.gcp.auth")
99

1010
dependencies {
11+
annotationProcessor("com.google.auto.service:auto-service")
12+
compileOnly("com.google.auto.service:auto-service-annotations")
13+
14+
// We use `compileOnly` dependency because during runtime all necessary classes are provided by
15+
// javaagent itself.
16+
compileOnly("io.opentelemetry:opentelemetry-api")
17+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
18+
compileOnly("io.opentelemetry:opentelemetry-exporter-otlp")
19+
20+
// Only dependencies added to `implementation` configuration will be picked up by Shadow plugin
21+
implementation("com.google.auth:google-auth-library-oauth2-http:1.30.1")
22+
1123
testImplementation("org.junit.jupiter:junit-jupiter")
1224
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
1325
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.gcp.auth;
7+
8+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
9+
import java.util.Locale;
10+
import java.util.function.Supplier;
11+
12+
/**
13+
* An enum representing configurable options for a GCP Authentication Extension. Each option has a
14+
* user-readable name and can be configured using environment variables or system properties.
15+
*/
16+
public enum ConfigurableOption {
17+
/**
18+
* Represents the Google Cloud Project ID option. Can be configured using the environment variable
19+
* `GOOGLE_CLOUD_PROJECT` or the system property `google.cloud.project`.
20+
*/
21+
GOOGLE_CLOUD_PROJECT("Google Cloud Project ID");
22+
23+
private final String userReadableName;
24+
private final String environmentVariableName;
25+
private final String systemPropertyName;
26+
27+
ConfigurableOption(String userReadableName) {
28+
this.userReadableName = userReadableName;
29+
this.environmentVariableName = this.name();
30+
this.systemPropertyName =
31+
this.environmentVariableName.toLowerCase(Locale.ENGLISH).replace('_', '.');
32+
}
33+
34+
/**
35+
* Returns the environment variable name associated with this option.
36+
*
37+
* @return the environment variable name (e.g., GOOGLE_CLOUD_PROJECT)
38+
*/
39+
String getEnvironmentVariable() {
40+
return this.environmentVariableName;
41+
}
42+
43+
/**
44+
* Returns the system property name associated with this option.
45+
*
46+
* @return the system property name (e.g., google.cloud.project)
47+
*/
48+
String getSystemProperty() {
49+
return this.systemPropertyName;
50+
}
51+
52+
/**
53+
* Retrieves the configured value for this option. This method checks the environment variable
54+
* first and then the system property.
55+
*
56+
* @return The configured value as a string, or throws an exception if not configured.
57+
* @throws ConfigurationException if neither the environment variable nor the system property is
58+
* set.
59+
*/
60+
String getConfiguredValue() {
61+
String envVar = System.getenv(this.getEnvironmentVariable());
62+
String sysProp = System.getProperty(this.getSystemProperty());
63+
64+
if (envVar != null && !envVar.isEmpty()) {
65+
return envVar;
66+
} else if (sysProp != null && !sysProp.isEmpty()) {
67+
return sysProp;
68+
} else {
69+
throw new ConfigurationException(
70+
String.format(
71+
"GCP Authentication Extension not configured properly: %s not configured. Configure it by exporting environment variable %s or system property %s",
72+
this.userReadableName, this.getEnvironmentVariable(), this.getSystemProperty()));
73+
}
74+
}
75+
76+
/**
77+
* Retrieves the value for this option, prioritizing environment variables and system properties.
78+
* If neither an environment variable nor a system property is set for this option, the provided
79+
* fallback function is used to determine the value.
80+
*
81+
* @param fallback A {@link Supplier} that provides the default value for the option when it is
82+
* not explicitly configured via an environment variable or system property.
83+
* @return The configured value for the option, obtained from the environment variable, system
84+
* property, or the fallback function, in that order of precedence.
85+
*/
86+
String getConfiguredValueWithFallback(Supplier<String> fallback) {
87+
try {
88+
return this.getConfiguredValue();
89+
} catch (ConfigurationException e) {
90+
return fallback.get();
91+
}
92+
}
93+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.gcp.auth;
7+
8+
import com.google.auth.oauth2.GoogleCredentials;
9+
import com.google.auto.service.AutoService;
10+
import io.opentelemetry.api.common.AttributeKey;
11+
import io.opentelemetry.api.common.Attributes;
12+
import io.opentelemetry.contrib.gcp.auth.GoogleAuthException.Reason;
13+
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
14+
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
15+
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
16+
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
17+
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
18+
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
19+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
20+
import io.opentelemetry.sdk.resources.Resource;
21+
import io.opentelemetry.sdk.trace.export.SpanExporter;
22+
import java.io.IOException;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
/**
27+
* An AutoConfigurationCustomizerProvider for Google Cloud Platform (GCP) OpenTelemetry (OTLP)
28+
* integration.
29+
*
30+
* <p>This class is registered as a service provider using {@link AutoService} and is responsible
31+
* for customizing the OpenTelemetry configuration for GCP specific behavior. It retrieves Google
32+
* Application Default Credentials (ADC) and adds them as authorization headers to the configured
33+
* {@link SpanExporter}. It also sets default properties and resource attributes for GCP
34+
* integration.
35+
*
36+
* @see AutoConfigurationCustomizerProvider
37+
* @see GoogleCredentials
38+
*/
39+
@AutoService(AutoConfigurationCustomizerProvider.class)
40+
public class GcpAuthAutoConfigurationCustomizerProvider
41+
implements AutoConfigurationCustomizerProvider {
42+
43+
static final String QUOTA_USER_PROJECT_HEADER = "X-Goog-User-Project";
44+
static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id";
45+
46+
/**
47+
* Customizes the provided {@link AutoConfigurationCustomizer}.
48+
*
49+
* <p>This method attempts to retrieve Google Application Default Credentials (ADC) and performs
50+
* the following: - Adds authorization headers to the configured {@link SpanExporter} based on the
51+
* retrieved credentials. - Adds default properties for OTLP endpoint and resource attributes for
52+
* GCP integration.
53+
*
54+
* @param autoConfiguration the AutoConfigurationCustomizer to customize.
55+
* @throws GoogleAuthException if there's an error retrieving Google Application Default
56+
* Credentials.
57+
* @throws io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException if required options are
58+
* not configured through environment variables or system properties.
59+
*/
60+
@Override
61+
public void customize(AutoConfigurationCustomizer autoConfiguration) {
62+
try {
63+
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
64+
autoConfiguration
65+
.addSpanExporterCustomizer(
66+
(exporter, configProperties) -> addAuthorizationHeaders(exporter, credentials))
67+
.addResourceCustomizer(GcpAuthAutoConfigurationCustomizerProvider::customizeResource);
68+
} catch (IOException e) {
69+
throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e);
70+
}
71+
}
72+
73+
@Override
74+
public int order() {
75+
return Integer.MAX_VALUE - 1;
76+
}
77+
78+
// Adds authorization headers to the calls made by the OtlpGrpcSpanExporter and
79+
// OtlpHttpSpanExporter.
80+
private static SpanExporter addAuthorizationHeaders(
81+
SpanExporter exporter, GoogleCredentials credentials) {
82+
if (exporter instanceof OtlpHttpSpanExporter) {
83+
OtlpHttpSpanExporterBuilder builder =
84+
((OtlpHttpSpanExporter) exporter)
85+
.toBuilder().setHeaders(() -> getRequiredHeaderMap(credentials));
86+
return builder.build();
87+
} else if (exporter instanceof OtlpGrpcSpanExporter) {
88+
OtlpGrpcSpanExporterBuilder builder =
89+
((OtlpGrpcSpanExporter) exporter)
90+
.toBuilder().setHeaders(() -> getRequiredHeaderMap(credentials));
91+
return builder.build();
92+
}
93+
return exporter;
94+
}
95+
96+
private static Map<String, String> getRequiredHeaderMap(GoogleCredentials credentials) {
97+
Map<String, String> gcpHeaders = new HashMap<>();
98+
try {
99+
credentials.refreshIfExpired();
100+
} catch (IOException e) {
101+
throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e);
102+
}
103+
gcpHeaders.put(QUOTA_USER_PROJECT_HEADER, credentials.getQuotaProjectId());
104+
gcpHeaders.put("Authorization", "Bearer " + credentials.getAccessToken().getTokenValue());
105+
return gcpHeaders;
106+
}
107+
108+
// Updates the current resource with the attributes required for ingesting OTLP data on GCP.
109+
private static Resource customizeResource(Resource resource, ConfigProperties configProperties) {
110+
String gcpProjectId =
111+
ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValueWithFallback(
112+
() -> {
113+
try {
114+
GoogleCredentials googleCredentials = GoogleCredentials.getApplicationDefault();
115+
return googleCredentials.getQuotaProjectId();
116+
} catch (IOException e) {
117+
throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e);
118+
}
119+
});
120+
121+
Resource res =
122+
Resource.create(
123+
Attributes.of(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), gcpProjectId));
124+
return resource.merge(res);
125+
}
126+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.gcp.auth;
7+
8+
/**
9+
* An unchecked exception indicating a failure during Google authentication. This exception is
10+
* thrown when there are issues with retrieving or refreshing Google Application Default Credentials
11+
* (ADC).
12+
*/
13+
public class GoogleAuthException extends RuntimeException {
14+
15+
private static final long serialVersionUID = 149908685226796448L;
16+
17+
/**
18+
* Constructs a new {@code GoogleAuthException} with the specified reason and cause.
19+
*
20+
* @param reason the reason for the authentication failure.
21+
* @param cause the underlying cause of the exception (e.g., an IOException).
22+
*/
23+
GoogleAuthException(Reason reason, Throwable cause) {
24+
super(reason.message, cause);
25+
}
26+
27+
/** Enumerates the possible reasons for a Google authentication failure. */
28+
enum Reason {
29+
/** Indicates a failure to retrieve Google Application Default Credentials. */
30+
FAILED_ADC_RETRIEVAL("Unable to retrieve Google Application Default Credentials."),
31+
/** Indicates a failure to retrieve Google Application Default Credentials. */
32+
FAILED_ADC_REFRESH("Unable to refresh Google Application Default Credentials.");
33+
34+
private final String message;
35+
36+
/**
37+
* Constructs a new {@code Reason} with the specified message.
38+
*
39+
* @param message the message describing the reason.
40+
*/
41+
Reason(String message) {
42+
this.message = message;
43+
}
44+
45+
/**
46+
* Returns the message associated with this reason.
47+
*
48+
* @return the message describing the reason.
49+
*/
50+
public String getMessage() {
51+
return message;
52+
}
53+
}
54+
}

gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/Main.java

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)