Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7082247
feat: introduce minimal implementation of OTel tracing
diegomarquezp Feb 4, 2026
bfe6a6d
chore: avoid new public methods
diegomarquezp Feb 5, 2026
ea6cb18
feat: add api tracer context
diegomarquezp Feb 5, 2026
97bec08
Merge remote-tracking branch 'origin/main' into observability/initial…
diegomarquezp Feb 5, 2026
097f701
chore: concise comment
diegomarquezp Feb 5, 2026
f547e5a
fix: use internal span kind for operations
diegomarquezp Feb 5, 2026
b7b9e31
chore: remove unnecessary inScope implementation
diegomarquezp Feb 5, 2026
556c84c
Revert "chore: remove unnecessary inScope implementation"
diegomarquezp Feb 5, 2026
0359b7d
test: add test for inScope()
diegomarquezp Feb 5, 2026
402cb89
Merge remote-tracking branch 'origin/main' into observability/initial…
diegomarquezp Feb 5, 2026
420278b
chore: use suggested server address resolution impl
diegomarquezp Feb 5, 2026
6de9fb6
fix: use concurrent hash map
diegomarquezp Feb 5, 2026
a61dacf
chore: remove default impl for startSpan with parent
diegomarquezp Feb 5, 2026
b000d52
chore: add opentelemery-context to tests
diegomarquezp Feb 5, 2026
9ce0c34
test: increase coverage for TracingTracerTest
diegomarquezp Feb 5, 2026
9c6c737
deps: include opentelemetry context in gax
diegomarquezp Feb 5, 2026
e77de58
chore: simplify and remove error handling
diegomarquezp Feb 9, 2026
8f07f61
chore: review refactor
diegomarquezp Feb 10, 2026
2cfcd68
chore: make TracingTracerFactory(recorder, opAtts, atAtts) package pr…
diegomarquezp Feb 10, 2026
2415c6b
chore: remove unnecessary inScope
diegomarquezp Feb 10, 2026
0f5e9b5
chore: rename startSpan to createSpan
diegomarquezp Feb 10, 2026
0c3bd22
chore: use server address instead of endpoint context
diegomarquezp Feb 10, 2026
bdeb291
chore: rename classes
diegomarquezp Feb 10, 2026
5c07a3c
chore: add javadoc for tracer
diegomarquezp Feb 10, 2026
ff1d45d
chore: rename to TraceSpan, improve javadocs
diegomarquezp Feb 10, 2026
043ce62
chore: format
diegomarquezp Feb 10, 2026
dbc5f41
Update gax-java/gax/src/main/java/com/google/api/gax/tracing/AppCentr…
diegomarquezp Feb 10, 2026
b4bce0d
chore: handle ipv6 in endpoint context
diegomarquezp Feb 10, 2026
6e95cf4
Merge branch 'observability/initial-tracing-impl' of https://github.c…
diegomarquezp Feb 10, 2026
e873904
chore: add language tests
diegomarquezp Feb 10, 2026
68943b0
chore: AppCentricTracer to implement interface
diegomarquezp Feb 11, 2026
ba3c080
feat(obs): generate gapic.properties file with repo property
diegomarquezp Feb 12, 2026
caead0d
feat: implement repo handling in gax
diegomarquezp Feb 12, 2026
58f72d2
test: add tests for repo property
diegomarquezp Feb 12, 2026
5a5d9d6
chore: speed up image building
diegomarquezp Feb 12, 2026
015aa88
build: introduce repo property to generation config
diegomarquezp Feb 12, 2026
0907e93
chore: update showcase module
diegomarquezp Feb 12, 2026
cd4d4d0
chore: revert operation implementation
diegomarquezp Feb 12, 2026
5b76e40
chore: extract common span attributes to separate class
diegomarquezp Feb 12, 2026
c783d68
chore: remove unused var
diegomarquezp Feb 12, 2026
1eeb197
Merge branch 'observability/initial-tracing-impl' into observability/…
diegomarquezp Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ RUN cat /java-formatter-version
RUN V=$(cat /java-formatter-version) && curl -o "/google-java-format.jar" "https://maven-central.storage-download.googleapis.com/maven2/com/google/googlejavaformat/google-java-format/${V}/google-java-format-${V}-all-deps.jar"

