diff --git a/README.md b/README.md index 1ff44801..41bb9c1e 100644 --- a/README.md +++ b/README.md @@ -47,5 +47,5 @@ See [Metrics Readme](exporters/metrics/README.md) for installation and usage ins See [Autoconfigure Readme](exporters/auto/README.md) for installation and usage instructions. -[maven-image]: https://maven-badges.herokuapp.com/maven-central/com.google.cloud.opentelemetry/exporter-trace/badge.svg -[maven-url]: https://maven-badges.herokuapp.com/maven-central/com.google.cloud.opentelemetry/exporter-trace +[maven-image]: https://img.shields.io/maven-central/v/com.google.cloud.opentelemetry/exporter-trace?color=dark-green +[maven-url]: https://central.sonatype.com/artifact/com.google.cloud.opentelemetry/exporter-trace diff --git a/detectors/resources-support/README.md b/detectors/resources-support/README.md index 9f33815c..d1ac8f89 100644 --- a/detectors/resources-support/README.md +++ b/detectors/resources-support/README.md @@ -10,5 +10,5 @@ The GCP environments supported by this library are - 4. [Google Cloud Functions](https://cloud.google.com/functions?hl=en) 5. [Google Cloud Run](https://cloud.google.com/run?hl=en) -[maven-image]: https://maven-badges.herokuapp.com/maven-central/com.google.cloud.opentelemetry/detector-resources-support/badge.svg -[maven-url]: https://maven-badges.herokuapp.com/maven-central/com.google.cloud.opentelemetry/detector-resources-support \ No newline at end of file +[maven-image]: https://img.shields.io/maven-central/v/com.google.cloud.opentelemetry/detector-resources-support?color=dark-green +[maven-url]: https://central.sonatype.com/artifact/com.google.cloud.opentelemetry/detector-resources-support diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfig.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfig.java index 09cf39f4..a11384b5 100644 --- a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfig.java +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfig.java @@ -22,8 +22,8 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * Retrieves Google Cloud project-id and a limited set of instance attributes from Metadata server. @@ -36,7 +36,7 @@ final class GCPMetadataConfig { private static final String DEFAULT_URL = "http://metadata.google.internal/computeMetadata/v1/"; private final String url; - private final Map cachedAttributes = new HashMap<>(); + private final Map cachedAttributes = new ConcurrentHashMap<>(); private GCPMetadataConfig() { this.url = DEFAULT_URL; 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 47f29675..57200eee 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 @@ -309,16 +309,20 @@ public void testGCFResourceWithCloudRunAttributesSucceeds() { /** Google App Engine Tests * */ @ParameterizedTest @MethodSource("provideGAEVariantEnvironmentVariable") - public void testGAEResourceWithAppEngineAttributesSucceeds(String gaeEnvironmentVar) { + public void testGAEResourceWithAppEngineAttributesSucceeds( + String gaeEnvironmentVar, + String metadataZone, + String expectedZone, + String metadataRegion, + String expectedRegion) { envVars.put("GAE_SERVICE", "app-engine-hello"); envVars.put("GAE_VERSION", "app-engine-hello-v1"); envVars.put("GAE_INSTANCE", "app-engine-hello-f236d"); envVars.put("GAE_ENV", gaeEnvironmentVar); TestUtils.stubEndpoint("/project/project-id", "GAE-pid"); - // for standard, the region should be extracted from region attribute - TestUtils.stubEndpoint("/instance/zone", "country-region-zone"); - TestUtils.stubEndpoint("/instance/region", "country-region1"); + TestUtils.stubEndpoint("/instance/zone", metadataZone); + TestUtils.stubEndpoint("/instance/region", metadataRegion); TestUtils.stubEndpoint("/instance/id", "GAE-instance-id"); EnvironmentVariables mockEnv = new EnvVarMock(envVars); @@ -332,27 +336,47 @@ public void testGAEResourceWithAppEngineAttributesSucceeds(String gaeEnvironment new GoogleAppEngine(mockEnv, mockMetadataConfig).getAttributes(), detectedAttributes); assertEquals("GAE-pid", detector.detectPlatform().getProjectId()); assertEquals(5, detectedAttributes.size()); - - if (gaeEnvironmentVar != null && gaeEnvironmentVar.equals("standard")) { - assertEquals( - "country-region1", detector.detectPlatform().getAttributes().get(GAE_CLOUD_REGION)); - } else { - assertEquals( - "country-region", detector.detectPlatform().getAttributes().get(GAE_CLOUD_REGION)); - } + assertEquals(expectedRegion, detector.detectPlatform().getAttributes().get(GAE_CLOUD_REGION)); assertEquals("app-engine-hello", detectedAttributes.get(GAE_MODULE_NAME)); assertEquals("app-engine-hello-v1", detectedAttributes.get(GAE_APP_VERSION)); assertEquals("app-engine-hello-f236d", detectedAttributes.get(GAE_INSTANCE_ID)); - assertEquals("country-region-zone", detectedAttributes.get(GAE_AVAILABILITY_ZONE)); + assertEquals(expectedZone, detectedAttributes.get(GAE_AVAILABILITY_ZONE)); } - // Provides key-value pair of GAE variant environment and the expected region - // value based on the environment variable + private static Arguments gaeTestArguments( + String gaeEnvironmentVar, + String metadataZone, + String expectedZone, + String metadataRegion, + String expectedRegion) { + return Arguments.of( + gaeEnvironmentVar, metadataZone, expectedZone, metadataRegion, expectedRegion); + } + + // Provides parameterized arguments for testGAEResourceWithAppEngineAttributesSucceeds private static Stream provideGAEVariantEnvironmentVariable() { + return Stream.of( - Arguments.of("standard"), - Arguments.of((String) null), - Arguments.of("flex"), - Arguments.of("")); + // GAE standard + gaeTestArguments( + "standard", + // the zone should be extracted from the zone attribute + "projects/233510669999/zones/us15", + "us15", + // the region should be extracted from region attribute + "projects/233510669999/regions/us-central1", + "us-central1"), + + // GAE flex + gaeTestArguments( + (String) null, + "country-region-zone", + "country-region-zone", + // the region should be extracted from the zone attribute + "", + "country-region"), + gaeTestArguments( + "flex", "country-region-zone", "country-region-zone", "", "country-region"), + gaeTestArguments("", "country-region-zone", "country-region-zone", "", "country-region")); } } diff --git a/examples/autoinstrument/build.gradle b/examples/autoinstrument/build.gradle index ff764382..cfd2efa0 100644 --- a/examples/autoinstrument/build.gradle +++ b/examples/autoinstrument/build.gradle @@ -64,7 +64,7 @@ jib { // Use the downloaded java agent. '-javaagent:/otelagent/otel_agent.jar', // Use the GCP exporter extensions. - '-Dotel.javaagent.experimental.extensions=/otelagent/gcp_ext.jar', + '-Dotel.javaagent.extensions=/otelagent/gcp_ext.jar', // Configure auto instrumentation. '-Dotel.traces.exporter=google_cloud_trace', '-Dotel.metrics.exporter=google_cloud_monitoring', diff --git a/examples/instrumentation-quickstart/build.gradle b/examples/instrumentation-quickstart/build.gradle index ca4abab6..f2d74b74 100644 --- a/examples/instrumentation-quickstart/build.gradle +++ b/examples/instrumentation-quickstart/build.gradle @@ -42,9 +42,15 @@ spotless { dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' - // Cannot be updated until logback is updated to 1.3+, probably in the next Spring Boot - // major version - implementation 'net.logstash.logback:logstash-logback-encoder:7.3' + // Use log4j2 for logging + // https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/howto.html#howto.logging.log4j + implementation "org.springframework.boot:spring-boot-starter-log4j2" + modules { + module("org.springframework.boot:spring-boot-starter-logging") { + replacedBy("org.springframework.boot:spring-boot-starter-log4j2", "Use Log4j2 instead of Logback") + } + } + implementation "org.apache.logging.log4j:log4j-layout-template-json" testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.testcontainers:testcontainers:1.19.4' diff --git a/examples/instrumentation-quickstart/otel-collector-config.yaml b/examples/instrumentation-quickstart/otel-collector-config.yaml index c195d404..513c8a0b 100644 --- a/examples/instrumentation-quickstart/otel-collector-config.yaml +++ b/examples/instrumentation-quickstart/otel-collector-config.yaml @@ -35,6 +35,18 @@ receivers: layout: '%Y-%m-%dT%H:%M:%S.%fZ' severity: parse_from: body.severity + preset: none + # parse minimal set of severity strings that Cloud Logging explicitly supports + # https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity + mapping: + debug: debug + info: info + info3: notice + warn: warning + error: error + fatal: critical + fatal3: alert + fatal4: emergency # set trace_flags to SAMPLED if GCP attribute is set to true - type: add diff --git a/examples/instrumentation-quickstart/src/main/resources/log4j2.xml b/examples/instrumentation-quickstart/src/main/resources/log4j2.xml new file mode 100644 index 00000000..e1aedfd4 --- /dev/null +++ b/examples/instrumentation-quickstart/src/main/resources/log4j2.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/otlp-spring/README.md b/examples/otlp-spring/README.md new file mode 100644 index 00000000..2dd249e9 --- /dev/null +++ b/examples/otlp-spring/README.md @@ -0,0 +1,96 @@ +# OTLP Trace with Spring Boot and Google Auth + +A sample Spring Boot service that exports OTLP traces, protected by Google authentication. The sample uses auto-refreshing credentials. + +### Prerequisites + +##### Get Google Cloud Credentials on your machine + +```shell +gcloud auth application-default login +``` + +##### Export the Google Cloud Project ID to `GOOGLE_CLOUD_PROJECT` environment variable: + +```shell +export GOOGLE_CLOUD_PROJECT="my-awesome-gcp-project-id" +``` + +##### Update build.gradle to set required arguments + +Update [`build.gradle`](build.grade) to set the following: + +``` + '-Dotel.resource.attributes=gcp.project_id=, + '-Dotel.exporter.otlp.headers=X-Goog-User-Project=', + # Optional - if you want to export using gRPC protocol + '-Dotel.exporter.otlp.protocol=grpc', +``` + +## Running Locally on your machine + +Setup your endpoint with the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable: + +```shell +export OTEL_EXPORTER_OTLP_ENDPOINT="http://your-endpoint:port" +``` + +To run the spring boot application from project root: + +```shell +./gradlew :examples-otlp-spring:run +``` + +This will start the web application on localhost:8080. The application provides 2 routes: + - http://localhost:8080/ : The index root; does not generate a trace. + - http://localhost:8080/work : This route generates a trace. + +Visit these routes to interact with the application. + +## Running on Google Kubernetes Engine + +> [!NOTE] +> You need to have a GKE cluster already setup in your GCP project before continuing with these steps. + +Create artifact registry repository to host your containerized image of the application: +```shell +gcloud artifacts repositories create otlp-samples --repository-format=docker --location=us-central1 + +gcloud auth configure-docker us-central1-docker.pkg.dev +``` + +Build and push your image to the Artifact Registry. +```shell +./gradlew :examples:otlp-spring:jib --image="us-central1-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/otlp-samples/spring-otlp-trace-example:v1" +``` + +Deploy the image on your Kubernetes cluster and setup port forwarding to interact with your cluster: +```shell +sed s/%GOOGLE_CLOUD_PROJECT%/$GOOGLE_CLOUD_PROJECT/g \ + examples/otlp-spring/k8s/deployment.yaml | kubectl apply -f - + +# This connects port 8080 on your machine to port 60000 on the spring-otlp-service +kubectl port-forward service/spring-otlp-service 8080:60000 +``` + +After successfully setting up port-forwarding, you can send requests to your cluster via `curl` or some similar tool: +```shell +curl http://localhost:8080/work?desc=test +``` + +### Sending continuous requests + +The sample comes with a [client program](src/test/java/com/google/cloud/opentelemetry/examples/otlpspring/MainClient.java) which sends requests to deployed application on your cluster at a fixed rate. +Once you have port forwarding setup for your cluster, run this client program to send continuous requests to the Spring service to generate multiple traces. + +```shell +./gradlew :examples-otlp-spring:runClient +``` + +### GKE Cleanup + +After running the sample, delete the deployment and the service if you are done with it: +```shell +kubectl delete services spring-otlp-service +kubectl delete deployment spring-otlp-trace-example +``` diff --git a/examples/otlp-spring/build.gradle b/examples/otlp-spring/build.gradle new file mode 100644 index 00000000..9c6c6f18 --- /dev/null +++ b/examples/otlp-spring/build.gradle @@ -0,0 +1,70 @@ +/* + * 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' + id 'application' + id 'com.google.cloud.tools.jib' +} + +description = 'Example showcasing OTLP trace ingest on GCP and Google Auth in a Spring Boot App' + +group 'com.google.cloud.opentelemetry.examples' + +repositories { + mavenCentral() +} + +dependencies { + implementation(libraries.opentelemetry_api) + implementation(libraries.opentelemetry_sdk) + implementation(libraries.opentelemetry_otlp_exporter) + implementation(libraries.opentelemetry_sdk_autoconf) + implementation(libraries.google_auth) + + implementation(libraries.spring_boot_starter_web) +} + +// Provide headers from env variable +// export OTEL_EXPORTER_OTLP_ENDPOINT="http://path/to/yourendpoint:port" +def autoconf_config = [ + '-Dotel.resource.attributes=gcp.project_id=', + '-Dotel.exporter.otlp.headers=X-Goog-User-Project=', + '-Dotel.traces.exporter=otlp', + '-Dotel.metrics.exporter=none', + '-Dotel.exporter.otlp.protocol=http/protobuf', + '-Dotel.java.global-autoconfigure.enabled=true', + '-Dotel.service.name=spring-otlp-trace-example-service', +] + +application { + mainClass = "com.google.cloud.opentelemetry.examples.otlpspring.Main" + applicationDefaultJvmArgs = autoconf_config +} + +jib { + from.image = "gcr.io/distroless/java-debian10:11" + containerizingMode = "packaged" + container.jvmFlags = autoconf_config as List + container.ports = ["8080"] + container.environment = [ + "OTEL_EXPORTER_OTLP_ENDPOINT":"http://path/to/yourendpoint:port", + ] +} + +tasks.register('runClient', JavaExec) { + classpath = sourceSets.test.runtimeClasspath + mainClass = 'com.google.cloud.opentelemetry.examples.otlpspring.MainClient' +} diff --git a/examples/otlp-spring/k8s/deployment.yaml b/examples/otlp-spring/k8s/deployment.yaml new file mode 100644 index 00000000..4d97a220 --- /dev/null +++ b/examples/otlp-spring/k8s/deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: spring-otlp-trace-example + namespace: default + labels: + app: spring-otlp-trace-example + tier: backend +spec: + selector: + matchLabels: + app: spring-otlp-trace-example + tier: backend + template: + metadata: + labels: + app: spring-otlp-trace-example + tier: backend + spec: + containers: + - name: spring-otlp-trace-example + image: us-central1-docker.pkg.dev/%GOOGLE_CLOUD_PROJECT%/otlp-samples/spring-otlp-trace-example:v1 + imagePullPolicy: Always + # required for resource detection in GKE environment + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CONTAINER_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name +--- +apiVersion: v1 +kind: Service +metadata: + name: spring-otlp-service +spec: + selector: + app: spring-otlp-trace-example + tier: backend + ports: + - protocol: TCP + port: 60000 + targetPort: 8080 diff --git a/examples/otlp-spring/src/main/java/com/google/cloud/opentelemetry/examples/otlpspring/ApplicationController.java b/examples/otlp-spring/src/main/java/com/google/cloud/opentelemetry/examples/otlpspring/ApplicationController.java new file mode 100644 index 00000000..7b9ca8a2 --- /dev/null +++ b/examples/otlp-spring/src/main/java/com/google/cloud/opentelemetry/examples/otlpspring/ApplicationController.java @@ -0,0 +1,74 @@ +/* + * 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.examples.otlpspring; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.util.Optional; +import java.util.Random; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ApplicationController { + private static final String INSTRUMENTATION_SCOPE_NAME = ApplicationController.class.getName(); + private static final Random random = new Random(); + + private static final String INDEX_GREETING = + "Welcome to OTLP Trace sample with Google Auth on Spring"; + private static final String WORK_RESPONSE_FMT = "Work finished in %d ms"; + + private final Logger logger = LoggerFactory.getLogger(ApplicationController.class); + private final OpenTelemetrySdk openTelemetrySdk; + + @Autowired + public ApplicationController(OpenTelemetrySdk openTelemetrySdk) { + this.openTelemetrySdk = openTelemetrySdk; + } + + @GetMapping("/") + public String index() { + return INDEX_GREETING; + } + + @GetMapping("/work") + public String simulateWork(@RequestParam(name = "desc") Optional description) { + String desc = description.orElse("generic"); + // Generate a span + Span span = + openTelemetrySdk.getTracer(INSTRUMENTATION_SCOPE_NAME).spanBuilder(desc).startSpan(); + long workDurationMillis; + try (Scope scope = span.makeCurrent()) { + span.addEvent("Event A"); + // Do some work for the use case + // Simulate work: this could be simulating a network request or an expensive disk operation + workDurationMillis = 100 + random.nextInt(5) * 100; + Thread.sleep(workDurationMillis); + span.addEvent("Event B"); + } catch (InterruptedException e) { + logger.debug("Error while sleeping: {}", e.getMessage()); + throw new RuntimeException(e); + } finally { + span.end(); + } + return String.format(WORK_RESPONSE_FMT, workDurationMillis); + } +} diff --git a/examples/otlp-spring/src/main/java/com/google/cloud/opentelemetry/examples/otlpspring/Main.java b/examples/otlp-spring/src/main/java/com/google/cloud/opentelemetry/examples/otlpspring/Main.java new file mode 100644 index 00000000..ecf28bbb --- /dev/null +++ b/examples/otlp-spring/src/main/java/com/google/cloud/opentelemetry/examples/otlpspring/Main.java @@ -0,0 +1,31 @@ +/* + * 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.examples.otlpspring; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Main { + private static final Logger logger = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) { + logger.info("Starting OTLP with Spring Boot and Google Auth"); + SpringApplication.run(Main.class, args); + } +} diff --git a/examples/otlp-spring/src/main/java/com/google/cloud/opentelemetry/examples/otlpspring/configuration/OpenTelemetryConfiguration.java b/examples/otlp-spring/src/main/java/com/google/cloud/opentelemetry/examples/otlpspring/configuration/OpenTelemetryConfiguration.java new file mode 100644 index 00000000..af0a3a7a --- /dev/null +++ b/examples/otlp-spring/src/main/java/com/google/cloud/opentelemetry/examples/otlpspring/configuration/OpenTelemetryConfiguration.java @@ -0,0 +1,113 @@ +/* + * 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.examples.otlpspring.configuration; + +import com.google.auth.oauth2.GoogleCredentials; +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.trace.export.SpanExporter; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +@Configuration +public class OpenTelemetryConfiguration { + private final Logger logger = LoggerFactory.getLogger(OpenTelemetryConfiguration.class); + + @Bean + @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) + public OpenTelemetrySdk getOpenTelemetrySdk() { + GoogleCredentials googleCredentials = getCredentials(); + AutoConfiguredOpenTelemetrySdk autoConfOTelSdk = + AutoConfiguredOpenTelemetrySdk.builder() + .addSpanExporterCustomizer( + (exporter, configProperties) -> + addAuthorizationHeaders(exporter, googleCredentials)) + .build(); + return autoConfOTelSdk.getOpenTelemetrySdk(); + } + + private GoogleCredentials getCredentials() { + try { + return GoogleCredentials.getApplicationDefault(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private SpanExporter addAuthorizationHeaders( + SpanExporter exporter, GoogleCredentials credentials) { + Map authHeaders = new ConcurrentHashMap<>(); + if (exporter instanceof OtlpHttpSpanExporter) { + try { + credentials.refreshIfExpired(); + OtlpHttpSpanExporterBuilder builder = + ((OtlpHttpSpanExporter) exporter) + .toBuilder() + .setHeaders( + () -> { + authHeaders.put("Authorization", refreshToken(credentials)); + return authHeaders; + }); + return builder.build(); + } catch (IOException e) { + logger.error("Error while adding headers : {}", e.getMessage()); + throw new RuntimeException(e); + } + } else if (exporter instanceof OtlpGrpcSpanExporter) { + try { + credentials.refreshIfExpired(); + OtlpGrpcSpanExporterBuilder builder = + ((OtlpGrpcSpanExporter) exporter) + .toBuilder() + .setHeaders( + () -> { + authHeaders.put("Authorization", refreshToken(credentials)); + return authHeaders; + }); + return builder.build(); + } catch (IOException e) { + logger.error("Error while adding headers: {}", e.getMessage()); + throw new RuntimeException(e); + } + } + return exporter; + } + + private String refreshToken(GoogleCredentials credentials) { + logger.info("Refreshing Google Credentials"); + try { + logger.info( + "Current access token expires at {}", credentials.getAccessToken().getExpirationTime()); + credentials.refreshIfExpired(); + logger.info("Credential refresh check complete"); + return String.format("Bearer %s", credentials.getAccessToken().getTokenValue()); + } catch (IOException e) { + logger.error("Error while refreshing credentials: {}", e.getMessage()); + throw new RuntimeException(e); + } + } +} diff --git a/examples/otlp-spring/src/main/resources/application.properties b/examples/otlp-spring/src/main/resources/application.properties new file mode 100644 index 00000000..3e1912f2 --- /dev/null +++ b/examples/otlp-spring/src/main/resources/application.properties @@ -0,0 +1,2 @@ +logging.level.org.springframework.web.servlet.DispatcherServlet=DEBUG +spring.application.name=otlp-trace-google-auth diff --git a/examples/otlp-spring/src/test/java/com/google/cloud/opentelemetry/examples/otlpspring/MainClient.java b/examples/otlp-spring/src/test/java/com/google/cloud/opentelemetry/examples/otlpspring/MainClient.java new file mode 100644 index 00000000..4b5a8453 --- /dev/null +++ b/examples/otlp-spring/src/test/java/com/google/cloud/opentelemetry/examples/otlpspring/MainClient.java @@ -0,0 +1,105 @@ +/* + * 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.examples.otlpspring; + +import static java.net.http.HttpResponse.BodyHandlers; +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.SECONDS; + +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.time.Duration; +import java.util.Random; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Client application that runs for ~ 2 hours and make HTTP requests to the spring application + * running on localhost:8080. + */ +class MainClient { + private static final String BASE_URL = "http://localhost:8080"; + private static final String[] ROUTES = new String[] {"/", "/work"}; + private static final Random random = new Random(); + private static final Logger logger = LoggerFactory.getLogger(Main.class.getSimpleName()); + // total time for which this client app runs + private static final int CLIENT_RUN_DURATION_MIN = 120; + + public static void main(String[] args) throws InterruptedException { + System.out.println("Starting client"); + HttpClient client = HttpClient.newHttpClient(); + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + scheduleApplicationCalls(client, scheduler); + boolean allTasksComplete = + scheduler.awaitTermination(CLIENT_RUN_DURATION_MIN + 5, TimeUnit.MINUTES); + if (allTasksComplete) { + logger.info("All scheduled calls finished successfully."); + } else { + logger.info("Scheduled calls timed out."); + } + } + + private static void scheduleApplicationCalls( + HttpClient client, ScheduledExecutorService scheduler) { + Runnable requestIssuer = + () -> { + String response = issueRequest(client); + logger.info("Response: {}", response); + }; + + final ScheduledFuture requestHandle = + scheduler.scheduleAtFixedRate(requestIssuer, 10, 10, TimeUnit.of(SECONDS)); + scheduler.schedule( + () -> { + requestHandle.cancel(true); + }, + CLIENT_RUN_DURATION_MIN, + TimeUnit.of(MINUTES)); + } + + private static String issueRequest(HttpClient client) { + HttpRequest httpRequest = + HttpRequest.newBuilder(URI.create(constructUrl())) + .GET() + .timeout(Duration.of(8, SECONDS)) + .build(); + try { + HttpResponse response = client.send(httpRequest, BodyHandlers.ofString()); + return response.body(); + } catch (IOException | InterruptedException e) { + logger.error("Unable to complete request: {}", e.getMessage()); + throw new RuntimeException(e); + } + } + + private static String constructUrl() { + String route = ROUTES[random.nextInt(2)]; + String additionalParams = ""; + if (route.equals("/work")) { + int randomWorkId = random.nextInt(100); + String workDescription = "test" + randomWorkId; + additionalParams = String.format("?desc=%s", workDescription); + } + return BASE_URL + route + additionalParams; + } +} diff --git a/examples/otlptrace/README.md b/examples/otlptrace/README.md index 8bf21dba..77a2f229 100644 --- a/examples/otlptrace/README.md +++ b/examples/otlptrace/README.md @@ -22,6 +22,8 @@ Next, update [`build.gradle`](build.grade) to set the following: ``` '-Dotel.resource.attributes=gcp.project_id=, '-Dotel.exporter.otlp.headers=X-Goog-User-Project=', + # Optional - if you want to export using gRPC protocol + '-Dotel.exporter.otlp.protocol=grpc', ``` Finally, to run the sample from the project root: diff --git a/examples/otlptrace/src/main/java/com/google/cloud/opentelemetry/example/otlptrace/OTLPTraceExample.java b/examples/otlptrace/src/main/java/com/google/cloud/opentelemetry/example/otlptrace/OTLPTraceExample.java index 9dfe9a6e..f5347a99 100644 --- a/examples/otlptrace/src/main/java/com/google/cloud/opentelemetry/example/otlptrace/OTLPTraceExample.java +++ b/examples/otlptrace/src/main/java/com/google/cloud/opentelemetry/example/otlptrace/OTLPTraceExample.java @@ -20,11 +20,12 @@ 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.common.CompletableResultCode; import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.io.*; import java.io.IOException; import java.util.Random; import java.util.concurrent.TimeUnit; @@ -48,6 +49,8 @@ private static OpenTelemetrySdk setupTraceExporter() throws IOException { } // Modifies the span exporter initially auto-configured using environment variables + // Note: This adds static authorization headers which are set only at initialization time. + // This will stop working after the token expires, since the token is not refreshed. private static SpanExporter addAuthorizationHeaders( SpanExporter exporter, GoogleCredentials credentials) { if (exporter instanceof OtlpHttpSpanExporter) { @@ -63,6 +66,18 @@ private static SpanExporter addAuthorizationHeaders( } catch (IOException e) { System.out.println("error:" + e.getMessage()); } + } else if (exporter instanceof OtlpGrpcSpanExporter) { + try { + credentials.refreshIfExpired(); + OtlpGrpcSpanExporterBuilder builder = + ((OtlpGrpcSpanExporter) exporter) + .toBuilder() + .addHeader( + "Authorization", "Bearer " + credentials.getAccessToken().getTokenValue()); + return builder.build(); + } catch (IOException e) { + throw new RuntimeException(e); + } } return exporter; } diff --git a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/AggregateByLabelMetricTimeSeriesBuilder.java b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/AggregateByLabelMetricTimeSeriesBuilder.java index bdb02029..40966945 100644 --- a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/AggregateByLabelMetricTimeSeriesBuilder.java +++ b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/AggregateByLabelMetricTimeSeriesBuilder.java @@ -59,19 +59,34 @@ public final class AggregateByLabelMetricTimeSeriesBuilder implements MetricTime private final String projectId; private final String prefix; private final Predicate> resourceAttributeFilter; + private final MonitoredResourceDescription monitoredResourceDescription; @Deprecated public AggregateByLabelMetricTimeSeriesBuilder(String projectId, String prefix) { this.projectId = projectId; this.prefix = prefix; this.resourceAttributeFilter = MetricConfiguration.NO_RESOURCE_ATTRIBUTES; + this.monitoredResourceDescription = MetricConfiguration.EMPTY_MONITORED_RESOURCE_DESCRIPTION; } + @Deprecated public AggregateByLabelMetricTimeSeriesBuilder( String projectId, String prefix, Predicate> resourceAttributeFilter) { this.projectId = projectId; this.prefix = prefix; this.resourceAttributeFilter = resourceAttributeFilter; + this.monitoredResourceDescription = MetricConfiguration.EMPTY_MONITORED_RESOURCE_DESCRIPTION; + } + + public AggregateByLabelMetricTimeSeriesBuilder( + String projectId, + String prefix, + Predicate> resourceAttributeFilter, + MonitoredResourceDescription monitoredResourceDescription) { + this.projectId = projectId; + this.prefix = prefix; + this.resourceAttributeFilter = resourceAttributeFilter; + this.monitoredResourceDescription = monitoredResourceDescription; } @Override @@ -135,7 +150,7 @@ private TimeSeries.Builder makeTimeSeriesHeader( return TimeSeries.newBuilder() .setMetric(mapMetric(attributes, descriptor.getType())) .setMetricKind(descriptor.getMetricKind()) - .setResource(mapResource(metric.getResource())); + .setResource(mapResource(metric.getResource(), monitoredResourceDescription)); } private Attributes extraLabelsFromResource(Resource resource) { diff --git a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/InternalMetricExporter.java b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/InternalMetricExporter.java index bbd65029..8a363c9a 100644 --- a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/InternalMetricExporter.java +++ b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/InternalMetricExporter.java @@ -68,6 +68,7 @@ class InternalMetricExporter implements MetricExporter { private final MetricDescriptorStrategy metricDescriptorStrategy; private final Predicate> resourceAttributesFilter; private final boolean useCreateServiceTimeSeries; + private final MonitoredResourceDescription monitoredResourceDescription; InternalMetricExporter( String projectId, @@ -75,13 +76,15 @@ class InternalMetricExporter implements MetricExporter { CloudMetricClient client, MetricDescriptorStrategy descriptorStrategy, Predicate> resourceAttributesFilter, - boolean useCreateServiceTimeSeries) { + boolean useCreateServiceTimeSeries, + MonitoredResourceDescription monitoredResourceDescription) { this.projectId = projectId; this.prefix = prefix; this.metricServiceClient = client; this.metricDescriptorStrategy = descriptorStrategy; this.resourceAttributesFilter = resourceAttributesFilter; this.useCreateServiceTimeSeries = useCreateServiceTimeSeries; + this.monitoredResourceDescription = monitoredResourceDescription; } static InternalMetricExporter createWithConfiguration(MetricConfiguration configuration) @@ -120,7 +123,8 @@ static InternalMetricExporter createWithConfiguration(MetricConfiguration config new CloudMetricClientImpl(MetricServiceClient.create(builder.build())), configuration.getDescriptorStrategy(), configuration.getResourceAttributesFilter(), - configuration.getUseServiceTimeSeries()); + configuration.getUseServiceTimeSeries(), + configuration.getMonitoredResourceDescription()); } @VisibleForTesting @@ -130,14 +134,16 @@ static InternalMetricExporter createWithClient( CloudMetricClient metricServiceClient, MetricDescriptorStrategy descriptorStrategy, Predicate> resourceAttributesFilter, - boolean useCreateServiceTimeSeries) { + boolean useCreateServiceTimeSeries, + MonitoredResourceDescription monitoredResourceDescription) { return new InternalMetricExporter( projectId, prefix, metricServiceClient, descriptorStrategy, resourceAttributesFilter, - useCreateServiceTimeSeries); + useCreateServiceTimeSeries, + monitoredResourceDescription); } private void exportDescriptor(MetricDescriptor descriptor) { @@ -161,7 +167,8 @@ public CompletableResultCode export(Collection metrics) { // 2. Attempt to register MetricDescriptors (using configured strategy) // 3. Fire the set of time series off. MetricTimeSeriesBuilder builder = - new AggregateByLabelMetricTimeSeriesBuilder(projectId, prefix, resourceAttributesFilter); + new AggregateByLabelMetricTimeSeriesBuilder( + projectId, prefix, resourceAttributesFilter, monitoredResourceDescription); for (final MetricData metricData : metrics) { // Extract all the underlying points. switch (metricData.getType()) { diff --git a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MetricConfiguration.java b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MetricConfiguration.java index fd525c16..b675c7e8 100644 --- a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MetricConfiguration.java +++ b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MetricConfiguration.java @@ -28,6 +28,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.semconv.ResourceAttributes; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.function.Predicate; import java.util.function.Supplier; @@ -45,6 +46,9 @@ public abstract class MetricConfiguration { /** Resource attribute filter that disables addition of resource attributes to metric labels. */ public static final Predicate> NO_RESOURCE_ATTRIBUTES = attributeKey -> false; + public static final MonitoredResourceDescription EMPTY_MONITORED_RESOURCE_DESCRIPTION = + new MonitoredResourceDescription("", Collections.emptySet()); + /** * Default resource attribute filter that adds recommended resource attributes to metric labels. */ @@ -151,6 +155,19 @@ public final String getProjectId() { */ public abstract boolean getUseServiceTimeSeries(); + /** + * Returns the custom {@link MonitoredResourceDescription} that is used to map the OpenTelemetry + * {@link io.opentelemetry.sdk.resources.Resource} to Google specific {@link + * com.google.api.MonitoredResource}. + * + *

