From cde09dba4cd6577b4a7a24d2ff194e658fc775a6 Mon Sep 17 00:00:00 2001 From: Karsten Schnitter Date: Mon, 9 Dec 2024 10:33:09 +0100 Subject: [PATCH 1/2] Add resource provider for CloudFoundry Adds a module to provide a resource provider for applications deployed to CloudFoundry. It parses the VCAP_APPLICATION environment variable to fill in the attributes specified in the semantic conventions[1]. The implementation follows the OsResource in the resources module. [1] https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/cloudfoundry.md Signed-off-by: Karsten Schnitter --- .../library/build.gradle.kts | 15 +++ .../resources/CloudFoundryResource.java | 107 ++++++++++++++++++ .../CloudFoundryResourceProvider.java | 20 ++++ .../resources/CloudFoundryResourceTest.java | 72 ++++++++++++ settings.gradle.kts | 1 + 5 files changed, 215 insertions(+) create mode 100644 instrumentation/cloudfoundry/cloudfoundry-resources/library/build.gradle.kts create mode 100644 instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResource.java create mode 100644 instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResourceProvider.java create mode 100644 instrumentation/cloudfoundry/cloudfoundry-resources/library/src/test/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResourceTest.java diff --git a/instrumentation/cloudfoundry/cloudfoundry-resources/library/build.gradle.kts b/instrumentation/cloudfoundry/cloudfoundry-resources/library/build.gradle.kts new file mode 100644 index 000000000000..6417f7c627c7 --- /dev/null +++ b/instrumentation/cloudfoundry/cloudfoundry-resources/library/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("otel.sdk-extension") +} + +dependencies { + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + + annotationProcessor("com.google.auto.service:auto-service") + compileOnly("com.google.auto.service:auto-service-annotations") + testCompileOnly("com.google.auto.service:auto-service-annotations") + + implementation("org.snakeyaml:snakeyaml-engine") + + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") +} diff --git a/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResource.java b/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResource.java new file mode 100644 index 000000000000..8e1939c54015 --- /dev/null +++ b/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResource.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cloudfoundry.resources; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +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.SchemaUrls; +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; +import org.snakeyaml.engine.v2.api.Load; +import org.snakeyaml.engine.v2.api.LoadSettings; + +public final class CloudFoundryResource { + + private static final String ENV_VCAP_APPLICATION = "VCAP_APPLICATION"; + + // copied from CloudfoundryIncubatingAttributes + private static final AttributeKey CLOUDFOUNDRY_APP_ID = + AttributeKey.stringKey("cloudfoundry.app.id"); + private static final AttributeKey CLOUDFOUNDRY_APP_INSTANCE_ID = + AttributeKey.stringKey("cloudfoundry.app.instance.id"); + private static final AttributeKey CLOUDFOUNDRY_APP_NAME = + AttributeKey.stringKey("cloudfoundry.app.name"); + private static final AttributeKey CLOUDFOUNDRY_ORG_ID = + AttributeKey.stringKey("cloudfoundry.org.id"); + private static final AttributeKey CLOUDFOUNDRY_ORG_NAME = + AttributeKey.stringKey("cloudfoundry.org.name"); + private static final AttributeKey CLOUDFOUNDRY_PROCESS_ID = + AttributeKey.stringKey("cloudfoundry.process.id"); + private static final AttributeKey CLOUDFOUNDRY_PROCESS_TYPE = + AttributeKey.stringKey("cloudfoundry.process.type"); + private static final AttributeKey CLOUDFOUNDRY_SPACE_ID = + AttributeKey.stringKey("cloudfoundry.space.id"); + private static final AttributeKey CLOUDFOUNDRY_SPACE_NAME = + AttributeKey.stringKey("cloudfoundry.space.name"); + private static final Resource INSTANCE = buildResource(System::getenv); + + private CloudFoundryResource() {} + + public static Resource get() { + return INSTANCE; + } + + static Resource buildResource(Function getenv) { + String vcapAppRaw = getenv.apply(ENV_VCAP_APPLICATION); + // If there is no VCAP_APPLICATION in the environment, we are likely not running in CloudFoundry + if (vcapAppRaw == null || vcapAppRaw.isEmpty()) { + return Resource.empty(); + } + CloudFoundryAttributesBuilder attributes = new CloudFoundryAttributesBuilder(vcapAppRaw); + attributes + .putString(CLOUDFOUNDRY_APP_ID, "application_id") + .putString(CLOUDFOUNDRY_APP_NAME, "application_name") + .putNumberAsString(CLOUDFOUNDRY_APP_INSTANCE_ID, "instance_index") + .putString(CLOUDFOUNDRY_ORG_ID, "organization_id") + .putString(CLOUDFOUNDRY_ORG_NAME, "organization_name") + .putString(CLOUDFOUNDRY_PROCESS_ID, "process_id") + .putString(CLOUDFOUNDRY_PROCESS_TYPE, "process_type") + .putString(CLOUDFOUNDRY_SPACE_ID, "space_id") + .putString(CLOUDFOUNDRY_SPACE_NAME, "space_name"); + return Resource.create(attributes.build(), SchemaUrls.V1_24_0); + } + + private static class CloudFoundryAttributesBuilder { + private final AttributesBuilder builder = Attributes.builder(); + private Map parsedData; + + @SuppressWarnings("unchecked") + private CloudFoundryAttributesBuilder(String rawData) { + Load load = new Load(LoadSettings.builder().build()); + try { + this.parsedData = (Map) load.loadFromString(rawData); + } catch (ClassCastException ex) { + this.parsedData = Collections.emptyMap(); + } + } + + @CanIgnoreReturnValue + private CloudFoundryAttributesBuilder putString(AttributeKey key, String name) { + Object value = parsedData.get(name); + if (value instanceof String) { + builder.put(key, (String) value); + } + return this; + } + + @CanIgnoreReturnValue + private CloudFoundryAttributesBuilder putNumberAsString(AttributeKey key, String name) { + Object value = parsedData.get(name); + if (value instanceof Number) { + builder.put(key, value.toString()); + } + return this; + } + + private Attributes build() { + return builder.build(); + } + } +} diff --git a/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResourceProvider.java b/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResourceProvider.java new file mode 100644 index 000000000000..70475259f8da --- /dev/null +++ b/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResourceProvider.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cloudfoundry.resources; + +import com.google.auto.service.AutoService; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +@AutoService(ResourceProvider.class) +public class CloudFoundryResourceProvider implements ResourceProvider { + + @Override + public Resource createResource(ConfigProperties configProperties) { + return CloudFoundryResource.get(); + } +} diff --git a/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/test/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResourceTest.java b/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/test/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResourceTest.java new file mode 100644 index 000000000000..25c1a96c76a2 --- /dev/null +++ b/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/test/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResourceTest.java @@ -0,0 +1,72 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.cloudfoundry.resources; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.SchemaUrls; +import io.opentelemetry.semconv.incubating.CloudfoundryIncubatingAttributes; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class CloudFoundryResourceTest { + + private static final String FULL_VCAP_APPLICATION = + "{" + + "\"application_id\":\"0193a038-e615-7e5e-92ca-f4bcd7ba0a25\"," + + "\"application_name\":\"cf-app-name\"," + + "\"instance_index\":1," + + "\"organization_id\":\"0193a375-8d8e-7e0c-a832-01ce9ded40dc\"," + + "\"organization_name\":\"cf-org-name\"," + + "\"process_id\":\"0193a4e3-8fd3-71b9-9fe3-5640c53bf1e2\"," + + "\"process_type\":\"web\"," + + "\"space_id\":\"0193a7e7-da17-7ea4-8940-b1e07b401b16\"," + + "\"space_name\":\"cf-space-name\"}"; + + @Test + void noVcapApplication() { + Map env = Collections.emptyMap(); + Resource resource = CloudFoundryResource.buildResource(env::get); + assertThat(resource).isEqualTo(Resource.empty()); + } + + @Test + void emptyVcapApplication() { + Map env = ImmutableMap.of("VCAP_APPLICATION", ""); + Resource resource = CloudFoundryResource.buildResource(env::get); + assertThat(resource).isEqualTo(Resource.empty()); + } + + @Test + void fullVcapApplication() { + Map env = ImmutableMap.of("VCAP_APPLICATION", FULL_VCAP_APPLICATION); + + Resource resource = CloudFoundryResource.buildResource(env::get); + + assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0); + assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_APP_ID)) + .isEqualTo("0193a038-e615-7e5e-92ca-f4bcd7ba0a25"); + assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_APP_INSTANCE_ID)) + .isEqualTo("1"); + assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_APP_NAME)) + .isEqualTo("cf-app-name"); + assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_ORG_ID)) + .isEqualTo("0193a375-8d8e-7e0c-a832-01ce9ded40dc"); + assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_ORG_NAME)) + .isEqualTo("cf-org-name"); + assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_PROCESS_ID)) + .isEqualTo("0193a4e3-8fd3-71b9-9fe3-5640c53bf1e2"); + assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_PROCESS_TYPE)) + .isEqualTo("web"); + assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_SPACE_ID)) + .isEqualTo("0193a7e7-da17-7ea4-8940-b1e07b401b16"); + assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_SPACE_NAME)) + .isEqualTo("cf-space-name"); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 67ff589c2439..4956792e67c7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -231,6 +231,7 @@ include(":instrumentation:cassandra:cassandra-4.4:testing") include(":instrumentation:cassandra:cassandra-4-common:testing") include(":instrumentation:cdi-testing") include(":instrumentation:clickhouse-client-0.5:javaagent") +include(":instrumentation:cloudfoundry:cloudfoundry-resources:library") include(":instrumentation:couchbase:couchbase-2.0:javaagent") include(":instrumentation:couchbase:couchbase-2.6:javaagent") include(":instrumentation:couchbase:couchbase-2-common:javaagent") From dfa1bde824ab2f60817d2340d37c884a2b75a8f0 Mon Sep 17 00:00:00 2001 From: Karsten Schnitter Date: Sat, 14 Dec 2024 07:51:45 +0100 Subject: [PATCH 2/2] Update instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResource.java Co-authored-by: jason plumb <75337021+breedx-splk@users.noreply.github.com> --- .../cloudfoundry/resources/CloudFoundryResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResource.java b/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResource.java index 8e1939c54015..de2f63513f44 100644 --- a/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResource.java +++ b/instrumentation/cloudfoundry/cloudfoundry-resources/library/src/main/java/io/opentelemetry/javaagent/instrumentation/cloudfoundry/resources/CloudFoundryResource.java @@ -70,7 +70,7 @@ static Resource buildResource(Function getenv) { private static class CloudFoundryAttributesBuilder { private final AttributesBuilder builder = Attributes.builder(); - private Map parsedData; + private final Map parsedData; @SuppressWarnings("unchecked") private CloudFoundryAttributesBuilder(String rawData) {