Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ components:
baggage-procesor:
- mikegoldsmith
- zeitlinger
cloudfoundry-resources:
- KarstenSchnitter
compressors:
- jack-berg
consistent-sampling:
Expand Down
27 changes: 27 additions & 0 deletions cloudfoundry-resources/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# OpenTelemetry CloudFoundry Resource Support

This module contains CloudFoundry resource detectors for OpenTelemetry.

The module detects environment variable `VCAP_APPLICATION`, which is present for applications deployed in CloudFoundry.
This variable contains a JSON structure, which is parsed to fill the following attributes.

| Resource attribute | `VCAP_APPLICATION` field |
|------------------------------|--------------------------|
| cloudfoundry.app.id | application_id |
| cloudfoundry.app.name | application_name |
| cloudfoundry.app.instance.id | instance_index |
| cloudfoundry.org.id | organization_id |
| cloudfoundry.org.name | organization_name |
| cloudfoundry.process.id | process_id |
| cloudfoundry.process.type | process_type |
| cloudfoundry.space.id | space_id |
| cloudfoundry.space.name | space_name |

The resource attributes follow the [CloudFoundry semantic convention.](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/cloudfoundry.md).
A description of `VCAP_APPLICATION` is available in the [CloudFoundry documentation](https://docs.cloudfoundry.org/devguide/deploy-apps/environment-variable.html#VCAP-APPLICATION).

## Component owners

- [Karsten Schnitter](https://github.com/KarstenSchnitter), SAP

Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).
24 changes: 24 additions & 0 deletions cloudfoundry-resources/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
id("otel.java-conventions")

id("otel.publish-conventions")
}

description = "OpenTelemetry CloudFoundry Resources"
otelJava.moduleName.set("io.opentelemetry.contrib.cloudfoundry.resources")

