Repository: GoogleCloudPlatform/opentelemetry-operations-java (java)

Issue:
Cloud Run Jobs are detected as gcp_compute_engine
Currently, workloads running as Cloud Run Jobs are being detected as GCP compute engine - this is because the resource detection does not support Cloud Run Jobs yet which leads to resource detection logic fall back to `gcp_compute_engine`.

Environment variables in a Cloud Run Job container differ from the environment variables set when the container is run as a Cloud Run Service.

Details around the differences between environment variables are documented [here](https://cloud.google.com/run/docs/container-contract#env-vars).

Update resource detection logic to support Cloud Run Job detection.

Pull request:
Support cloud run jobs in resource detection
### Description
Adds support for detecting Cloud Run Jobs environment.

### Testing
 - Added unit tests pass + Build work.
 - Tested with manually built resource-detection library.
      - Debug exporter export output. 
`
INFO: metric: ImmutableMetricData{resource=Resource{schemaUrl=null, attributes={cloud.account.id="otel-test", cloud.platform="gcp_cloud_run", cloud.provider="gcp", cloud.region="us-central1", faas.instance="003fb3e0c8b947e4481e1e7e76dc3c594af483befe470a0bbb6fff27f96fa9caa77e6e202dcea43a646942", faas.name="job-metrics-export", gcp.cloud_run.job.execution="job-metrics-export-sprng", gcp.cloud_run.job.task_index=0}}, instrumentationScopeInfo=InstrumentationScopeInfo{name=instrumentation-library-name, version=semver:1.0.0, schemaUrl=null, attributes={}}, name=example_counter, description=Processed jobs, unit=1, type=LONG_SUM, data=ImmutableSumData{points=[ImmutableLongPointData{startEpochNanos=1726685490477889000, epochNanos=1726685512168409000, attributes={}, value=968, exemplars=[]}], monotonic=true, aggregationTemporality=CUMULATIVE}}
` 

Re #369 

Patch:
diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/AttributeKeys.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/AttributeKeys.java
index c7a45ac..3ea75c4 100644
--- a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/AttributeKeys.java
+++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/AttributeKeys.java
@@ -52,6 +52,10 @@ public final class AttributeKeys {
   public static final String SERVERLESS_COMPUTE_CLOUD_REGION = AttributeKeys.CLOUD_REGION;
   public static final String SERVERLESS_COMPUTE_INSTANCE_ID = AttributeKeys.INSTANCE_ID;
 
+  // Cloud Run Job Specific Attributes
+  public static final String GCR_JOB_EXECUTION_KEY = "gcr_job_execution_key";
+  public static final String GCR_JOB_TASK_INDEX = "gcr_job_task_index";
+
   static final String AVAILABILITY_ZONE = "availability_zone";
   static final String CLOUD_REGION = "cloud_region";
   static final String INSTANCE_ID = "instance_id";
diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetector.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetector.java
index 414f9b5..78e1082 100644
--- a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetector.java
+++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetector.java
@@ -53,6 +53,8 @@ public class GCPPlatformDetector {
       return SupportedPlatform.GOOGLE_CLOUD_RUN;
     } else if (environmentVariables.get("FUNCTION_TARGET") != null) {
       return SupportedPlatform.GOOGLE_CLOUD_FUNCTIONS;
+    } else if (environmentVariables.get("CLOUD_RUN_JOB") != null) {
+      return SupportedPlatform.GOOGLE_CLOUD_RUN_JOB;
     } else if (environmentVariables.get("GAE_SERVICE") != null) {
       return SupportedPlatform.GOOGLE_APP_ENGINE;
     }
@@ -75,6 +77,9 @@ public class GCPPlatformDetector {
       case GOOGLE_CLOUD_FUNCTIONS:
         detectedPlatform = new GoogleCloudFunction(environmentVariables, metadataConfig);
         break;
+      case GOOGLE_CLOUD_RUN_JOB:
+        detectedPlatform = new GoogleCloudRunJob(environmentVariables, metadataConfig);
+        break;
       case GOOGLE_APP_ENGINE:
         detectedPlatform = new GoogleAppEngine(environmentVariables, metadataConfig);
         break;
@@ -98,8 +103,10 @@ public class GCPPlatformDetector {
     GOOGLE_KUBERNETES_ENGINE,
     /** Represents the Google App Engine platform. Could either be flex or standard. */
     GOOGLE_APP_ENGINE,
-    /** Represents the Google Cloud Run platform. */
+    /** Represents the Google Cloud Run platform (Service). */
     GOOGLE_CLOUD_RUN,
+    /** Represents the Google Cloud Run platform (Jobs). */
+    GOOGLE_CLOUD_RUN_JOB,
     /** Represents the Google Cloud Functions platform. */
     GOOGLE_CLOUD_FUNCTIONS,
     /** Represents the case when the application is not running on GCP. */
diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudRunJob.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudRunJob.java
new file mode 100644
index 0000000..08f6a4a
--- /dev/null
+++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudRunJob.java
@@ -0,0 +1,58 @@
+/*
+ * 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.detection;
+
+import java.util.HashMap;
+import java.util.Map;
+
+final class GoogleCloudRunJob implements DetectedPlatform {
+  private final GCPMetadataConfig metadataConfig;
+  private final EnvironmentVariables environmentVariables;
+  private final Map<String, String> availableAttributes;
+
+  GoogleCloudRunJob(EnvironmentVariables environmentVariables, GCPMetadataConfig metadataConfig) {
+    this.metadataConfig = metadataConfig;
+    this.environmentVariables = environmentVariables;
+    this.availableAttributes = prepareAttributes();
+  }
+
+  private Map<String, String> prepareAttributes() {
+    Map<String, String> map = new HashMap<>();
+    map.put(AttributeKeys.SERVERLESS_COMPUTE_NAME, this.environmentVariables.get("CLOUD_RUN_JOB"));
+    map.put(
+        AttributeKeys.GCR_JOB_EXECUTION_KEY, this.environmentVariables.get("CLOUD_RUN_EXECUTION"));
+    map.put(
+        AttributeKeys.GCR_JOB_TASK_INDEX, this.environmentVariables.get("CLOUD_RUN_TASK_INDEX"));
+    map.put(AttributeKeys.SERVERLESS_COMPUTE_INSTANCE_ID, this.metadataConfig.getInstanceId());
+    map.put(AttributeKeys.SERVERLESS_COMPUTE_CLOUD_REGION, this.metadataConfig.getRegionFromZone());
+    return map;
+  }
+
+  @Override
+  public GCPPlatformDetector.SupportedPlatform getSupportedPlatform() {
+    return GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_RUN_JOB;
+  }
+
+  @Override
+  public String getProjectId() {
+    return metadataConfig.getProjectId();
+  }
+
+  @Override
+  public Map<String, String> getAttributes() {
+    return this.availableAttributes;
+  }
+}
diff --git a/examples/metrics/src/main/java/com/google/cloud/opentelemetry/example/metrics/MetricsExporterExample.java b/examples/metrics/src/main/java/com/google/cloud/opentelemetry/example/metrics/MetricsExporterExample.java
index fd463f0..a97df0f 100644
--- a/examples/metrics/src/main/java/com/google/cloud/opentelemetry/example/metrics/MetricsExporterExample.java
+++ b/examples/metrics/src/main/java/com/google/cloud/opentelemetry/example/metrics/MetricsExporterExample.java
@@ -30,6 +30,7 @@ import io.opentelemetry.api.metrics.LongCounter;
 import io.opentelemetry.api.metrics.Meter;
 import io.opentelemetry.contrib.gcp.resource.GCPResourceProvider;
 import io.opentelemetry.exporter.logging.LoggingMetricExporter;
+import io.opentelemetry.sdk.common.CompletableResultCode;
 import io.opentelemetry.sdk.metrics.SdkMeterProvider;
 import io.opentelemetry.sdk.metrics.export.MetricExporter;
 import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
@@ -37,6 +38,7 @@ import io.opentelemetry.sdk.resources.Resource;
 import java.io.IOException;
 import java.time.Duration;
 import java.util.Random;
+import java.util.concurrent.TimeUnit;
 
 public class MetricsExporterExample {
   private static SdkMeterProvider METER_PROVIDER;
@@ -146,9 +148,16 @@ public class MetricsExporterExample {
     } finally {
       System.out.println("Shutting down the metrics-example application");
 
-      METER_PROVIDER.shutdown();
+      CompletableResultCode resultCode = METER_PROVIDER.shutdown();
+      // Wait upto 60 seconds for job to complete
+      resultCode.join(60, TimeUnit.SECONDS);
+      if (resultCode.isSuccess()) {
+        System.out.println("Shutdown completed successfully!");
+      } else {
+        System.out.println("Unable to shutdown gracefully!");
+      }
 
-      System.out.println("Shutdown complete");
+      System.out.println("Exiting job");
     }
   }
 }
diff --git a/exporters/auto/README.md b/exporters/auto/README.md
index e9687ef..e5f1f63 100644
--- a/exporters/auto/README.md
+++ b/exporters/auto/README.md
@@ -15,7 +15,7 @@ To leverage the GCP extensions to this, simply add the exporter-auto dependency
 <dependency>
   <groupId>com.google.cloud.opentelemetry</groupId>
   <artifactId>exporter-auto</artifactId>
-  <version>0.27.0-alpha</version>
+  <version>0.31.0-alpha</version>
   <!-- Add the classifier if you explicitly wish to use the shaded variant -->
   <classifier>shaded</classifier>
 </dependency>
@@ -23,12 +23,12 @@ To leverage the GCP extensions to this, simply add the exporter-auto dependency
 
 #### Gradle
 ```groovy
-implementation "com.google.cloud.opentelemetry:exporter-auto:0.27.0-alpha"
+implementation "com.google.cloud.opentelemetry:exporter-auto:0.31.0-alpha"
 ```
 
 To use the shaded variant with Gradle,
 ```groovy
-implementation "com.google.cloud.opentelemetry:exporter-auto:0.27.0-alpha:shaded"
+implementation "com.google.cloud.opentelemetry:exporter-auto:0.31.0-alpha:shaded"
 ```
 
 #### Shaded Local JAR
@@ -38,9 +38,9 @@ You can download the shaded JAR for `exporter-auto` from the following link -
 https://repo1.maven.org/maven2/com/google/cloud/opentelemetry/exporter-auto/<VERSION>-alpha/exporter-auto-<VERSION>-alpha-shaded.jar
 ```
 Replace `<VERSION>` with the version you wish to download. For instance, shaded
-variant for `v0.27.0`, will be found at -
+variant for `v0.31.0`, will be found at -
 
-`https://repo1.maven.org/maven2/com/google/cloud/opentelemetry/exporter-auto/0.27.0-alpha/exporter-auto-0.27.0-alpha-shaded.jar`
+`https://repo1.maven.org/maven2/com/google/cloud/opentelemetry/exporter-auto/0.31.0-alpha/exporter-auto-0.31.0-alpha-shaded.jar`
 
 **Note: Make sure to use the latest release [![LatestRelease][maven-image]][maven-url].**
 
diff --git a/exporters/trace/README.md b/exporters/trace/README.md
index 50f923e..1e3a4c2 100644
--- a/exporters/trace/README.md
+++ b/exporters/trace/README.md
@@ -29,28 +29,39 @@ config:
 <dependency>
   <groupId>com.google.cloud.opentelemetry</groupId>
   <artifactId>exporter-trace</artifactId>
-  <version>0.20.0</version>
+  <version>0.31.0</version>
 </dependency>
 ```
 
 ### Usage
-  If you are running in a GCP environment, the exporter will automatically authenticate using the environment's service account. If not, you will need to follow the instructions in Authentication.  
+If you are running in a GCP environment, the exporter will automatically authenticate using the environment's service account. If not, you will need to follow the instructions in Authentication.
+
+See [the code example](../../examples/trace) for details.
 
 #### Create the exporter
 
-You can create exporter and register it in the OpenTelemetry SDK using the default configuration as follows:
+You can create exporter using the default configuration as follows:
 
 ```java
-    TraceExporter traceExporter = TraceExporter.createWithDefaultConfiguration();
-    OpenTelemetrySdk.getTracerProvider().addSpanProcessor(SimpleSpanProcessor.newBuilder(traceExporter).build());
+    SpanExporter traceExporter = TraceExporter.createWithDefaultConfiguration(); 
 ```
 
 You can also customize the configuration using a TraceConfiguration object
 ```java
-    TraceExporter traceExporter = TraceExporter.createWithConfiguration(
+    SpanExporter traceExporter = TraceExporter.createWithConfiguration(
       TraceConfiguration.builder().setProjectId("myCoolGcpProject").build()
     );
-    OpenTelemetrySdk.getTracerProvider().addSpanProcessor(SimpleSpanProcessor.newBuilder(traceExporter).build());
+```
+
+You can register a `SpanExporter` with your OpenTelemetry instance as follows:
+
+```java
+  OpenTelemetrySdk.builder()
+        .setTracerProvider(
+            SdkTracerProvider.builder()
+                .addSpanProcessor(BatchSpanProcessor.builder(traceExporter).build())
+                .build())
+        .build();
 ```
 
 #### Specifying a project ID


Test patch:
diff --git a/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetectorTest.java b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetectorTest.java
index 57200ee..2cdaf11 100644
--- a/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetectorTest.java
+++ b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetectorTest.java
@@ -274,10 +274,10 @@ public class GCPPlatformDetectorTest {
         detector.detectPlatform().getAttributes());
   }
 
-  /** Google Cloud Run Tests * */
+  /** Google Cloud Run Tests (Service) * */
   @Test
   public void testGCFResourceWithCloudRunAttributesSucceeds() {
-    // Setup GCR required env vars
+    // Setup GCR service required env vars
     envVars.put("K_SERVICE", "cloud-run-hello");
     envVars.put("K_REVISION", "cloud-run-hello.1");
     envVars.put("K_CONFIGURATION", "cloud-run-hello");
@@ -306,6 +306,37 @@ public class GCPPlatformDetectorTest {
     assertEquals("GCR-instance-id", detectedAttributes.get(SERVERLESS_COMPUTE_INSTANCE_ID));
   }
 
+  /** Google Cloud Run Tests (Jobs) * */
+  @Test
+  public void testCloudRunJobResourceWithAttributesSucceeds() {
+    // Setup GCR Job required env vars
+    envVars.put("CLOUD_RUN_JOB", "cloud-run-hello-job");
+    envVars.put("CLOUD_RUN_EXECUTION", "cloud-run-hello-job-1a2b3c");
+    envVars.put("CLOUD_RUN_TASK_INDEX", "0");
+
+    TestUtils.stubEndpoint("/project/project-id", "GCR-pid");
+    TestUtils.stubEndpoint("/instance/zone", "country-region-zone");
+    TestUtils.stubEndpoint("/instance/id", "GCR-job-instance-id");
+
+    EnvironmentVariables mockEnv = new EnvVarMock(envVars);
+    GCPPlatformDetector detector = new GCPPlatformDetector(mockMetadataConfig, mockEnv);
+
+    Map<String, String> detectedAttributes = detector.detectPlatform().getAttributes();
+    assertEquals(
+        GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_RUN_JOB,
+        detector.detectPlatform().getSupportedPlatform());
+    assertEquals(
+        new GoogleCloudRunJob(mockEnv, mockMetadataConfig).getAttributes(), detectedAttributes);
+    assertEquals("GCR-pid", detector.detectPlatform().getProjectId());
+    assertEquals(5, detectedAttributes.size());
+
+    assertEquals("cloud-run-hello-job-1a2b3c", detectedAttributes.get(GCR_JOB_EXECUTION_KEY));
+    assertEquals("0", detectedAttributes.get(GCR_JOB_TASK_INDEX));
+    assertEquals("cloud-run-hello-job", detectedAttributes.get(SERVERLESS_COMPUTE_NAME));
+    assertEquals("country-region", detectedAttributes.get(SERVERLESS_COMPUTE_CLOUD_REGION));
+    assertEquals("GCR-job-instance-id", detectedAttributes.get(SERVERLESS_COMPUTE_INSTANCE_ID));
+  }
+
   /** Google App Engine Tests * */
   @ParameterizedTest
   @MethodSource("provideGAEVariantEnvironmentVariable")