This returns the {@link MetricConfiguration#EMPTY_MONITORED_RESOURCE_DESCRIPTION} if not set + * through exporter configuration. + * + * @return The {@link MonitoredResourceDescription} object containing the MonitoredResource type + * and its expected labels. + */ + public abstract MonitoredResourceDescription getMonitoredResourceDescription(); + @VisibleForTesting abstract boolean getInsecureEndpoint(); @@ -176,6 +193,7 @@ public static Builder builder() { .setInsecureEndpoint(false) .setUseServiceTimeSeries(false) .setResourceAttributesFilter(DEFAULT_RESOURCE_ATTRIBUTES_FILTER) + .setMonitoredResourceDescription(EMPTY_MONITORED_RESOURCE_DESCRIPTION) .setMetricServiceEndpoint(MetricServiceStubSettings.getDefaultEndpoint()); } @@ -238,6 +256,19 @@ public final Builder setProjectId(String projectId) { */ public abstract Builder setUseServiceTimeSeries(boolean useServiceTimeSeries); + /** + * Sets the {@link MonitoredResourceDescription} that is used to map OpenTelemetry {@link + * io.opentelemetry.sdk.resources.Resource}s to Google specific {@link + * com.google.api.MonitoredResource}s. + * + * @param monitoredResourceDescription the {@link MonitoredResourceDescription} object + * responsible for providing mapping between the custom {@link + * com.google.api.MonitoredResource} and the expected labels. + * @return this. + */ + public abstract Builder setMonitoredResourceDescription( + MonitoredResourceDescription monitoredResourceDescription); + /** * Set a filter to determine which resource attributes to add to metrics as metric labels. By * default, it adds service.name, service.namespace, and service.instance.id. This is diff --git a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MonitoredResourceDescription.java b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MonitoredResourceDescription.java new file mode 100644 index 00000000..9427782a --- /dev/null +++ b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/MonitoredResourceDescription.java @@ -0,0 +1,59 @@ +/* + * 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.metric; + +import java.util.Collections; +import java.util.Set; +import javax.annotation.concurrent.Immutable; + +/** + * This class holds the mapping between Google Cloud's monitored resource type and the labels for + * identifying the given monitored resource type. + */ +@Immutable +public final class MonitoredResourceDescription { + private final String mrType; + private final Set mrLabels; + + /** + * Public constructor. + * + * @param mrType The monitored resource type for which the mapping is being specified. + * @param mrLabels A set of labels which uniquely identify a given monitored resource. + */ + public MonitoredResourceDescription(String mrType, Set mrLabels) { + this.mrType = mrType; + this.mrLabels = Collections.unmodifiableSet(mrLabels); + } + + /** + * Returns the set of labels used to identify the monitored resource represented in this mapping. + * + * @return Immutable set of labels that map to the specified monitored resource type. + */ + public Set getMonitoredResourceLabels() { + return mrLabels; + } + + /** + * The type of the monitored resource for which mapping is defined. + * + * @return The type of the monitored resource. + */ + public String getMonitoredResourceType() { + return mrType; + } +} diff --git a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/ResourceTranslator.java b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/ResourceTranslator.java index 790f7713..51b65ae2 100644 --- a/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/ResourceTranslator.java +++ b/exporters/metrics/src/main/java/com/google/cloud/opentelemetry/metric/ResourceTranslator.java @@ -17,13 +17,22 @@ import com.google.api.MonitoredResource; import com.google.cloud.opentelemetry.resource.GcpResource; +import com.google.common.base.Strings; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.sdk.resources.Resource; +import java.util.Set; +import java.util.logging.Logger; /** Translates from OpenTelemetry Resource into Google Cloud Monitoring's MonitoredResource. */ public class ResourceTranslator { + private static final String CUSTOM_MR_KEY = "gcp.resource_type"; + private static final Logger LOGGER = + Logger.getLogger(ResourceTranslator.class.getCanonicalName()); + private ResourceTranslator() {} /** Converts a Java OpenTelemetry SDK resource into a MonitoredResource from GCP. */ + @Deprecated public static MonitoredResource mapResource(Resource resource) { GcpResource gcpResource = com.google.cloud.opentelemetry.resource.ResourceTranslator.mapResource(resource); @@ -32,4 +41,59 @@ public static MonitoredResource mapResource(Resource resource) { gcpResource.getResourceLabels().getLabels().forEach(mr::putLabels); return mr.build(); } + + /** + * Converts a Java OpenTelemetry SDK {@link Resource} into a Google specific {@link + * MonitoredResource}. + * + * @param resource The OpenTelemetry {@link Resource} to be converted. + * @param mrDescription The {@link MonitoredResourceDescription} in case the OpenTelemetry SDK + * {@link Resource} needs to be converted to a custom {@link MonitoredResource}. For use-cases + * not requiring custom {@link MonitoredResource}s, use the {@link + * MetricConfiguration#EMPTY_MONITORED_RESOURCE_DESCRIPTION}. + * @return The converted {@link MonitoredResource} based on the provided {@link + * MonitoredResourceDescription}. + */ + static MonitoredResource mapResource( + Resource resource, MonitoredResourceDescription mrDescription) { + String mrTypeToMap = resource.getAttributes().get(AttributeKey.stringKey(CUSTOM_MR_KEY)); + if (Strings.isNullOrEmpty(mrTypeToMap)) { + return mapResourceUsingCustomerMappings(resource); + } else if (!mrTypeToMap.equals(mrDescription.getMonitoredResourceType())) { + LOGGER.warning( + String.format( + "MonitoredResource type mismatch: Description provided for %s, but found %s in resource attributes. Defaulting to standard mappings.", + mrDescription.getMonitoredResourceType(), mrTypeToMap)); + return mapResourceUsingCustomerMappings(resource); + } else { + return mapResourceUsingPlatformMappings(resource, mrTypeToMap, mrDescription); + } + } + + private static MonitoredResource mapResourceUsingPlatformMappings( + Resource resource, + String mrTypeToMap, + MonitoredResourceDescription monitoredResourceDescription) { + Set expectedMRLabels = monitoredResourceDescription.getMonitoredResourceLabels(); + MonitoredResource.Builder mr = MonitoredResource.newBuilder(); + mr.setType(mrTypeToMap); + expectedMRLabels.forEach( + expectedLabel -> { + String foundValue = resource.getAttribute(AttributeKey.stringKey(expectedLabel)); + if (foundValue != null) { + // only put labels for found value + mr.putLabels(expectedLabel, foundValue); + } + }); + return mr.build(); + } + + private static MonitoredResource mapResourceUsingCustomerMappings(Resource resource) { + GcpResource gcpResource = + com.google.cloud.opentelemetry.resource.ResourceTranslator.mapResource(resource); + MonitoredResource.Builder mr = MonitoredResource.newBuilder(); + mr.setType(gcpResource.getResourceType()); + gcpResource.getResourceLabels().getLabels().forEach(mr::putLabels); + return mr.build(); + } } diff --git a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/FakeData.java b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/FakeData.java index 44b99600..24c9c7ba 100644 --- a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/FakeData.java +++ b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/FakeData.java @@ -61,6 +61,20 @@ public class FakeData { static final Attributes someLabels = Attributes.builder().put("label1", "value1").put("label2", "False").build(); + static final Attributes someCustomMRAttributes = + Attributes.builder() + .put("gcp.resource_type", "custom_mr_instance") // required to trigger platform mapping + .put("host_id", aHostId) + .put("location", aCloudZone) + .put("service_instance_id", "test-gcs-service-id") + .put("foo", "bar") // extra label, gets ignored + .build(); + + static final Resource aCustomMonitoredResource = Resource.create(someCustomMRAttributes); + + static final Resource aCustomMonitoredResourceWithNoAttributes = + Resource.create(Attributes.builder().put("gcp.resource_type", "custom_mr_instance").build()); + static final Attributes someGceAttributes = Attributes.builder() .put( @@ -119,6 +133,26 @@ public class FakeData { ImmutableSumData.create( true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint))); + static final MetricData aMetricDataWithCustomResource = + ImmutableMetricData.createLongSum( + aCustomMonitoredResource, + anInstrumentationLibraryInfo, + "opentelemetry/name", + "description", + "ns", + ImmutableSumData.create( + true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint))); + + static final MetricData aMetricDataWithEmptyResourceAttributes = + ImmutableMetricData.createLongSum( + aCustomMonitoredResourceWithNoAttributes, + anInstrumentationLibraryInfo, + "opentelemetry/name", + "description", + "ns", + ImmutableSumData.create( + true, AggregationTemporality.CUMULATIVE, ImmutableList.of(aLongPoint))); + static final MetricData googleComputeServiceMetricData = ImmutableMetricData.createLongSum( aGceResource, diff --git a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/GoogleCloudMetricExporterTest.java b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/GoogleCloudMetricExporterTest.java index 9484d4c0..5b260eb6 100644 --- a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/GoogleCloudMetricExporterTest.java +++ b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/GoogleCloudMetricExporterTest.java @@ -26,6 +26,8 @@ import static com.google.cloud.opentelemetry.metric.FakeData.aHostId; import static com.google.cloud.opentelemetry.metric.FakeData.aLongPoint; import static com.google.cloud.opentelemetry.metric.FakeData.aMetricData; +import static com.google.cloud.opentelemetry.metric.FakeData.aMetricDataWithCustomResource; +import static com.google.cloud.opentelemetry.metric.FakeData.aMetricDataWithEmptyResourceAttributes; import static com.google.cloud.opentelemetry.metric.FakeData.aProjectId; import static com.google.cloud.opentelemetry.metric.FakeData.aSpanId; import static com.google.cloud.opentelemetry.metric.FakeData.aTraceId; @@ -33,6 +35,7 @@ import static com.google.cloud.opentelemetry.metric.FakeData.googleComputeServiceMetricData; import static com.google.cloud.opentelemetry.metric.MetricConfiguration.DEFAULT_PREFIX; import static com.google.cloud.opentelemetry.metric.MetricConfiguration.DEFAULT_RESOURCE_ATTRIBUTES_FILTER; +import static com.google.cloud.opentelemetry.metric.MetricConfiguration.EMPTY_MONITORED_RESOURCE_DESCRIPTION; import static com.google.cloud.opentelemetry.metric.MetricConfiguration.NO_RESOURCE_ATTRIBUTES; import static com.google.cloud.opentelemetry.metric.MetricTranslator.METRIC_DESCRIPTOR_TIME_UNIT; import static com.google.cloud.opentelemetry.metric.MetricTranslator.NANO_PER_SECOND; @@ -71,6 +74,7 @@ import com.google.protobuf.Any; import com.google.protobuf.Timestamp; import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.metrics.SdkMeterProvider; @@ -83,6 +87,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.junit.Before; import org.junit.Test; @@ -134,7 +139,8 @@ public void testExportSendsAllDescriptorsOnce() { mockClient, MetricDescriptorStrategy.SEND_ONCE, DEFAULT_RESOURCE_ATTRIBUTES_FILTER, - false); + false, + EMPTY_MONITORED_RESOURCE_DESCRIPTION); CompletableResultCode result = exporter.export(ImmutableList.of(aMetricData, aHistogram)); assertTrue(result.isSuccess()); CompletableResultCode result2 = exporter.export(ImmutableList.of(aMetricData, aHistogram)); @@ -244,7 +250,8 @@ public void testExportSucceeds() { mockClient, MetricDescriptorStrategy.ALWAYS_SEND, DEFAULT_RESOURCE_ATTRIBUTES_FILTER, - false); + false, + EMPTY_MONITORED_RESOURCE_DESCRIPTION); CompletableResultCode result = exporter.export(ImmutableList.of(aMetricData)); verify(mockClient, times(1)).createMetricDescriptor(metricDescriptorCaptor.capture()); @@ -366,7 +373,8 @@ public void testExportWithHistogram_Succeeds() { mockClient, MetricDescriptorStrategy.ALWAYS_SEND, DEFAULT_RESOURCE_ATTRIBUTES_FILTER, - false); + false, + EMPTY_MONITORED_RESOURCE_DESCRIPTION); CompletableResultCode result = exporter.export(ImmutableList.of(aHistogram)); verify(mockClient, times(1)).createMetricDescriptor(metricDescriptorCaptor.capture()); verify(mockClient, times(1)) @@ -387,7 +395,8 @@ public void testExportWithNonSupportedMetricTypeReturnsFailure() { mockClient, MetricDescriptorStrategy.ALWAYS_SEND, NO_RESOURCE_ATTRIBUTES, - false); + false, + EMPTY_MONITORED_RESOURCE_DESCRIPTION); MetricData metricData = ImmutableMetricData.createDoubleSummary( @@ -405,6 +414,298 @@ public void testExportWithNonSupportedMetricTypeReturnsFailure() { assertFalse(result.isSuccess()); } + // TODO(psx95): Convert test to JUnit5 parameterized test + @Test + public void testExportWithMonitoredResourceMappingSucceeds() { + MonitoredResourceDescription monitoredResourceDescription = + new MonitoredResourceDescription( + "custom_mr_instance", Set.of("service_instance_id", "host_id", "location")); + + // controls which resource attributes end up in metric labels + Predicate> customAttributesFilter = + attributeKey -> + !attributeKey.getKey().isEmpty() && attributeKey.getKey().equals("service_instance_id"); + + MetricDescriptor expectedDescriptor = + MetricDescriptor.newBuilder() + .setDisplayName(aMetricDataWithCustomResource.getName()) + .setType(DEFAULT_PREFIX + "/" + aMetricDataWithCustomResource.getName()) + .addLabels( + LabelDescriptor.newBuilder() + .setKey("service_instance_id") + .setValueType(ValueType.STRING) + .build()) + .addLabels( + LabelDescriptor.newBuilder() + .setKey("label1") + .setValueType(ValueType.STRING) + .build()) + .addLabels( + LabelDescriptor.newBuilder() + .setKey("label2") + .setValueType(ValueType.STRING) + .build()) + .setMetricKind(MetricKind.CUMULATIVE) + .setValueType(MetricDescriptor.ValueType.INT64) + .setUnit(METRIC_DESCRIPTOR_TIME_UNIT) + .setDescription(aMetricDataWithCustomResource.getDescription()) + .build(); + TimeInterval expectedTimeInterval = + TimeInterval.newBuilder() + .setStartTime( + Timestamp.newBuilder() + .setSeconds(aLongPoint.getStartEpochNanos() / NANO_PER_SECOND) + .setNanos(0) + .build()) + .setEndTime( + Timestamp.newBuilder() + .setSeconds(aLongPoint.getEpochNanos() / NANO_PER_SECOND) + .setNanos(0) + .build()) + .build(); + Point expectedPoint = + Point.newBuilder() + .setValue(TypedValue.newBuilder().setInt64Value(aLongPoint.getValue())) + .setInterval(expectedTimeInterval) + .build(); + TimeSeries expectedTimeSeries = + TimeSeries.newBuilder() + .setMetric( + Metric.newBuilder() + .setType(expectedDescriptor.getType()) + .putLabels("service_instance_id", "test-gcs-service-id") + .putLabels("label1", "value1") + .putLabels("label2", "false") + .putLabels(LABEL_INSTRUMENTATION_SOURCE, "instrumentName") + .putLabels(LABEL_INSTRUMENTATION_VERSION, "0") + .build()) + .addPoints(expectedPoint) + .setMetricKind(expectedDescriptor.getMetricKind()) + .setResource( + MonitoredResource.newBuilder() + .setType("custom_mr_instance") + .putLabels("service_instance_id", "test-gcs-service-id") + .putLabels("location", aCloudZone) + .putLabels("host_id", aHostId) + .build()) + .build(); + CreateMetricDescriptorRequest expectedRequest = + CreateMetricDescriptorRequest.newBuilder() + .setName("projects/" + aProjectId) + .setMetricDescriptor(expectedDescriptor) + .build(); + ProjectName expectedProjectName = ProjectName.of(aProjectId); + + MetricExporter exporter = + InternalMetricExporter.createWithClient( + aProjectId, + DEFAULT_PREFIX, + mockClient, + MetricDescriptorStrategy.ALWAYS_SEND, + customAttributesFilter, + false, + monitoredResourceDescription); + + CompletableResultCode result = exporter.export(ImmutableList.of(aMetricDataWithCustomResource)); + verify(mockClient, times(1)).createMetricDescriptor(metricDescriptorCaptor.capture()); + verify(mockClient, times(1)) + .createTimeSeries(projectNameArgCaptor.capture(), timeSeriesArgCaptor.capture()); + + assertTrue(result.isSuccess()); + assertEquals(expectedRequest, metricDescriptorCaptor.getValue()); + assertEquals(expectedProjectName, projectNameArgCaptor.getValue()); + assertEquals(1, timeSeriesArgCaptor.getValue().size()); + assertEquals(expectedTimeSeries, timeSeriesArgCaptor.getValue().get(0)); + } + + @Test + public void testExportWithMonitoredResourceMappingSucceeds_NoMRLabels() { + MonitoredResourceDescription monitoredResourceDescription = + new MonitoredResourceDescription("custom_mr_instance", Collections.emptySet()); + + // controls which resource attributes end up in metric labels + Predicate> customAttributesFilter = + attributeKey -> + !attributeKey.getKey().isEmpty() && attributeKey.getKey().equals("service_instance_id"); + + MetricDescriptor expectedDescriptor = + MetricDescriptor.newBuilder() + .setDisplayName(aMetricDataWithCustomResource.getName()) + .setType(DEFAULT_PREFIX + "/" + aMetricDataWithCustomResource.getName()) + .addLabels( + LabelDescriptor.newBuilder() + .setKey("service_instance_id") + .setValueType(ValueType.STRING) + .build()) + .addLabels( + LabelDescriptor.newBuilder() + .setKey("label1") + .setValueType(ValueType.STRING) + .build()) + .addLabels( + LabelDescriptor.newBuilder() + .setKey("label2") + .setValueType(ValueType.STRING) + .build()) + .setMetricKind(MetricKind.CUMULATIVE) + .setValueType(MetricDescriptor.ValueType.INT64) + .setUnit(METRIC_DESCRIPTOR_TIME_UNIT) + .setDescription(aMetricDataWithCustomResource.getDescription()) + .build(); + TimeInterval expectedTimeInterval = + TimeInterval.newBuilder() + .setStartTime( + Timestamp.newBuilder() + .setSeconds(aLongPoint.getStartEpochNanos() / NANO_PER_SECOND) + .setNanos(0) + .build()) + .setEndTime( + Timestamp.newBuilder() + .setSeconds(aLongPoint.getEpochNanos() / NANO_PER_SECOND) + .setNanos(0) + .build()) + .build(); + Point expectedPoint = + Point.newBuilder() + .setValue(TypedValue.newBuilder().setInt64Value(aLongPoint.getValue())) + .setInterval(expectedTimeInterval) + .build(); + TimeSeries expectedTimeSeries = + TimeSeries.newBuilder() + .setMetric( + Metric.newBuilder() + .setType(expectedDescriptor.getType()) + .putLabels("service_instance_id", "test-gcs-service-id") + .putLabels("label1", "value1") + .putLabels("label2", "false") + .putLabels(LABEL_INSTRUMENTATION_SOURCE, "instrumentName") + .putLabels(LABEL_INSTRUMENTATION_VERSION, "0") + .build()) + .addPoints(expectedPoint) + .setMetricKind(expectedDescriptor.getMetricKind()) + .setResource(MonitoredResource.newBuilder().setType("custom_mr_instance").build()) + .build(); + CreateMetricDescriptorRequest expectedRequest = + CreateMetricDescriptorRequest.newBuilder() + .setName("projects/" + aProjectId) + .setMetricDescriptor(expectedDescriptor) + .build(); + ProjectName expectedProjectName = ProjectName.of(aProjectId); + + MetricExporter exporter = + InternalMetricExporter.createWithClient( + aProjectId, + DEFAULT_PREFIX, + mockClient, + MetricDescriptorStrategy.ALWAYS_SEND, + customAttributesFilter, + false, + monitoredResourceDescription); + + CompletableResultCode result = exporter.export(ImmutableList.of(aMetricDataWithCustomResource)); + verify(mockClient, times(1)).createMetricDescriptor(metricDescriptorCaptor.capture()); + verify(mockClient, times(1)) + .createTimeSeries(projectNameArgCaptor.capture(), timeSeriesArgCaptor.capture()); + + assertTrue(result.isSuccess()); + assertEquals(expectedRequest, metricDescriptorCaptor.getValue()); + assertEquals(expectedProjectName, projectNameArgCaptor.getValue()); + assertEquals(1, timeSeriesArgCaptor.getValue().size()); + assertEquals(expectedTimeSeries, timeSeriesArgCaptor.getValue().get(0)); + } + + @Test + public void testExportWithMonitoredResourceMappingSucceeds_NoResourceLabels() { + MonitoredResourceDescription monitoredResourceDescription = + new MonitoredResourceDescription( + "custom_mr_instance", Set.of("service_instance_id", "host_id", "location")); + + // controls which resource attributes end up in metric labels + Predicate> customAttributesFilter = + attributeKey -> + !attributeKey.getKey().isEmpty() && attributeKey.getKey().equals("service_instance_id"); + + MetricDescriptor expectedDescriptor = + MetricDescriptor.newBuilder() + .setDisplayName(aMetricDataWithEmptyResourceAttributes.getName()) + .setType(DEFAULT_PREFIX + "/" + aMetricDataWithEmptyResourceAttributes.getName()) + .addLabels( + LabelDescriptor.newBuilder() + .setKey("label1") + .setValueType(ValueType.STRING) + .build()) + .addLabels( + LabelDescriptor.newBuilder() + .setKey("label2") + .setValueType(ValueType.STRING) + .build()) + .setMetricKind(MetricKind.CUMULATIVE) + .setValueType(MetricDescriptor.ValueType.INT64) + .setUnit(METRIC_DESCRIPTOR_TIME_UNIT) + .setDescription(aMetricDataWithEmptyResourceAttributes.getDescription()) + .build(); + TimeInterval expectedTimeInterval = + TimeInterval.newBuilder() + .setStartTime( + Timestamp.newBuilder() + .setSeconds(aLongPoint.getStartEpochNanos() / NANO_PER_SECOND) + .setNanos(0) + .build()) + .setEndTime( + Timestamp.newBuilder() + .setSeconds(aLongPoint.getEpochNanos() / NANO_PER_SECOND) + .setNanos(0) + .build()) + .build(); + Point expectedPoint = + Point.newBuilder() + .setValue(TypedValue.newBuilder().setInt64Value(aLongPoint.getValue())) + .setInterval(expectedTimeInterval) + .build(); + TimeSeries expectedTimeSeries = + TimeSeries.newBuilder() + .setMetric( + Metric.newBuilder() + .setType(expectedDescriptor.getType()) + .putLabels("label1", "value1") + .putLabels("label2", "false") + .putLabels(LABEL_INSTRUMENTATION_SOURCE, "instrumentName") + .putLabels(LABEL_INSTRUMENTATION_VERSION, "0") + .build()) + .addPoints(expectedPoint) + .setMetricKind(expectedDescriptor.getMetricKind()) + .setResource(MonitoredResource.newBuilder().setType("custom_mr_instance").build()) + .build(); + CreateMetricDescriptorRequest expectedRequest = + CreateMetricDescriptorRequest.newBuilder() + .setName("projects/" + aProjectId) + .setMetricDescriptor(expectedDescriptor) + .build(); + ProjectName expectedProjectName = ProjectName.of(aProjectId); + + MetricExporter exporter = + InternalMetricExporter.createWithClient( + aProjectId, + DEFAULT_PREFIX, + mockClient, + MetricDescriptorStrategy.ALWAYS_SEND, + customAttributesFilter, + false, + monitoredResourceDescription); + + CompletableResultCode result = + exporter.export(ImmutableList.of(aMetricDataWithEmptyResourceAttributes)); + verify(mockClient, times(1)).createMetricDescriptor(metricDescriptorCaptor.capture()); + verify(mockClient, times(1)) + .createTimeSeries(projectNameArgCaptor.capture(), timeSeriesArgCaptor.capture()); + + assertTrue(result.isSuccess()); + assertEquals(expectedRequest, metricDescriptorCaptor.getValue()); + assertEquals(expectedProjectName, projectNameArgCaptor.getValue()); + assertEquals(1, timeSeriesArgCaptor.getValue().size()); + assertEquals(expectedTimeSeries, timeSeriesArgCaptor.getValue().get(0)); + } + @Test public void verifyExporterWorksWithDefaultConfiguration() { try (MockedStatic mockedServiceOptions = @@ -466,7 +767,8 @@ public void verifyExporterExportGoogleServiceMetrics() { mockClient, MetricDescriptorStrategy.ALWAYS_SEND, NO_RESOURCE_ATTRIBUTES, - true); + true, + EMPTY_MONITORED_RESOURCE_DESCRIPTION); CompletableResultCode result = exporter.export(ImmutableList.of(googleComputeServiceMetricData)); diff --git a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/MetricConfigurationTest.java b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/MetricConfigurationTest.java index ab8119f2..62add406 100644 --- a/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/MetricConfigurationTest.java +++ b/exporters/metrics/src/test/java/com/google/cloud/opentelemetry/metric/MetricConfigurationTest.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -29,6 +30,7 @@ import io.opentelemetry.api.common.AttributeKey; import java.time.Duration; import java.util.Date; +import java.util.Set; import java.util.function.Predicate; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,22 +53,29 @@ public void testDefaultConfigurationSucceeds() { assertNull(configuration.getCredentials()); assertEquals(PROJECT_ID, configuration.getProjectId()); assertFalse(configuration.getUseServiceTimeSeries()); + assertNotNull(configuration.getResourceAttributesFilter()); + assertNotNull(configuration.getMonitoredResourceDescription()); } @Test public void testSetAllConfigurationFieldsSucceeds() { Predicate> allowAllPredicate = attributeKey -> true; + MonitoredResourceDescription customMRMapping = + new MonitoredResourceDescription("custom_mr", Set.of("instance_id", "foo_bar", "host_id")); + MetricConfiguration configuration = MetricConfiguration.builder() .setProjectId(PROJECT_ID) .setCredentials(FAKE_CREDENTIALS) .setResourceAttributesFilter(allowAllPredicate) + .setMonitoredResourceDescription(customMRMapping) .setUseServiceTimeSeries(true) .build(); assertEquals(FAKE_CREDENTIALS, configuration.getCredentials()); assertEquals(PROJECT_ID, configuration.getProjectId()); assertEquals(allowAllPredicate, configuration.getResourceAttributesFilter()); + assertEquals(customMRMapping, configuration.getMonitoredResourceDescription()); assertTrue(configuration.getUseServiceTimeSeries()); } diff --git a/propagators/gcp/README.md b/propagators/gcp/README.md index 864f3ad5..7a3b1b18 100644 --- a/propagators/gcp/README.md +++ b/propagators/gcp/README.md @@ -13,9 +13,9 @@ The preferred mechanism is to use the [SDK autoconfigure extension](https://gith - Add a runtime dependency from your java project to this library. - You can now use `oneway-gcp` and `gcp` as viable propagation strings in [the `otel.propagators` flag](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/autoconfigure#propagator) -- When using the autoinstrumentation agent, you also need to pass our propagators to the autoinstrumentation "extension" classpath. This can be done through the `otel.javaagent.experimental.extensions` flag. +- When using the autoinstrumentation agent, you also need to pas our propagators to the autoinstrumentation "extension" classpath. This can be done through the `otel.javaagent.extensions` flag. ``` - -Dotel.javaagent.experimental.extensions=/path/to/downloaded/gcp-propagator.jar + -Dotel.javaagent.extensions=/path/to/downloaded/gcp-propagator.jar ``` For a complete example see [here](https://github.com/GoogleCloudPlatform/opentelemetry-operations-java/blob/main/examples/autoinstrument/build.gradle#L63). diff --git a/settings.gradle b/settings.gradle index b47840e0..2fc46b66 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,6 +26,7 @@ rootProject.name = "opentelemetry-operations-java" include ":exporter-trace" include ":examples-trace" +include ":examples-otlp-spring" include ":examples-otlptrace" include ":exporter-metrics" include ":examples-metrics" @@ -84,3 +85,6 @@ project(':propagators-gcp').projectDir = project(':shared-resourcemapping').projectDir = "$rootDir/shared/resourcemapping" as File + +project(':examples-otlp-spring').projectDir = + "$rootDir/examples/otlp-spring" as File diff --git a/shared/resourcemapping/build.gradle b/shared/resourcemapping/build.gradle index 55fb8137..2e0d715d 100644 --- a/shared/resourcemapping/build.gradle +++ b/shared/resourcemapping/build.gradle @@ -28,7 +28,9 @@ dependencies { annotationProcessor(libraries.auto_value) implementation(libraries.opentelemetry_semconv) implementation platform(libraries.opentelemetry_bom) - testImplementation(testLibraries.junit) + testImplementation(testLibraries.junit5) + testImplementation(testLibraries.junit5_params) + testRuntimeOnly(testLibraries.junit5_runtime) testImplementation(libraries.opentelemetry_semconv) } @@ -57,6 +59,11 @@ publishing { } } +test { + // required for discovering JUnit 5 tests + useJUnitPlatform() +} + // This is to fix the explicit dependency error which comes when publishing via the `candidate` task publishMavenPublicationToMavenRepository.dependsOn jar signMavenPublication.dependsOn jar diff --git a/shared/resourcemapping/src/main/java/com/google/cloud/opentelemetry/resource/ResourceTranslator.java b/shared/resourcemapping/src/main/java/com/google/cloud/opentelemetry/resource/ResourceTranslator.java index a6a3df6b..988613f1 100644 --- a/shared/resourcemapping/src/main/java/com/google/cloud/opentelemetry/resource/ResourceTranslator.java +++ b/shared/resourcemapping/src/main/java/com/google/cloud/opentelemetry/resource/ResourceTranslator.java @@ -27,7 +27,7 @@ /** Translates from OpenTelemetry Resource into Google Cloud's notion of resource. */ public class ResourceTranslator { - private static String UNKNOWN_SERVICE_PREFIX = "unknown_service"; + private static final String UNKNOWN_SERVICE_PREFIX = "unknown_service"; private ResourceTranslator() {} @@ -89,11 +89,11 @@ public static AttributeMapping create( } } - private static List GCE_INSTANCE_LABELS = + private static final List GCE_INSTANCE_LABELS = Arrays.asList( AttributeMapping.create("zone", ResourceAttributes.CLOUD_AVAILABILITY_ZONE), AttributeMapping.create("instance_id", ResourceAttributes.HOST_ID)); - private static List K8S_CONTAINER_LABELS = + private static final List K8S_CONTAINER_LABELS = Arrays.asList( AttributeMapping.create( "location", @@ -103,18 +103,42 @@ public static AttributeMapping create( AttributeMapping.create("namespace_name", ResourceAttributes.K8S_NAMESPACE_NAME), AttributeMapping.create("container_name", ResourceAttributes.K8S_CONTAINER_NAME), AttributeMapping.create("pod_name", ResourceAttributes.K8S_POD_NAME)); - private static List AWS_EC2_INSTANCE_LABELS = + private static final List K8S_POD_LABELS = + Arrays.asList( + AttributeMapping.create( + "location", + Arrays.asList( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION)), + AttributeMapping.create("cluster_name", ResourceAttributes.K8S_CLUSTER_NAME), + AttributeMapping.create("namespace_name", ResourceAttributes.K8S_NAMESPACE_NAME), + AttributeMapping.create("pod_name", ResourceAttributes.K8S_POD_NAME)); + private static final List K8S_NODE_LABELS = + Arrays.asList( + AttributeMapping.create( + "location", + Arrays.asList( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION)), + AttributeMapping.create("cluster_name", ResourceAttributes.K8S_CLUSTER_NAME), + AttributeMapping.create("node_name", ResourceAttributes.K8S_NODE_NAME)); + private static final List K8S_CLUSTER_LABELS = + Arrays.asList( + AttributeMapping.create( + "location", + Arrays.asList( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, ResourceAttributes.CLOUD_REGION)), + AttributeMapping.create("cluster_name", ResourceAttributes.K8S_CLUSTER_NAME)); + private static final List AWS_EC2_INSTANCE_LABELS = Arrays.asList( AttributeMapping.create("instance_id", ResourceAttributes.HOST_ID), AttributeMapping.create("region", ResourceAttributes.CLOUD_AVAILABILITY_ZONE), AttributeMapping.create("aws_account", ResourceAttributes.CLOUD_ACCOUNT_ID)); - private static List GOOGLE_CLOUD_APP_ENGINE_INSTANCE_LABELS = + private static final List GOOGLE_CLOUD_APP_ENGINE_INSTANCE_LABELS = Arrays.asList( AttributeMapping.create("module_id", ResourceAttributes.FAAS_NAME), AttributeMapping.create("version_id", ResourceAttributes.FAAS_VERSION), AttributeMapping.create("instance_id", ResourceAttributes.FAAS_INSTANCE), AttributeMapping.create("location", ResourceAttributes.CLOUD_REGION)); - private static List GENERIC_TASK_LABELS = + private static final List GENERIC_TASK_LABELS = Arrays.asList( AttributeMapping.create( "location", @@ -131,7 +155,7 @@ public static AttributeMapping create( Arrays.asList( ResourceAttributes.SERVICE_INSTANCE_ID, ResourceAttributes.FAAS_INSTANCE), "")); - private static List GENERIC_NODE_LABELS = + private static final List GENERIC_NODE_LABELS = Arrays.asList( AttributeMapping.create( "location", @@ -148,20 +172,35 @@ public static AttributeMapping create( public static GcpResource mapResource(Resource resource) { String platform = resource.getAttribute(ResourceAttributes.CLOUD_PLATFORM); if (platform == null) { - return genericTaskOrNode(resource); + return mapK8sResourceOrGenericTaskOrNode(resource); } switch (platform) { case ResourceAttributes.CloudPlatformValues.GCP_COMPUTE_ENGINE: return mapBase(resource, "gce_instance", GCE_INSTANCE_LABELS); - case ResourceAttributes.CloudPlatformValues.GCP_KUBERNETES_ENGINE: - return mapBase(resource, "k8s_container", K8S_CONTAINER_LABELS); case ResourceAttributes.CloudPlatformValues.AWS_EC2: return mapBase(resource, "aws_ec2_instance", AWS_EC2_INSTANCE_LABELS); case ResourceAttributes.CloudPlatformValues.GCP_APP_ENGINE: return mapBase(resource, "gae_instance", GOOGLE_CLOUD_APP_ENGINE_INSTANCE_LABELS); default: - return genericTaskOrNode(resource); + return mapK8sResourceOrGenericTaskOrNode(resource); + } + } + + private static GcpResource mapK8sResourceOrGenericTaskOrNode(Resource resource) { + // if k8s.cluster.name is set, pattern match for various k8s resources. + // this will also match non-cloud k8s platforms like minikube. + if (resource.getAttribute(ResourceAttributes.K8S_CLUSTER_NAME) != null) { + if (resource.getAttribute(ResourceAttributes.K8S_CONTAINER_NAME) != null) { + return mapBase(resource, "k8s_container", K8S_CONTAINER_LABELS); + } else if (resource.getAttribute(ResourceAttributes.K8S_POD_NAME) != null) { + return mapBase(resource, "k8s_pod", K8S_POD_LABELS); + } else if (resource.getAttribute(ResourceAttributes.K8S_NODE_NAME) != null) { + return mapBase(resource, "k8s_node", K8S_NODE_LABELS); + } else { + return mapBase(resource, "k8s_cluster", K8S_CLUSTER_LABELS); + } } + return genericTaskOrNode(resource); } private static GcpResource genericTaskOrNode(Resource resource) { diff --git a/shared/resourcemapping/src/test/java/com/google/cloud/opentelemetry/resource/ResourceTranslatorTest.java b/shared/resourcemapping/src/test/java/com/google/cloud/opentelemetry/resource/ResourceTranslatorTest.java index 66b90f93..95cd9efd 100644 --- a/shared/resourcemapping/src/test/java/com/google/cloud/opentelemetry/resource/ResourceTranslatorTest.java +++ b/shared/resourcemapping/src/test/java/com/google/cloud/opentelemetry/resource/ResourceTranslatorTest.java @@ -15,372 +15,427 @@ */ package com.google.cloud.opentelemetry.resource; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.semconv.ResourceAttributes; +import java.util.AbstractMap.SimpleEntry; +import java.util.Collections; import java.util.Map; -import java.util.stream.Collectors; import java.util.stream.Stream; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; -@RunWith(JUnit4.class) public class ResourceTranslatorTest { - @Test - public void testMapResourcesWithGCEResource() { - Map, String> testAttributes = - java.util.stream.Stream.of( - new Object[][] { - {ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP}, - { - ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CloudPlatformValues.GCP_COMPUTE_ENGINE - }, - {ResourceAttributes.CLOUD_ACCOUNT_ID, "GCE-pid"}, - {ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"}, - {ResourceAttributes.CLOUD_REGION, "country-region"}, - {ResourceAttributes.HOST_ID, "GCE-instance-id"}, - {ResourceAttributes.HOST_NAME, "GCE-instance-name"}, - {ResourceAttributes.HOST_TYPE, "GCE-instance-type"} - }) - .collect( - Collectors.toMap(data -> (AttributeKey) data[0], data -> (String) data[1])); - AttributesBuilder attrBuilder = Attributes.builder(); - testAttributes.forEach( - (key, value) -> { - attrBuilder.put(key, value); - }); - Attributes attributes = attrBuilder.build(); - - GcpResource monitoredResource = - ResourceTranslator.mapResource(io.opentelemetry.sdk.resources.Resource.create(attributes)); - - assertEquals("gce_instance", monitoredResource.getResourceType()); - Map monitoredResourceMap = monitoredResource.getResourceLabels().getLabels(); - assertEquals(2, monitoredResourceMap.size()); - - Map expectedMappings = - Stream.of( - new Object[][] { - {"instance_id", "GCE-instance-id"}, - {"zone", "country-region-zone"}, - }) - .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1])); - expectedMappings.forEach( - (key, value) -> { - assertEquals(value, monitoredResourceMap.get(key)); - }); + private static Arguments generateOTelResourceMappingTestArgs( + Map, String> otelResourceLabels, + String expectedMRType, + Map expectedMRLabels) { + return Arguments.of(otelResourceLabels, expectedMRType, expectedMRLabels); } - @Test - public void testMapResourcesWithGKEResource() { - Map, String> testAttributes = - Stream.of( - new Object[][] { - {ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP}, - { + private static Stream provideOTelResourceAttributesToMonitoredResourceMapping() { + return Stream.of( + // test cases for k8s + // test cases for k8s_container on [GCP, AWS, Azure, non-cloud] + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP), + new SimpleEntry<>( ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CloudPlatformValues.GCP_KUBERNETES_ENGINE - }, - {ResourceAttributes.CLOUD_ACCOUNT_ID, "GCE-pid"}, - {ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"}, - {ResourceAttributes.CLOUD_REGION, "country-region"}, - {ResourceAttributes.HOST_ID, "GCE-instance-id"}, - {ResourceAttributes.HOST_NAME, "GCE-instance-name"}, - {ResourceAttributes.HOST_TYPE, "GCE-instance-type"}, - {ResourceAttributes.K8S_CLUSTER_NAME, "GKE-cluster-name"}, - {ResourceAttributes.K8S_NAMESPACE_NAME, "GKE-testNameSpace"}, - {ResourceAttributes.K8S_POD_NAME, "GKE-testHostName"}, - {ResourceAttributes.K8S_CONTAINER_NAME, "GKE-testContainerName"} - }) - .collect( - Collectors.toMap(data -> (AttributeKey) data[0], data -> (String) data[1])); - AttributesBuilder attrBuilder = Attributes.builder(); - testAttributes.forEach(attrBuilder::put); - Attributes attributes = attrBuilder.build(); - - GcpResource monitoredResource = ResourceTranslator.mapResource(Resource.create(attributes)); - - assertEquals("k8s_container", monitoredResource.getResourceType()); - Map monitoredResourceMap = monitoredResource.getResourceLabels().getLabels(); - assertEquals(5, monitoredResourceMap.size()); - - Map expectedMappings = - Stream.of( - new Object[][] { - {"location", "country-region-zone"}, - {"cluster_name", "GKE-cluster-name"}, - {"namespace_name", "GKE-testNameSpace"}, - {"pod_name", "GKE-testHostName"}, - {"container_name", "GKE-testContainerName"} - }) - .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1])); - expectedMappings.forEach( - (key, value) -> { - assertEquals(value, monitoredResourceMap.get(key)); - }); - } - - @Test - public void testMapResourcesWithAwsEc2Instance() { - Map, String> testAttributes = - Stream.of( - new Object[][] { - {ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP}, - { + ResourceAttributes.CloudPlatformValues.GCP_KUBERNETES_ENGINE), + new SimpleEntry<>(ResourceAttributes.CLOUD_ACCOUNT_ID, "GCE-pid"), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.CLOUD_REGION, "country-region"), + new SimpleEntry<>(ResourceAttributes.HOST_ID, "GCE-instance-id"), + new SimpleEntry<>(ResourceAttributes.HOST_NAME, "GCE-instance-name"), + new SimpleEntry<>(ResourceAttributes.HOST_TYPE, "GCE-instance-type"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "GKE-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NAMESPACE_NAME, "GKE-testNameSpace"), + new SimpleEntry<>(ResourceAttributes.K8S_POD_NAME, "GKE-testHostName"), + new SimpleEntry<>(ResourceAttributes.K8S_CONTAINER_NAME, "GKE-testContainerName")), + "k8s_container", + Map.ofEntries( + new SimpleEntry<>("location", "country-region-zone"), + new SimpleEntry<>("cluster_name", "GKE-cluster-name"), + new SimpleEntry<>("namespace_name", "GKE-testNameSpace"), + new SimpleEntry<>("pod_name", "GKE-testHostName"), + new SimpleEntry<>("container_name", "GKE-testContainerName"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS), + new SimpleEntry<>( ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CloudPlatformValues.AWS_EC2 - }, - {ResourceAttributes.CLOUD_ACCOUNT_ID, "aws-id"}, - {ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"}, - {ResourceAttributes.CLOUD_REGION, "country-region"}, - {ResourceAttributes.HOST_ID, "aws-instance-id"} - }) - .collect( - Collectors.toMap(data -> (AttributeKey) data[0], data -> (String) data[1])); - AttributesBuilder attrBuilder = Attributes.builder(); - testAttributes.forEach(attrBuilder::put); - Attributes attributes = attrBuilder.build(); - - GcpResource monitoredResource = ResourceTranslator.mapResource(Resource.create(attributes)); - - assertEquals("aws_ec2_instance", monitoredResource.getResourceType()); - Map monitoredResourceMap = monitoredResource.getResourceLabels().getLabels(); - assertEquals(3, monitoredResourceMap.size()); - - Map expectedMappings = - Stream.of( - new Object[][] { - {"region", "country-region-zone"}, - {"instance_id", "aws-instance-id"}, - {"aws_account", "aws-id"} - }) - .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1])); - expectedMappings.forEach( - (key, value) -> { - assertEquals(value, monitoredResourceMap.get(key)); - }); - } - - @Test - public void testMapResourcesWithGenericTaskFallback_FAASIgnored() { - Map, String> testAttributes = - java.util.stream.Stream.of( - new Object[][] { - {ResourceAttributes.SERVICE_NAME, "my-service-prevailed"}, - {ResourceAttributes.FAAS_NAME, "my-service-ignored"}, - {ResourceAttributes.SERVICE_NAMESPACE, "prod"}, - {ResourceAttributes.FAAS_INSTANCE, "1234"} - }) - .collect( - Collectors.toMap(data -> (AttributeKey) data[0], data -> (String) data[1])); - AttributesBuilder attrBuilder = Attributes.builder(); - testAttributes.forEach(attrBuilder::put); - Attributes attributes = attrBuilder.build(); - - GcpResource monitoredResource = - ResourceTranslator.mapResource(io.opentelemetry.sdk.resources.Resource.create(attributes)); - - assertEquals("generic_task", monitoredResource.getResourceType()); - - Map monitoredResourceMap = monitoredResource.getResourceLabels().getLabels(); - assertEquals(4, monitoredResourceMap.size()); - - Map expectedMappings = - Stream.of( - new Object[][] { - {"job", "my-service-prevailed"}, - {"namespace", "prod"}, - {"task_id", "1234"}, - {"location", "global"}, - }) - .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1])); - expectedMappings.forEach( - (key, value) -> { - assertEquals(value, monitoredResourceMap.get(key)); - }); - } - - @Test - public void testMapResourcesWithGenericTaskFallback_FAASPrevailed() { - Map, String> testAttributes = - java.util.stream.Stream.of( - new Object[][] { - {ResourceAttributes.SERVICE_NAME, "unknown_service_foo"}, - {ResourceAttributes.FAAS_NAME, "my-service-faas"}, - {ResourceAttributes.SERVICE_NAMESPACE, "prod"}, - {ResourceAttributes.FAAS_INSTANCE, "1234"} - }) - .collect( - Collectors.toMap(data -> (AttributeKey) data[0], data -> (String) data[1])); - AttributesBuilder attrBuilder = Attributes.builder(); - testAttributes.forEach(attrBuilder::put); - Attributes attributes = attrBuilder.build(); - - GcpResource monitoredResource = - ResourceTranslator.mapResource(io.opentelemetry.sdk.resources.Resource.create(attributes)); - - assertEquals("generic_task", monitoredResource.getResourceType()); - - Map monitoredResourceMap = monitoredResource.getResourceLabels().getLabels(); - assertEquals(4, monitoredResourceMap.size()); - - Map expectedMappings = - Stream.of( - new Object[][] { - {"job", "my-service-faas"}, - {"namespace", "prod"}, - {"task_id", "1234"}, - {"location", "global"}, - }) - .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1])); - expectedMappings.forEach( - (key, value) -> { - assertEquals(value, monitoredResourceMap.get(key)); - }); - } - - @Test - public void testMapResourcesWithGenericTaskFallback_UnknownService() { - Map, String> testAttributes = - java.util.stream.Stream.of( - new Object[][] { - {ResourceAttributes.SERVICE_NAME, "unknown_service_foo"}, - {ResourceAttributes.SERVICE_NAMESPACE, "prod"}, - {ResourceAttributes.FAAS_INSTANCE, "1234"} - }) - .collect( - Collectors.toMap(data -> (AttributeKey) data[0], data -> (String) data[1])); - AttributesBuilder attrBuilder = Attributes.builder(); - testAttributes.forEach(attrBuilder::put); - Attributes attributes = attrBuilder.build(); - - GcpResource monitoredResource = - ResourceTranslator.mapResource(io.opentelemetry.sdk.resources.Resource.create(attributes)); - - assertEquals("generic_task", monitoredResource.getResourceType()); - - Map monitoredResourceMap = monitoredResource.getResourceLabels().getLabels(); - assertEquals(4, monitoredResourceMap.size()); - - Map expectedMappings = - Stream.of( - new Object[][] { - {"job", "unknown_service_foo"}, - {"namespace", "prod"}, - {"task_id", "1234"}, - {"location", "global"}, - }) - .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1])); - expectedMappings.forEach( - (key, value) -> { - assertEquals(value, monitoredResourceMap.get(key)); - }); + ResourceAttributes.CloudPlatformValues.AWS_EKS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "EKS-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NAMESPACE_NAME, "EKS-namespace-name"), + new SimpleEntry<>(ResourceAttributes.K8S_POD_NAME, "EKS-pod-name"), + new SimpleEntry<>(ResourceAttributes.K8S_CONTAINER_NAME, "EKS-container-name")), + "k8s_container", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "EKS-cluster-name"), + new SimpleEntry<>("namespace_name", "EKS-namespace-name"), + new SimpleEntry<>("pod_name", "EKS-pod-name"), + new SimpleEntry<>("container_name", "EKS-container-name"), + new SimpleEntry<>("location", "country-region-zone"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, + ResourceAttributes.CloudProviderValues.AZURE), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.AZURE_AKS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "AKS-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NAMESPACE_NAME, "AKS-namespace-name"), + new SimpleEntry<>(ResourceAttributes.K8S_POD_NAME, "AKS-pod-name"), + new SimpleEntry<>(ResourceAttributes.K8S_CONTAINER_NAME, "AKS-container-name")), + "k8s_container", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "AKS-cluster-name"), + new SimpleEntry<>("namespace_name", "AKS-namespace-name"), + new SimpleEntry<>("pod_name", "AKS-pod-name"), + new SimpleEntry<>("container_name", "AKS-container-name"), + new SimpleEntry<>("location", "country-region-zone"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "non-cloud-cluster-name"), + new SimpleEntry<>( + ResourceAttributes.K8S_NAMESPACE_NAME, "non-cloud-namespace-name"), + new SimpleEntry<>(ResourceAttributes.K8S_POD_NAME, "non-cloud-pod-name"), + new SimpleEntry<>( + ResourceAttributes.K8S_CONTAINER_NAME, "non-cloud-container-name")), + "k8s_container", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "non-cloud-cluster-name"), + new SimpleEntry<>("namespace_name", "non-cloud-namespace-name"), + new SimpleEntry<>("pod_name", "non-cloud-pod-name"), + new SimpleEntry<>("container_name", "non-cloud-container-name"), + new SimpleEntry<>("location", "country-region-zone"))), + // test cases for k8s_pod on [GCP, AWS, Azure, non-cloud] + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.GCP_KUBERNETES_ENGINE), + new SimpleEntry<>(ResourceAttributes.CLOUD_ACCOUNT_ID, "GCE-pid"), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.CLOUD_REGION, "country-region"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "GKE-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NAMESPACE_NAME, "GKE-testNameSpace"), + new SimpleEntry<>(ResourceAttributes.K8S_POD_NAME, "GKE-testHostName")), + "k8s_pod", + Map.ofEntries( + new SimpleEntry<>("location", "country-region-zone"), + new SimpleEntry<>("cluster_name", "GKE-cluster-name"), + new SimpleEntry<>("namespace_name", "GKE-testNameSpace"), + new SimpleEntry<>("pod_name", "GKE-testHostName"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.AWS_EKS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "EKS-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NAMESPACE_NAME, "EKS-namespace-name"), + new SimpleEntry<>(ResourceAttributes.K8S_POD_NAME, "EKS-pod-name")), + "k8s_pod", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "EKS-cluster-name"), + new SimpleEntry<>("pod_name", "EKS-pod-name"), + new SimpleEntry<>("namespace_name", "EKS-namespace-name"), + new SimpleEntry<>("location", "country-region-zone"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, + ResourceAttributes.CloudProviderValues.AZURE), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.AZURE_AKS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "AKS-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NAMESPACE_NAME, "AKS-namespace-name"), + new SimpleEntry<>(ResourceAttributes.K8S_POD_NAME, "AKS-pod-name")), + "k8s_pod", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "AKS-cluster-name"), + new SimpleEntry<>("pod_name", "AKS-pod-name"), + new SimpleEntry<>("namespace_name", "AKS-namespace-name"), + new SimpleEntry<>("location", "country-region-zone"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "non-cloud-cluster-name"), + new SimpleEntry<>( + ResourceAttributes.K8S_NAMESPACE_NAME, "non-cloud-namespace-name"), + new SimpleEntry<>(ResourceAttributes.K8S_POD_NAME, "non-cloud-pod-name")), + "k8s_pod", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "non-cloud-cluster-name"), + new SimpleEntry<>("pod_name", "non-cloud-pod-name"), + new SimpleEntry<>("namespace_name", "non-cloud-namespace-name"), + new SimpleEntry<>("location", "country-region-zone"))), + // test cases for k8s_node on [GCP, AWS, Azure, non-cloud] + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.GCP_KUBERNETES_ENGINE), + new SimpleEntry<>(ResourceAttributes.CLOUD_ACCOUNT_ID, "GCE-pid"), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.CLOUD_REGION, "country-region"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "GKE-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NODE_NAME, "GKE-node-name")), + "k8s_node", + Map.ofEntries( + new SimpleEntry<>("location", "country-region-zone"), + new SimpleEntry<>("node_name", "GKE-node-name"), + new SimpleEntry<>("cluster_name", "GKE-cluster-name"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.AWS_EKS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "EKS-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NODE_NAME, "EKS-node-name")), + "k8s_node", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "EKS-cluster-name"), + new SimpleEntry<>("node_name", "EKS-node-name"), + new SimpleEntry<>("location", "country-region-zone"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, + ResourceAttributes.CloudProviderValues.AZURE), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.AZURE_AKS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "AKS-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NODE_NAME, "AKS-node-name")), + "k8s_node", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "AKS-cluster-name"), + new SimpleEntry<>("node_name", "AKS-node-name"), + new SimpleEntry<>("location", "country-region-zone"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "non-cloud-cluster-name"), + new SimpleEntry<>(ResourceAttributes.K8S_NODE_NAME, "non-cloud-node-name")), + "k8s_node", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "non-cloud-cluster-name"), + new SimpleEntry<>("node_name", "non-cloud-node-name"), + new SimpleEntry<>("location", "country-region-zone"))), + // test cases for k8s_cluster on [GCP, AWS, Azure, non-cloud] + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.GCP_KUBERNETES_ENGINE), + new SimpleEntry<>(ResourceAttributes.CLOUD_ACCOUNT_ID, "GCE-pid"), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.CLOUD_REGION, "country-region"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "GKE-cluster-name")), + "k8s_cluster", + Map.ofEntries( + new SimpleEntry<>("location", "country-region-zone"), + new SimpleEntry<>("cluster_name", "GKE-cluster-name"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.AWS_EKS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "EKS-cluster-name")), + "k8s_cluster", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "EKS-cluster-name"), + new SimpleEntry<>("location", "country-region-zone"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, + ResourceAttributes.CloudProviderValues.AZURE), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.AZURE_AKS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "AKS-cluster-name")), + "k8s_cluster", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "AKS-cluster-name"), + new SimpleEntry<>("location", "country-region-zone"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.K8S_CLUSTER_NAME, "non-cloud-cluster-name")), + "k8s_cluster", + Map.ofEntries( + new SimpleEntry<>("cluster_name", "non-cloud-cluster-name"), + new SimpleEntry<>("location", "country-region-zone"))), + // test case for GCE Instance + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.GCP_COMPUTE_ENGINE), + new SimpleEntry<>(ResourceAttributes.CLOUD_ACCOUNT_ID, "GCE-pid"), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.CLOUD_REGION, "country-region"), + new SimpleEntry<>(ResourceAttributes.HOST_ID, "GCE-instance-id"), + new SimpleEntry<>(ResourceAttributes.HOST_NAME, "GCE-instance-name"), + new SimpleEntry<>(ResourceAttributes.HOST_TYPE, "GCE-instance-type")), + "gce_instance", + Map.ofEntries( + new SimpleEntry<>("instance_id", "GCE-instance-id"), + new SimpleEntry<>("zone", "country-region-zone"))), + // test case for EC2 Instance + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>( + ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS), + new SimpleEntry<>( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.AWS_EC2), + new SimpleEntry<>(ResourceAttributes.CLOUD_ACCOUNT_ID, "aws-id"), + new SimpleEntry<>( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone"), + new SimpleEntry<>(ResourceAttributes.CLOUD_REGION, "country-region"), + new SimpleEntry<>(ResourceAttributes.HOST_ID, "aws-instance-id")), + "aws_ec2_instance", + Map.ofEntries( + new SimpleEntry<>("region", "country-region-zone"), + new SimpleEntry<>("instance_id", "aws-instance-id"), + new SimpleEntry<>("aws_account", "aws-id"))), + // test cases for generic_task + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>(ResourceAttributes.SERVICE_NAME, "my-service-prevailed"), + new SimpleEntry<>(ResourceAttributes.FAAS_NAME, "my-service-ignored"), + new SimpleEntry<>(ResourceAttributes.SERVICE_NAMESPACE, "prod"), + new SimpleEntry<>(ResourceAttributes.FAAS_INSTANCE, "1234")), + "generic_task", + Map.ofEntries( + new SimpleEntry<>("job", "my-service-prevailed"), + new SimpleEntry<>("namespace", "prod"), + new SimpleEntry<>("task_id", "1234"), + new SimpleEntry<>("location", "global"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>(ResourceAttributes.SERVICE_NAME, "unknown_service_foo"), + new SimpleEntry<>(ResourceAttributes.FAAS_NAME, "my-service-faas"), + new SimpleEntry<>(ResourceAttributes.SERVICE_NAMESPACE, "prod"), + new SimpleEntry<>(ResourceAttributes.FAAS_INSTANCE, "1234")), + "generic_task", + Map.ofEntries( + new SimpleEntry<>("job", "my-service-faas"), + new SimpleEntry<>("namespace", "prod"), + new SimpleEntry<>("task_id", "1234"), + new SimpleEntry<>("location", "global"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>(ResourceAttributes.SERVICE_NAME, "unknown_service_foo"), + new SimpleEntry<>(ResourceAttributes.SERVICE_NAMESPACE, "prod"), + new SimpleEntry<>(ResourceAttributes.FAAS_INSTANCE, "1234")), + "generic_task", + Map.ofEntries( + new SimpleEntry<>("job", "unknown_service_foo"), + new SimpleEntry<>("namespace", "prod"), + new SimpleEntry<>("task_id", "1234"), + new SimpleEntry<>("location", "global"))), + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>(ResourceAttributes.SERVICE_NAME, "my-service-name"), + new SimpleEntry<>(ResourceAttributes.SERVICE_NAMESPACE, "prod"), + new SimpleEntry<>(ResourceAttributes.SERVICE_INSTANCE_ID, "1234")), + "generic_task", + Map.ofEntries( + new SimpleEntry<>("job", "my-service-name"), + new SimpleEntry<>("namespace", "prod"), + new SimpleEntry<>("task_id", "1234"), + new SimpleEntry<>("location", "global"))), + // test cases for generic_node + generateOTelResourceMappingTestArgs( + Map.ofEntries( + new SimpleEntry<>(ResourceAttributes.SERVICE_NAME, "unknown_service"), + new SimpleEntry<>(ResourceAttributes.SERVICE_NAMESPACE, "prod")), + "generic_node", + Map.ofEntries( + new SimpleEntry<>("namespace", "prod"), + new SimpleEntry<>("node_id", ""), + new SimpleEntry<>("location", "global"))), + generateOTelResourceMappingTestArgs( + Collections.emptyMap(), + "generic_node", + Map.ofEntries( + new SimpleEntry<>("namespace", ""), + new SimpleEntry<>("node_id", ""), + new SimpleEntry<>("location", "global")))); } - @Test - public void testMapResourcesWithGlobal() { - Map, String> testAttributes = - java.util.stream.Stream.of( - new Object[][] { - {ResourceAttributes.SERVICE_NAME, "my-service-name"}, - {ResourceAttributes.SERVICE_NAMESPACE, "prod"}, - {ResourceAttributes.SERVICE_INSTANCE_ID, "1234"} - }) - .collect( - Collectors.toMap(data -> (AttributeKey) data[0], data -> (String) data[1])); - AttributesBuilder attrBuilder = Attributes.builder(); - testAttributes.forEach(attrBuilder::put); - Attributes attributes = attrBuilder.build(); - - GcpResource monitoredResource = - ResourceTranslator.mapResource(io.opentelemetry.sdk.resources.Resource.create(attributes)); - - assertEquals("generic_task", monitoredResource.getResourceType()); - - Map monitoredResourceMap = monitoredResource.getResourceLabels().getLabels(); - assertEquals(4, monitoredResourceMap.size()); - - Map expectedMappings = - Stream.of( - new Object[][] { - {"job", "my-service-name"}, - {"namespace", "prod"}, - {"task_id", "1234"}, - {"location", "global"}, - }) - .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1])); - expectedMappings.forEach( - (key, value) -> { - assertEquals(value, monitoredResourceMap.get(key)); - }); - } + @ParameterizedTest + @MethodSource("provideOTelResourceAttributesToMonitoredResourceMapping") + public void testMapResourcesWithGCPResources( + Map, String> resourceAttributes, + String expectedResourceType, + Map expectedResources) { - @Test - public void testMapResourcesFallbackServiceNameOnly() { - Map, String> testAttributes = - java.util.stream.Stream.of( - new Object[][] { - {ResourceAttributes.SERVICE_NAME, "unknown_service"}, - {ResourceAttributes.SERVICE_NAMESPACE, "prod"} - }) - .collect( - Collectors.toMap(data -> (AttributeKey) data[0], data -> (String) data[1])); AttributesBuilder attrBuilder = Attributes.builder(); - testAttributes.forEach(attrBuilder::put); + resourceAttributes.forEach(attrBuilder::put); Attributes attributes = attrBuilder.build(); - GcpResource monitoredResource = - ResourceTranslator.mapResource(io.opentelemetry.sdk.resources.Resource.create(attributes)); - - assertEquals("generic_node", monitoredResource.getResourceType()); - - Map monitoredResourceMap = monitoredResource.getResourceLabels().getLabels(); - assertEquals(3, monitoredResourceMap.size()); - - Map expectedMappings = - Stream.of( - new Object[][] { - {"namespace", "prod"}, - {"node_id", ""}, - {"location", "global"}, - }) - .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1])); - expectedMappings.forEach( - (key, value) -> { - assertEquals(value, monitoredResourceMap.get(key)); - }); - } - - @Test - public void testMapResourcesFallback() { - Attributes attributes = Attributes.builder().build(); - - GcpResource monitoredResource = - ResourceTranslator.mapResource(io.opentelemetry.sdk.resources.Resource.create(attributes)); - - assertEquals("generic_node", monitoredResource.getResourceType()); + GcpResource monitoredResource = ResourceTranslator.mapResource(Resource.create(attributes)); + assertEquals(expectedResourceType, monitoredResource.getResourceType()); Map monitoredResourceMap = monitoredResource.getResourceLabels().getLabels(); - assertEquals(3, monitoredResourceMap.size()); + assertEquals(expectedResources.size(), monitoredResourceMap.size()); - Map expectedMappings = - Stream.of( - new Object[][] { - {"namespace", ""}, - {"node_id", ""}, - {"location", "global"}, - }) - .collect(Collectors.toMap(data -> (String) data[0], data -> (String) data[1])); - expectedMappings.forEach( - (key, value) -> { - assertEquals(value, monitoredResourceMap.get(key)); - }); + expectedResources.forEach((key, value) -> assertEquals(value, monitoredResourceMap.get(key))); } }