dependencies {
api("io.opentelemetry:opentelemetry-api")
api("io.opentelemetry:opentelemetry-sdk")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")

compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")

implementation("com.fasterxml.jackson.core:jackson-core")
implementation("io.opentelemetry.semconv:opentelemetry-semconv")
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")

testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.cloudfoundry.resources;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
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.io.IOException;
import java.util.function.Function;
import java.util.logging.Logger;

public final class CloudFoundryResource {

private static final String ENV_VCAP_APPLICATION = "VCAP_APPLICATION";

// copied from CloudfoundryIncubatingAttributes
private static final AttributeKey<String> CLOUDFOUNDRY_APP_ID =
AttributeKey.stringKey("cloudfoundry.app.id");
private static final AttributeKey<String> CLOUDFOUNDRY_APP_INSTANCE_ID =
AttributeKey.stringKey("cloudfoundry.app.instance.id");
private static final AttributeKey<String> CLOUDFOUNDRY_APP_NAME =
AttributeKey.stringKey("cloudfoundry.app.name");
private static final AttributeKey<String> CLOUDFOUNDRY_ORG_ID =
AttributeKey.stringKey("cloudfoundry.org.id");
private static final AttributeKey<String> CLOUDFOUNDRY_ORG_NAME =
AttributeKey.stringKey("cloudfoundry.org.name");
private static final AttributeKey<String> CLOUDFOUNDRY_PROCESS_ID =
AttributeKey.stringKey("cloudfoundry.process.id");
private static final AttributeKey<String> CLOUDFOUNDRY_PROCESS_TYPE =
AttributeKey.stringKey("cloudfoundry.process.type");
private static final AttributeKey<String> CLOUDFOUNDRY_SPACE_ID =
AttributeKey.stringKey("cloudfoundry.space.id");
private static final AttributeKey<String> CLOUDFOUNDRY_SPACE_NAME =
AttributeKey.stringKey("cloudfoundry.space.name");
private static final Logger LOG = Logger.getLogger(CloudFoundryResource.class.getName());
private static final JsonFactory JSON_FACTORY = new JsonFactory();
private static final Resource INSTANCE = buildResource(System::getenv);

private CloudFoundryResource() {}

public static Resource get() {
return INSTANCE;
}

static Resource buildResource(Function<String, String> 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();
}

AttributesBuilder builder = Attributes.builder();
try (JsonParser parser = JSON_FACTORY.createParser(vcapAppRaw)) {
parser.nextToken();
while (parser.nextToken() != JsonToken.END_OBJECT) {
String name = parser.currentName();
parser.nextToken();
String value = parser.getValueAsString();
switch (name) {
case "application_id":
builder.put(CLOUDFOUNDRY_APP_ID, value);
break;
case "application_name":
builder.put(CLOUDFOUNDRY_APP_NAME, value);
break;
case "instance_index":
builder.put(CLOUDFOUNDRY_APP_INSTANCE_ID, value);
break;
case "organization_id":
builder.put(CLOUDFOUNDRY_ORG_ID, value);
break;
case "organization_name":
builder.put(CLOUDFOUNDRY_ORG_NAME, value);
break;
case "process_id":
builder.put(CLOUDFOUNDRY_PROCESS_ID, value);
break;
case "process_type":
builder.put(CLOUDFOUNDRY_PROCESS_TYPE, value);
break;
case "space_id":
builder.put(CLOUDFOUNDRY_SPACE_ID, value);
break;
case "space_name":
builder.put(CLOUDFOUNDRY_SPACE_NAME, value);
break;
default:
parser.skipChildren();
break;
}
}
} catch (IOException e) {
LOG.warning("Cannot parse contents of environment variable VCAP_APPLICATION. Invalid JSON");
}

return Resource.create(builder.build(), SchemaUrls.V1_24_0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.cloudfoundry.resources;

import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
import io.opentelemetry.sdk.resources.Resource;

public class CloudFoundryResourceProvider implements ResourceProvider {

@Override
public Resource createResource(ConfigProperties configProperties) {
return CloudFoundryResource.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.opentelemetry.contrib.cloudfoundry.resources.CloudFoundryResourceProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.contrib.cloudfoundry.resources;

import static org.assertj.core.api.Assertions.assertThat;

import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.semconv.SchemaUrls;
import io.opentelemetry.semconv.incubating.CloudfoundryIncubatingAttributes;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

class CloudFoundryResourceTest {

private static Map<String, String> createVcapApplicationEnv(String value) {
Map<String, String> environment = new HashMap<>();
environment.put("VCAP_APPLICATION", value);
return environment;
}

private static String loadVcapApplicationSample(String filename) {
try (InputStream is =
CloudFoundryResourceTest.class.getClassLoader().getResourceAsStream(filename)) {
if (is != null) {
return new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining());
}
Assertions.fail("Cannot load resource " + filename);
} catch (IOException e) {
Assertions.fail("Error reading " + filename);
}
return "";
}

@Test
void noVcapApplication() {
Map<String, String> env = Collections.emptyMap();
Resource resource = CloudFoundryResource.buildResource(env::get);
assertThat(resource).isEqualTo(Resource.empty());
}

@Test
void emptyVcapApplication() {
Map<String, String> env = createVcapApplicationEnv("");
Resource resource = CloudFoundryResource.buildResource(env::get);
assertThat(resource).isEqualTo(Resource.empty());
}

@Test
void fullVcapApplication() {
String json = loadVcapApplicationSample("vcap_application.json");
Map<String, String> env = createVcapApplicationEnv(json);

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");
}
}
19 changes: 19 additions & 0 deletions cloudfoundry-resources/src/test/resources/vcap_application.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"application_id": "0193a038-e615-7e5e-92ca-f4bcd7ba0a25",
"application_name": "cf-app-name",
"application_uris": [
"testapp.example.com"
],
"cf_api": "https://api.cf.example.com",
"limits": {
"fds": 256
},
"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",
"users": null
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ include(":aws-xray")
include(":aws-xray-propagator")
include(":baggage-processor")
include(":compressors:compressor-zstd")
include(":cloudfoundry-resources")
include(":consistent-sampling")
include(":dependencyManagement")
include(":disk-buffering")
Expand Down