# Compile and install packages
RUN mvn install -B -ntp -DskipTests -Dclirr.skip -Dcheckstyle.skip
RUN mvn install -B -ntp -T 1.5C -DskipTests -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip
RUN cp "/root/.m2/repository/com/google/api/gapic-generator-java/${DOCKER_GAPIC_GENERATOR_VERSION}/gapic-generator-java-${DOCKER_GAPIC_GENERATOR_VERSION}.jar" \
"./gapic-generator-java.jar"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,14 @@ static GapicMetadata defaultGapicMetadata() {

public abstract Transport transport();

public abstract Optional<String> repo();

public static Builder builder() {
return new AutoValue_GapicContext.Builder()
.setMixinServices(Collections.emptyList())
.setGapicMetadataEnabled(false)
.setRestNumericEnumsEnabled(false);
.setRestNumericEnumsEnabled(false)
.setRepo(Optional.empty());
}

@AutoValue.Builder
Expand Down Expand Up @@ -130,6 +133,8 @@ public Builder setHelperResourceNames(Set<ResourceName> helperResourceNames) {

public abstract Builder setTransport(Transport transport);

public abstract Builder setRepo(Optional<String> repo);

abstract ImmutableMap<String, ResourceName> resourceNames();

abstract ImmutableMap<String, ResourceName> helperResourceNames();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public static GapicContext parse(CodeGeneratorRequest request) {
Optional<GapicLanguageSettings> languageSettingsOpt =
GapicLanguageSettingsParser.parse(gapicYamlConfigPathOpt);
Optional<String> transportOpt = PluginArgumentParser.parseTransport(request);
Optional<String> repoOpt = PluginArgumentParser.parseRepo(request);

boolean willGenerateMetadata = PluginArgumentParser.hasMetadataFlag(request);
boolean willGenerateNumericEnum = PluginArgumentParser.hasNumericEnumFlag(request);
Expand Down Expand Up @@ -253,6 +254,7 @@ public static GapicContext parse(CodeGeneratorRequest request) {
.setServiceYamlProto(serviceYamlProtoOpt.orElse(null))
.setTransport(transport)
.setRestNumericEnumsEnabled(willGenerateNumericEnum)
.setRepo(repoOpt)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class PluginArgumentParser {
@VisibleForTesting static final String KEY_NUMERIC_ENUM = "rest-numeric-enums";
@VisibleForTesting static final String KEY_SERVICE_YAML_CONFIG = "api-service-config";
@VisibleForTesting static final String KEY_TRANSPORT = "transport";
@VisibleForTesting static final String KEY_REPO = "repo";

private static final String JSON_FILE_ENDING = "grpc_service_config.json";
private static final String GAPIC_YAML_FILE_ENDING = "gapic.yaml";
Expand All @@ -53,6 +54,10 @@ static Optional<String> parseTransport(CodeGeneratorRequest request) {
return parseConfigArgument(request.getParameter(), KEY_TRANSPORT);
}

static Optional<String> parseRepo(CodeGeneratorRequest request) {
return parseConfigArgument(request.getParameter(), KEY_REPO);
}
Comment on lines +57 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The parseRepo method uses parseConfigArgument, which parses plugin arguments by splitting the input string by commas. If the repo value contains a comma, it will be split, potentially allowing an attacker to inject additional plugin flags or options (e.g., injecting ,metadata to enable metadata generation).


static boolean hasMetadataFlag(CodeGeneratorRequest request) {
return hasFlag(request.getParameter(), KEY_METADATA);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ protected static CodeGeneratorResponse write(

writeMetadataFile(context, writePackageInfo(gapicPackageInfo, codeWriter, jos), jos);
writeReflectConfigFile(gapicPackageInfo.packageInfo().pakkage(), reflectConfigInfo, jos);
writeGapicPropertiesFile(context, jos);

jos.finish();
jos.flush();
Expand Down Expand Up @@ -212,6 +213,22 @@ private static void writeMetadataFile(GapicContext context, String path, JarOutp
}
}

@VisibleForTesting
static void writeGapicPropertiesFile(GapicContext context, JarOutputStream jos) {
context
.repo()
.ifPresent(
repo -> {
JarEntry jarEntry = new JarEntry("src/main/resources/gapic.properties");
try {
jos.putNextEntry(jarEntry);
jos.write(String.format("repo=%s\n", repo).getBytes(StandardCharsets.UTF_8));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The writeGapicPropertiesFile method writes the repo string directly into a properties file using String.format("repo=%s\n", repo). If the repo string (which originates from user-supplied plugin arguments) contains a newline character, it can be used to inject arbitrary properties into the gapic.properties file. This could potentially influence the behavior of other components that read this configuration file.

} catch (IOException e) {
throw new GapicWriterException("Could not write repo file", e);
}
});
}

private static String getPath(String pakkage, String className) {
String path = pakkage.replaceAll("\\.", "/");
if (className.startsWith("Mock") || className.endsWith("Test")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import static com.google.api.generator.gapic.protoparser.PluginArgumentParser.KEY_METADATA;
import static com.google.api.generator.gapic.protoparser.PluginArgumentParser.KEY_NUMERIC_ENUM;
import static com.google.api.generator.gapic.protoparser.PluginArgumentParser.KEY_REPO;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
Expand Down Expand Up @@ -269,6 +270,21 @@ void hasFlag_flagFound() {
assertTrue(PluginArgumentParser.hasFlag(rawArgument, KEY_METADATA));
}

@Test
void parseRepo_onlyOnePresent() {
String repo = "googleapis/sdk-platform-java";
CodeGeneratorRequest request =
CodeGeneratorRequest.newBuilder().setParameter(createRepo(repo)).build();
assertEquals(repo, PluginArgumentParser.parseRepo(request).get());
}

@Test
void parseRepo_noneFound() {
CodeGeneratorRequest request =
CodeGeneratorRequest.newBuilder().setParameter("metadata").build();
assertFalse(PluginArgumentParser.parseRepo(request).isPresent());
}

private static String createGrpcServiceConfig(String path) {
return String.format("%s=%s", PluginArgumentParser.KEY_GRPC_SERVICE_CONFIG, path);
}
Expand All @@ -280,4 +296,8 @@ private static String createGapicConfig(String path) {
private static String createServiceConfig(String path) {
return String.format("%s=%s", PluginArgumentParser.KEY_SERVICE_YAML_CONFIG, path);
}

private static String createRepo(String repo) {
return String.format("%s=%s", KEY_REPO, repo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.google.gson.Gson;
import com.google.protobuf.ByteString;
import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
Expand All @@ -25,6 +26,7 @@
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
Expand Down Expand Up @@ -144,4 +146,26 @@ void productionWrite_emptyGapicContext_succeeds() throws IOException {
"temp-codegen.srcjar");
assertNull(result);
}

@Test
void writeRepoFile_isWritten() throws IOException {
String repo = "googleapis/sdk-platform-java";
GapicContext context = GapicContext.EMPTY.toBuilder().setRepo(Optional.of(repo)).build();
Writer.writeGapicPropertiesFile(context, jarOutputStream);

closeJarOutputStream();

try (JarFile jarFile = new JarFile(file)) {
Enumeration<JarEntry> entries = jarFile.entries();
assertThat(entries.hasMoreElements()).isTrue();
JarEntry entry = entries.nextElement();
assertThat(entries.hasMoreElements()).isFalse();
assertEquals("src/main/resources/gapic.properties", entry.getName());
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(jarFile.getInputStream(entry)))) {
String line = reader.readLine();
assertEquals("repo=" + repo, line);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.google.api.gax.core.ExecutorAsBackgroundResource;
import com.google.api.gax.core.ExecutorProvider;
import com.google.api.gax.rpc.internal.QuotaProjectIdHidingCredentials;
import com.google.api.gax.tracing.ApiTracerContext;
import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.api.gax.tracing.BaseApiTracerFactory;
import com.google.auth.ApiKeyCredentials;
Expand Down Expand Up @@ -269,6 +270,11 @@ public static ClientContext create(StubSettings settings) throws IOException {
if (watchdogProvider != null && watchdogProvider.shouldAutoClose()) {
backgroundResources.add(watchdog);
}
ApiTracerContext apiTracerContext =
ApiTracerContext.newBuilder()
.setServerAddress(endpointContext.resolvedServerAddress())
.build();
ApiTracerFactory apiTracerFactory = settings.getTracerFactory().withContext(apiTracerContext);

return newBuilder()
.setBackgroundResources(backgroundResources.build())
Expand All @@ -284,7 +290,7 @@ public static ClientContext create(StubSettings settings) throws IOException {
.setQuotaProjectId(settings.getQuotaProjectId())
.setStreamWatchdog(watchdog)
.setStreamWatchdogCheckIntervalDuration(settings.getStreamWatchdogCheckIntervalDuration())
.setTracerFactory(settings.getTracerFactory())
.setTracerFactory(apiTracerFactory)
.setEndpointContext(endpointContext)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.net.HostAndPort;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -133,6 +134,8 @@ public static EndpointContext getDefaultInstance() {

public abstract String resolvedEndpoint();

public abstract String resolvedServerAddress();

public abstract Builder toBuilder();

public static Builder newBuilder() {
Expand Down Expand Up @@ -228,6 +231,8 @@ public abstract static class Builder {

public abstract Builder setResolvedEndpoint(String resolvedEndpoint);

public abstract Builder setResolvedServerAddress(String serverAddress);

public abstract Builder setResolvedUniverseDomain(String resolvedUniverseDomain);

abstract Builder setUseS2A(boolean useS2A);
Expand Down Expand Up @@ -382,6 +387,23 @@ boolean shouldUseS2A() {
return mtlsEndpoint().contains(Credentials.GOOGLE_DEFAULT_UNIVERSE);
}

private String parseServerAddress(String endpoint) {
if (Strings.isNullOrEmpty(endpoint)) {
return endpoint;
}
String hostPort = endpoint;
if (hostPort.contains("://")) {
// Strip the scheme if present. HostAndPort doesn't support schemes.
hostPort = hostPort.substring(hostPort.indexOf("://") + 3);
}
try {
return HostAndPort.fromString(hostPort).getHost();
} catch (IllegalArgumentException e) {
// Fallback for cases HostAndPort can't handle.
return hostPort;
}
}

// Default to port 443 for HTTPS. Using HTTP requires explicitly setting the endpoint
private String buildEndpointTemplate(String serviceName, String resolvedUniverseDomain) {
return serviceName + "." + resolvedUniverseDomain + ":443";
Expand Down Expand Up @@ -416,7 +438,9 @@ String mtlsEndpointResolver(
public EndpointContext build() throws IOException {
// The Universe Domain is used to resolve the Endpoint. It should be resolved first
setResolvedUniverseDomain(determineUniverseDomain());
setResolvedEndpoint(determineEndpoint());
String endpoint = determineEndpoint();
setResolvedEndpoint(endpoint);
setResolvedServerAddress(parseServerAddress(endpoint));
setUseS2A(shouldUseS2A());
return autoBuild();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2026 Google LLC
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google LLC nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package com.google.api.gax.tracing;

import com.google.api.core.InternalApi;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
* A context object that contains information used to infer attributes that are common for all
* {@link ApiTracer}s.
*
* <p>For internal use only.
*/
@InternalApi
@AutoValue
public abstract class ApiTracerContext {
private static final Logger LOGGER = Logger.getLogger(ApiTracerContext.class.getName());
private static final String GAPIC_PROPERTIES_FILE = "/gapic.properties";
private static final String REPO_KEY = "repo";

@Nullable
public abstract String getServerAddress();

@Nullable
public abstract String getRepo();

public static Builder newBuilder() {
return newBuilder(ApiTracerContext.class.getResourceAsStream(GAPIC_PROPERTIES_FILE));
}

@VisibleForTesting
static Builder newBuilder(@Nullable InputStream inputStream) {
Builder builder = new AutoValue_ApiTracerContext.Builder();
loadRepoFromProperties(builder, inputStream);
return builder;
}

private static void loadRepoFromProperties(Builder builder, @Nullable InputStream is) {
if (is == null) {
return;
}
try {
Properties properties = new Properties();
properties.load(is);
builder.setRepo(properties.getProperty(REPO_KEY));
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Could not load gapic.properties", e);
} finally {
try {
is.close();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Could not close gapic.properties stream", e);
}
}
}
Comment on lines 73 to 90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The loadRepoFromProperties method can be simplified by using a try-with-resources statement. This is the modern and recommended way to handle resources like InputStream in Java, as it ensures the stream is closed correctly and makes the code more concise and readable.

  private static void loadRepoFromProperties(Builder builder, @Nullable InputStream is) {
    if (is == null) {
      return;
    }
    try (InputStream inputStream = is) {
      Properties properties = new Properties();
      properties.load(inputStream);
      builder.setRepo(properties.getProperty(REPO_KEY));
    } catch (IOException e) {
      LOGGER.log(Level.WARNING, "Could not load or close gapic.properties", e);
    }
  }


@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setServerAddress(String serverAddress);

public abstract Builder setRepo(String repo);

public abstract ApiTracerContext build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,15 @@ enum OperationType {
* @param operationType the type of operation that the tracer will trace
*/
ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType);

/**
* Returns a new {@link ApiTracerFactory} that will use the provided context to infer attributes
* for all tracers created by the factory.
*
* @param context an {@link ApiTracerContext} object containing information to construct
* attributes
*/
default ApiTracerFactory withContext(ApiTracerContext context) {
return this;
}
}
Loading
Loading