Skip to content

Commit 2858a2c

Browse files
Add CloudFoundry Resource Provider (#1613)
Signed-off-by: Karsten Schnitter <[email protected]> Co-authored-by: Trask Stalnaker <[email protected]>
1 parent 539e0ef commit 2858a2c

File tree

11 files changed

+292
-1
lines changed

11 files changed

+292
-1
lines changed

.github/component_owners.yml

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ components:
2525
baggage-processor:
2626
- mikegoldsmith
2727
- zeitlinger
28+
cloudfoundry-resources:
29+
- KarstenSchnitter
2830
compressors:
2931
- jack-berg
3032
consistent-sampling:

.github/scripts/draft-change-log-entries.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ component_names["aws-xray/"]="AWS X-Ray SDK support"
2929
component_names["aws-xray-propagator/"]="AWS X-Ray propagator"
3030
component_names["azure-resources/"]="Azure resources"
3131
component_names["baggage-processor/"]="Baggage processor"
32+
component_names["cloudfoundry-resources/"]="CloudFoundry resources"
3233
component_names["compressors/"]="Compressors"
3334
component_names["consistent-sampling/"]="Consistent sampling"
3435
component_names["disk-buffering/"]="Disk buffering"
@@ -49,7 +50,6 @@ component_names["runtime-attach/"]="Runtime attach"
4950
component_names["resource-providers/"]="Resource providers"
5051
component_names["samplers/"]="Samplers"
5152
component_names["span-stacktrace/"]="Span stack traces"
52-
component_names["static-instrumenter/"]="Static instrumenter"
5353

5454
echo "## Unreleased"
5555
echo

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
- Support Lineage in XRay trace header and remove additional baggage from being added
1515
([#1671](https://github.com/open-telemetry/opentelemetry-java-contrib/pull/1671))
1616

17+
### CloudFoundry resources - New 🌟
18+
19+
CloudFoundry resource detector.
20+
1721
### Disk buffering
1822

1923
- Use delegate's temporality

cloudfoundry-resources/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# OpenTelemetry CloudFoundry Resource Support
2+
3+
This module contains CloudFoundry resource detectors for OpenTelemetry.
4+
5+
The module detects environment variable `VCAP_APPLICATION`, which is present for applications deployed in CloudFoundry.
6+
This variable contains a JSON structure, which is parsed to fill the following attributes.
7+
8+
| Resource attribute | `VCAP_APPLICATION` field |
9+
|------------------------------|--------------------------|
10+
| cloudfoundry.app.id | application_id |
11+
| cloudfoundry.app.name | application_name |
12+
| cloudfoundry.app.instance.id | instance_index |
13+
| cloudfoundry.org.id | organization_id |
14+
| cloudfoundry.org.name | organization_name |
15+
| cloudfoundry.process.id | process_id |
16+
| cloudfoundry.process.type | process_type |
17+
| cloudfoundry.space.id | space_id |
18+
| cloudfoundry.space.name | space_name |
19+
20+
The resource attributes follow the [CloudFoundry semantic convention.](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/cloudfoundry.md).
21+
A description of `VCAP_APPLICATION` is available in the [CloudFoundry documentation](https://docs.cloudfoundry.org/devguide/deploy-apps/environment-variable.html#VCAP-APPLICATION).
22+
23+
## Component owners
24+
25+
- [Karsten Schnitter](https://github.com/KarstenSchnitter), SAP
26+
27+
Learn more about component owners in [component_owners.yml](../.github/component_owners.yml).
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
plugins {
2+
id("otel.java-conventions")
3+
4+
id("otel.publish-conventions")
5+
}
6+
7+
description = "OpenTelemetry CloudFoundry Resources"
8+
otelJava.moduleName.set("io.opentelemetry.contrib.cloudfoundry.resources")
9+
10+
dependencies {
11+
api("io.opentelemetry:opentelemetry-api")
12+
api("io.opentelemetry:opentelemetry-sdk")
13+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
14+
15+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
16+
17+
implementation("com.fasterxml.jackson.core:jackson-core")
18+
implementation("io.opentelemetry.semconv:opentelemetry-semconv")
19+
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
20+
21+
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
22+
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
23+
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.cloudfoundry.resources;
7+
8+
import com.fasterxml.jackson.core.JsonFactory;
9+
import com.fasterxml.jackson.core.JsonParser;
10+
import com.fasterxml.jackson.core.JsonToken;
11+
import io.opentelemetry.api.common.AttributeKey;
12+
import io.opentelemetry.api.common.Attributes;
13+
import io.opentelemetry.api.common.AttributesBuilder;
14+
import io.opentelemetry.sdk.resources.Resource;
15+
import io.opentelemetry.semconv.SchemaUrls;
16+
import java.io.IOException;
17+
import java.util.function.Function;
18+
import java.util.logging.Logger;
19+
20+
public final class CloudFoundryResource {
21+
22+
private static final String ENV_VCAP_APPLICATION = "VCAP_APPLICATION";
23+
24+
// copied from CloudfoundryIncubatingAttributes
25+
private static final AttributeKey<String> CLOUDFOUNDRY_APP_ID =
26+
AttributeKey.stringKey("cloudfoundry.app.id");
27+
private static final AttributeKey<String> CLOUDFOUNDRY_APP_INSTANCE_ID =
28+
AttributeKey.stringKey("cloudfoundry.app.instance.id");
29+
private static final AttributeKey<String> CLOUDFOUNDRY_APP_NAME =
30+
AttributeKey.stringKey("cloudfoundry.app.name");
31+
private static final AttributeKey<String> CLOUDFOUNDRY_ORG_ID =
32+
AttributeKey.stringKey("cloudfoundry.org.id");
33+
private static final AttributeKey<String> CLOUDFOUNDRY_ORG_NAME =
34+
AttributeKey.stringKey("cloudfoundry.org.name");
35+
private static final AttributeKey<String> CLOUDFOUNDRY_PROCESS_ID =
36+
AttributeKey.stringKey("cloudfoundry.process.id");
37+
private static final AttributeKey<String> CLOUDFOUNDRY_PROCESS_TYPE =
38+
AttributeKey.stringKey("cloudfoundry.process.type");
39+
private static final AttributeKey<String> CLOUDFOUNDRY_SPACE_ID =
40+
AttributeKey.stringKey("cloudfoundry.space.id");
41+
private static final AttributeKey<String> CLOUDFOUNDRY_SPACE_NAME =
42+
AttributeKey.stringKey("cloudfoundry.space.name");
43+
private static final Logger LOG = Logger.getLogger(CloudFoundryResource.class.getName());
44+
private static final JsonFactory JSON_FACTORY = new JsonFactory();
45+
private static final Resource INSTANCE = buildResource(System::getenv);
46+
47+
private CloudFoundryResource() {}
48+
49+
public static Resource get() {
50+
return INSTANCE;
51+
}
52+
53+
static Resource buildResource(Function<String, String> getenv) {
54+
String vcapAppRaw = getenv.apply(ENV_VCAP_APPLICATION);
55+
// If there is no VCAP_APPLICATION in the environment, we are likely not running in CloudFoundry
56+
if (vcapAppRaw == null || vcapAppRaw.isEmpty()) {
57+
return Resource.empty();
58+
}
59+
60+
AttributesBuilder builder = Attributes.builder();
61+
try (JsonParser parser = JSON_FACTORY.createParser(vcapAppRaw)) {
62+
parser.nextToken();
63+
while (parser.nextToken() != JsonToken.END_OBJECT) {
64+
String name = parser.currentName();
65+
parser.nextToken();
66+
String value = parser.getValueAsString();
67+
switch (name) {
68+
case "application_id":
69+
builder.put(CLOUDFOUNDRY_APP_ID, value);
70+
break;
71+
case "application_name":
72+
builder.put(CLOUDFOUNDRY_APP_NAME, value);
73+
break;
74+
case "instance_index":
75+
builder.put(CLOUDFOUNDRY_APP_INSTANCE_ID, value);
76+
break;
77+
case "organization_id":
78+
builder.put(CLOUDFOUNDRY_ORG_ID, value);
79+
break;
80+
case "organization_name":
81+
builder.put(CLOUDFOUNDRY_ORG_NAME, value);
82+
break;
83+
case "process_id":
84+
builder.put(CLOUDFOUNDRY_PROCESS_ID, value);
85+
break;
86+
case "process_type":
87+
builder.put(CLOUDFOUNDRY_PROCESS_TYPE, value);
88+
break;
89+
case "space_id":
90+
builder.put(CLOUDFOUNDRY_SPACE_ID, value);
91+
break;
92+
case "space_name":
93+
builder.put(CLOUDFOUNDRY_SPACE_NAME, value);
94+
break;
95+
default:
96+
parser.skipChildren();
97+
break;
98+
}
99+
}
100+
} catch (IOException e) {
101+
LOG.warning("Cannot parse contents of environment variable VCAP_APPLICATION. Invalid JSON");
102+
}
103+
104+
return Resource.create(builder.build(), SchemaUrls.V1_24_0);
105+
}
106+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.cloudfoundry.resources;
7+
8+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
9+
import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider;
10+
import io.opentelemetry.sdk.resources.Resource;
11+
12+
public class CloudFoundryResourceProvider implements ResourceProvider {
13+
14+
@Override
15+
public Resource createResource(ConfigProperties configProperties) {
16+
return CloudFoundryResource.get();
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.opentelemetry.contrib.cloudfoundry.resources.CloudFoundryResourceProvider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.cloudfoundry.resources;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import io.opentelemetry.sdk.resources.Resource;
11+
import io.opentelemetry.semconv.SchemaUrls;
12+
import io.opentelemetry.semconv.incubating.CloudfoundryIncubatingAttributes;
13+
import java.io.BufferedReader;
14+
import java.io.IOException;
15+
import java.io.InputStream;
16+
import java.io.InputStreamReader;
17+
import java.nio.charset.StandardCharsets;
18+
import java.util.Collections;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.stream.Collectors;
22+
import org.assertj.core.api.Assertions;
23+
import org.junit.jupiter.api.Test;
24+
25+
class CloudFoundryResourceTest {
26+
27+
private static Map<String, String> createVcapApplicationEnv(String value) {
28+
Map<String, String> environment = new HashMap<>();
29+
environment.put("VCAP_APPLICATION", value);
30+
return environment;
31+
}
32+
33+
private static String loadVcapApplicationSample(String filename) {
34+
try (InputStream is =
35+
CloudFoundryResourceTest.class.getClassLoader().getResourceAsStream(filename)) {
36+
if (is != null) {
37+
return new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))
38+
.lines()
39+
.collect(Collectors.joining());
40+
}
41+
Assertions.fail("Cannot load resource " + filename);
42+
} catch (IOException e) {
43+
Assertions.fail("Error reading " + filename);
44+
}
45+
return "";
46+
}
47+
48+
@Test
49+
void noVcapApplication() {
50+
Map<String, String> env = Collections.emptyMap();
51+
Resource resource = CloudFoundryResource.buildResource(env::get);
52+
assertThat(resource).isEqualTo(Resource.empty());
53+
}
54+
55+
@Test
56+
void emptyVcapApplication() {
57+
Map<String, String> env = createVcapApplicationEnv("");
58+
Resource resource = CloudFoundryResource.buildResource(env::get);
59+
assertThat(resource).isEqualTo(Resource.empty());
60+
}
61+
62+
@Test
63+
void fullVcapApplication() {
64+
String json = loadVcapApplicationSample("vcap_application.json");
65+
Map<String, String> env = createVcapApplicationEnv(json);
66+
67+
Resource resource = CloudFoundryResource.buildResource(env::get);
68+
69+
assertThat(resource.getSchemaUrl()).isEqualTo(SchemaUrls.V1_24_0);
70+
assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_APP_ID))
71+
.isEqualTo("0193a038-e615-7e5e-92ca-f4bcd7ba0a25");
72+
assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_APP_INSTANCE_ID))
73+
.isEqualTo("1");
74+
assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_APP_NAME))
75+
.isEqualTo("cf-app-name");
76+
assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_ORG_ID))
77+
.isEqualTo("0193a375-8d8e-7e0c-a832-01ce9ded40dc");
78+
assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_ORG_NAME))
79+
.isEqualTo("cf-org-name");
80+
assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_PROCESS_ID))
81+
.isEqualTo("0193a4e3-8fd3-71b9-9fe3-5640c53bf1e2");
82+
assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_PROCESS_TYPE))
83+
.isEqualTo("web");
84+
assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_SPACE_ID))
85+
.isEqualTo("0193a7e7-da17-7ea4-8940-b1e07b401b16");
86+
assertThat(resource.getAttribute(CloudfoundryIncubatingAttributes.CLOUDFOUNDRY_SPACE_NAME))
87+
.isEqualTo("cf-space-name");
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"application_id": "0193a038-e615-7e5e-92ca-f4bcd7ba0a25",
3+
"application_name": "cf-app-name",
4+
"application_uris": [
5+
"testapp.example.com"
6+
],
7+
"cf_api": "https://api.cf.example.com",
8+
"limits": {
9+
"fds": 256
10+
},
11+
"instance_index": 1,
12+
"organization_id": "0193a375-8d8e-7e0c-a832-01ce9ded40dc",
13+
"organization_name": "cf-org-name",
14+
"process_id": "0193a4e3-8fd3-71b9-9fe3-5640c53bf1e2",
15+
"process_type": "web",
16+
"space_id": "0193a7e7-da17-7ea4-8940-b1e07b401b16",
17+
"space_name": "cf-space-name",
18+
"users": null
19+
}

settings.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ include(":aws-xray-propagator")
4141
include(":azure-resources")
4242
include(":baggage-processor")
4343
include(":compressors:compressor-zstd")
44+
include(":cloudfoundry-resources")
4445
include(":consistent-sampling")
4546
include(":dependencyManagement")
4647
include(":disk-buffering")

0 commit comments

Comments
 (0)