Skip to content

Commit 3784873

Browse files
Introduce Cloud Logging Exporter (#169)
Add OpenTelemetry Signal Exporters for Cloud Logging Register signal exporters for logs, metrics and traces, that forward signals to SAP Cloud Logging, if an appropriate service binding is found or do a non operation otherwise. This allows developers to explicitly configure the export, e.g. by `otel.logs.exporter=cloud-logging`. Additional exporters can be added with comma-separated labels. Signed-off-by: Karsten Schnitter <[email protected]>
1 parent 47abfd7 commit 3784873

30 files changed

+1489
-59
lines changed

cf-java-logging-support-opentelemetry-agent-extension/README.md

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# OpenTelemetry Java Agent Extension for SAP Cloud Logging
22

33
This module provides an extension for the [OpenTelemetry Java Agent](https://opentelemetry.io/docs/instrumentation/java/automatic/).
4-
The extension scans the service bindings of an application for SAP Cloud Logging.
4+
The extension scans the service bindings of an application for [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging).
55
If such a binding is found, the OpenTelemetry Java Agent is configured to ship observability data to that service.
66
Thus, this extension provides a convenient auto-instrumentation for Java applications running on SAP BTP.
77

8-
The extension provides two main features:
8+
The extension provides the following main features:
99

10-
* auto-configuration of the OpenTelemetry connection to SAP Cloud Logging
11-
* adding resource attributes to describe the CF application
10+
* additional exporters for logs, metrics and traces for [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging)
11+
* auto-configuration of the generic OpenTelemetry connection to [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging)
12+
* adding resource attributes describing the CF application
1213

1314
See the section on [configuration](#configuration) for further details.
1415

@@ -18,7 +19,7 @@ Any Java application can be instrumented with the OpenTelemetry Java Agent and t
1819

1920
```sh
2021
java -javaagent:/path/to/opentelemetry-javaagent-<version>.jar \
21-
-Dotel.javaagent-extensions=/path/to/cf-java-logging-support-opentelemetry-agent-extension-<versions>.jar \
22+
-Dotel.javaagent-extensions=/path/to/cf-java-logging-support-opentelemetry-agent-extension-<version>.jar \
2223
# your Java application command
2324
```
2425

@@ -37,27 +38,76 @@ java -javaagent:BOOT-INF/lib/opentelemetry-javaagent-<version>.jar \
3738
3839
See the [example manifest](../sample-spring-boot/manifest-otel-javaagent.yml), how this translates into a deployment description.
3940

40-
For the instrumentation to send observability data to SAP Cloud Logging, the application needs to be bound to a corresponding service instance.
41-
The service instance can be either managed or [user-provided](#using-user-provided-service-instances).
41+
Once the agent is attached to the JVM with the ectension in place, there are two ways, which can be used to send data to [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging):
42+
43+
1. Use the `cloud-logging` exporters explicitly as provided by the extension.
44+
This can be achieved via system properties or environment variables:
45+
```sh
46+
-Dotel.logs.exporter=cloud-logging \
47+
-Dotel.metrics.exporter=cloud-logging \
48+
-Dotel.traces.exporter=cloud-logging
49+
50+
#or
51+
52+
export OTEL_LOGS_EXPORTER=cloud-logging
53+
export OTEL_METRICS_EXPORTER=cloud-logging
54+
export OTEL_TRACES_EXPORTER=cloud-logging
55+
java #...
56+
```
57+
58+
2. Use the default `otlp` exporter with the provided default configuration from the extension:
59+
60+
```sh
61+
-Dotel.logs.exporter=otlp \
62+
-Dotel.metrics.exporter=otlp # default value \
63+
-Dotel.traces.exporter=otlp # default value
64+
65+
#or
4266

43-
Note, that the OpenTelemetry Java Agent currently only sends traces and metrics by default.
44-
To enable logs, the additional property `-Dotel.logs.exporter=otlp` is required.
67+
export OTEL_LOGS_EXPORTER=otlp
68+
export OTEL_METRICS_EXPORTER=otlp # default value
69+
export OTEL_TRACES_EXPORTER=otlp # default value
70+
java #...
71+
```
72+
73+
Note, that the OpenTelemetry Java Agent currently sends traces and metrics by default using the `otlp` exporter.
74+
That means, without any configuration the agent with the extension will forward metrics and traces to [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging).
75+
See TODO for the difference between `cloud-logging` and `otlp` exporters.
76+
The benefit of the `cloud-logging` exporter is, that it can be combined with a different configuration of the `otlp` exporter.
77+
78+
For the instrumentation to send observability data to [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging), the application needs to be bound to a corresponding service instance.
79+
The service instance can be either managed or [user-provided](#using-user-provided-service-instances).
4580

4681
## Configuration
4782

4883
The OpenTelemetry Java Agent supports a wide variety of [configuration options](https://opentelemetry.io/docs/instrumentation/java/automatic/agent-config/).
4984
As the extension provides configuration via SPI, all its configuration takes lower precedence than other configuration options for OpenTelemetry.
5085
Users can easily overwrite any setting using environment variables or system properties.
5186

87+
### Using the Extension
88+
89+
The extension needs to be started with the OpenTelemetry Java Agent as outlined in the [Quick Start Guide](#quickstart-guide).
90+
You need to enable shipping data either by using the `cloud-logging` exporters or relying on the `otlp` exporters for each signal type.
91+
Multiple different exporters can be configured with comma separation.
92+
Using the custom `cloud-logging` exporter will enable you, to use the default `otlp` exporter for different services.
93+
The extension will configure a default endpoint and credentials for the `otlp` endpoints, so no further configuration is required.
94+
95+
Note, that the `cloud-logging` exporter is just a facade for the `otlp` exporter to allow configuration of multiple data sinks.
96+
There is no custom network client provided by this extension.
97+
5298
### Configuring the Extension
5399

54100
The extension itself can be configured by specifying the following system properties:
55101

56102
| Property | Default Value | Comment |
57103
|----------|---------------|---------|
58-
| `com.sap.otel.extension.cloud-logging.label` | `cloud-logging` | The label of the managed service binding to bind to. |
59-
| `com.sap.otel.extension.cloud-logging.tag` | `Cloud Logging` | The tag of any service binding (managed or user-provided) to bind to. |
60-
| `otel.javaagent.extension.sap.cf.resource.enabled` or `env(OTEL_JAVAAGENT_EXTENSION_SAP_CF_RESOURCE_ENABLED)` | `true` | Whether to add CF resource attributes to all events. |
104+
| `otel.javaagent.extension.sap.cf.binding.cloud-logging.label` or `com.sap.otel.extension.cloud-logging.label` | `cloud-logging` | The label of the managed service binding to bind to. |
105+
| `otel.javaagent.extension.sap.cf.binding.cloud-logging.tag` or `com.sap.otel.extension.cloud-logging.tag` | `Cloud Logging` | The tag of any service binding (managed or user-provided) to bind to. |
106+
| `otel.javaagent.extension.sap.cf.binding.user-provided.label` | `user-provided` | The label of a user-provided service binding to bind to. Note, this label is defined by the Cloud Foundry instance. |
107+
| `otel.javaagent.extension.sap.cf.resource.enabled` | `true` | Whether to add CF resource attributes to all events. |
108+
109+
> The `otel.javaagent.extension.sap.*` properties are preferred over the `com.sap.otel.extension.*` properties, which are kept for compatibility.
110+
Each `otel.javaagent.extension.sap.*` property can also be provided as environment variable `OTEL_JAVAAGENT_EXTENSION_SAP_*`.
61111

62112
The extension will scan the environment variable `VCAP_SERVICES` for CF service bindings.
63113
User-provided bindings will take precedence over managed bindings of the configured label ("cloud-logging" by default).
@@ -90,7 +140,7 @@ The [OpenTelemetry Java Instrumentation project](https://github.com/open-telemet
90140

91141
## Using User-Provided Service Instances
92142

93-
The extension provides support not only for managed service instance of SAP Cloud Logging but also for user-provided service instances.
143+
The extension provides support not only for managed service instance of [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging) but also for user-provided service instances.
94144
This helps to fine-tune the configuration, e.g. leave out or reconfigure the syslog drain.
95145
Furthermore, this helps on sharing service instances across CF orgs or landscapes.
96146

@@ -103,7 +153,7 @@ The extension requires four fields in the user-provided service credentials and
103153
| `ingest-otlp-cert`| The mTLS client certificate in PEM format matching the client key. Line breaks as `\n`. |
104154
| `server-ca` | The trusted mTLS server certificate in PEM format. Line breaks as `\n`. |
105155

106-
If you have a SAP Cloud Logging service key, you can generate the required JSON file with jq:
156+
If you have a [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging) service key, you can generate the required JSON file with jq:
107157

108158
```bash
109159
cf service-key cls test \
@@ -119,5 +169,17 @@ Using this file, you can create the required user-provided service:
119169
```
120170

121171
Note, that you can easily feed arbitrary credentials to the extension.
122-
It does not need to be SAP Cloud Logging.
172+
It does not need to be [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging).
123173
You can even change the tag using the configuration parameters of the extension.
174+
175+
## Implementation Differences between Cloud-Logging and OTLP Exporter
176+
177+
The `cloud-logging` exporter provided by this extension is a facade for the `OtlpGrpcExporter` provided by the OpenTelemetry Java Agent, just like the `otlp` exporter.
178+
The difference is just during the bootstrapping phase.
179+
The main differences are:
180+
181+
* The `cloud-logging` exporter will send data to all found bindings to SAP Cloud Logging.
182+
The auto-instrumentation of the `otlp` exporter will just configure the first binding it finds priotizing user-provided services.
183+
* The `otlp` configuration will write the required certificates and keys to temporary files, which are deleted when the JVM is shut down. The `cloud-logging` exporter will keep the secrets in memory.
184+
* Since the `otlp` exporter is the default for traces and metrics, just attaching the extension and binding to Cloud Logging will result in metrics and traces being forwarded.
185+
The `cloud-logging` exporter needs to be configured explictly as does the `otlp` exporter for logs.

cf-java-logging-support-opentelemetry-agent-extension/dependency-reduced-pom.xml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
<excludes>
3333
<exclude>io.opentelemetry</exclude>
3434
<exclude>com.fasterxml.jackson.core</exclude>
35+
<exclude>com.squareup.okhttp3</exclude>
36+
<exclude>com.squareup.okio</exclude>
37+
<exclude>org.jetbrains.kotlin</exclude>
38+
<exclude>org.jetbrains</exclude>
3539
</excludes>
3640
</artifactSet>
3741
</configuration>
@@ -58,7 +62,13 @@
5862
<groupId>io.opentelemetry</groupId>
5963
<artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
6064
<version>1.31.0</version>
61-
<scope>provided</scope>
65+
<scope>compile</scope>
66+
</dependency>
67+
<dependency>
68+
<groupId>io.opentelemetry</groupId>
69+
<artifactId>opentelemetry-exporter-otlp</artifactId>
70+
<version>1.31.0</version>
71+
<scope>compile</scope>
6272
</dependency>
6373
<dependency>
6474
<groupId>org.slf4j</groupId>

cf-java-logging-support-opentelemetry-agent-extension/pom.xml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@
4949
<dependency>
5050
<groupId>io.opentelemetry</groupId>
5151
<artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
52-
<scope>provided</scope>
52+
</dependency>
53+
<dependency>
54+
<groupId>io.opentelemetry</groupId>
55+
<artifactId>opentelemetry-exporter-otlp</artifactId>
5356
</dependency>
5457
<dependency>
5558
<groupId>io.pivotal.cfenv</groupId>
@@ -83,6 +86,10 @@
8386
<excludes>
8487
<exclude>io.opentelemetry</exclude>
8588
<exclude>com.fasterxml.jackson.core</exclude>
89+
<exclude>com.squareup.okhttp3</exclude>
90+
<exclude>com.squareup.okio</exclude>
91+
<exclude>org.jetbrains.kotlin</exclude>
92+
<exclude>org.jetbrains</exclude>
8693
</excludes>
8794
</artifactSet>
8895
</configuration>

cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public class CloudLoggingConfigurationCustomizerProvider implements AutoConfigur
1313
public void customize(AutoConfigurationCustomizer autoConfiguration) {
1414
autoConfiguration
1515
.addPropertiesSupplier(new CloudLoggingBindingPropertiesSupplier(cfEnv));
16+
17+
// ConfigurableLogRecordExporterProvider
1618
}
1719

1820
}

cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/attributes/CloudFoundryResourceCustomizer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ public Resource apply(Resource resource, ConfigProperties configProperties) {
3434
CfApplication cfApp = cfEnv.getApp();
3535
ResourceBuilder rb = Resource.builder();
3636
rb.put("service.name", cfApp.getApplicationName());
37-
rb.put("sap.cf.source_id", getString(cfApp, "source_id"));
37+
rb.put("sap.cf.source_id", cfApp.getApplicationId());
3838
rb.put("sap.cf.instance_id", cfApp.getInstanceIndex());
3939
rb.put("sap.cf.app_id", cfApp.getApplicationId());
4040
rb.put("sap.cf.app_name", cfApp.getApplicationName());
4141
rb.put("sap.cf.space_id", cfApp.getSpaceId());
4242
rb.put("sap.cf.space_name", cfApp.getSpaceName());
4343
rb.put("sap.cf.org_id", getString(cfApp, "organization_id"));
4444
rb.put("sap.cf.org_name", getString(cfApp, "organization_name"));
45+
rb.put("sap.cf.process.id", getString(cfApp, "process_id"));
46+
rb.put("sap.cf.process.type", getString(cfApp, "process_type"));
4547
return rb.build();
4648
}
4749

cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingBindingPropertiesSupplier.java

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding;
22

3+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
4+
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
35
import io.pivotal.cfenv.core.CfEnv;
46
import io.pivotal.cfenv.core.CfService;
57

@@ -17,18 +19,34 @@
1719
public class CloudLoggingBindingPropertiesSupplier implements Supplier<Map<String, String>> {
1820

1921
private static final Logger LOG = Logger.getLogger(CloudLoggingBindingPropertiesSupplier.class.getName());
20-
private static final String CLOUD_LOGGING_LABEL = System.getProperty("com.sap.otel.extension.cloud-logging.label", "cloud-logging");
21-
private static final String CLOUD_LOGGING_TAG = System.getProperty("com.sap.otel.extension.cloud-logging.tag", "Cloud Logging");
22-
private static final String USER_PROVIDED_LABEL = "user-provided";
23-
public static final String OTLP_ENDPOINT = "ingest-otlp-endpoint";
24-
public static final String OTLP_CLIENT_KEY = "ingest-otlp-key";
25-
public static final String OTLP_CLIENT_CERT = "ingest-otlp-cert";
26-
public static final String OTLP_SERVER_CERT = "server-ca";
22+
private static final String OTLP_ENDPOINT = "ingest-otlp-endpoint";
23+
private static final String OTLP_CLIENT_KEY = "ingest-otlp-key";
24+
private static final String OTLP_CLIENT_CERT = "ingest-otlp-cert";
25+
private static final String OTLP_SERVER_CERT = "server-ca";
2726

28-
private final CfEnv cfEnv;
27+
private final CloudLoggingServicesProvider cloudLoggingServicesProvider;
2928

3029
public CloudLoggingBindingPropertiesSupplier(CfEnv cfEnv) {
31-
this.cfEnv = cfEnv;
30+
Map<String, String> defaults = new HashMap<>();
31+
defaults.put("com.sap.otel.extension.cloud-logging.label", "cloud-logging");
32+
defaults.put("com.sap.otel.extension.cloud-logging.tag", "Cloud Logging");
33+
defaults.put("otel.javaagent.extension.sap.cf.binding.user-provided.label", "user-provided");
34+
ConfigProperties configProperties = DefaultConfigProperties.create(defaults);
35+
this.cloudLoggingServicesProvider = new CloudLoggingServicesProvider(configProperties, cfEnv);
36+
}
37+
38+
private static boolean isBlank(String text) {
39+
return text == null || text.trim().isEmpty();
40+
}
41+
42+
private static File writeFile(String prefix, String suffix, String content) throws IOException {
43+
File file = File.createTempFile(prefix, suffix);
44+
file.deleteOnExit();
45+
try (FileWriter writer = new FileWriter(file)) {
46+
writer.append(content);
47+
LOG.fine("Created temporary file " + file.getAbsolutePath());
48+
}
49+
return file;
3250
}
3351

3452
/**
@@ -41,10 +59,7 @@ public CloudLoggingBindingPropertiesSupplier(CfEnv cfEnv) {
4159
*/
4260
@Override
4361
public Map<String, String> get() {
44-
Stream<CfService> userProvided = cfEnv.findServicesByLabel(USER_PROVIDED_LABEL).stream();
45-
Stream<CfService> managed = cfEnv.findServicesByLabel(CLOUD_LOGGING_LABEL).stream();
46-
return Stream.concat(userProvided, managed)
47-
.filter(svc -> svc.existsByTagIgnoreCase(CLOUD_LOGGING_TAG))
62+
return cloudLoggingServicesProvider.get()
4863
.findFirst()
4964
.map(this::createEndpointConfiguration).orElseGet(Collections::emptyMap);
5065
}
@@ -94,18 +109,4 @@ private Map<String, String> createEndpointConfiguration(CfService svc) {
94109
}
95110
}
96111

97-
private static boolean isBlank(String text) {
98-
return text == null || text.trim().isEmpty();
99-
}
100-
101-
private static File writeFile(String prefix, String suffix, String content) throws IOException {
102-
File file = File.createTempFile(prefix, suffix);
103-
file.deleteOnExit();
104-
try (FileWriter writer = new FileWriter(file)) {
105-
writer.append(content);
106-
LOG.fine("Created temporary file " + file.getAbsolutePath());
107-
}
108-
return file;
109-
}
110-
111112
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding;
2+
3+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
4+
import io.pivotal.cfenv.core.CfEnv;
5+
import io.pivotal.cfenv.core.CfService;
6+
7+
import java.util.List;
8+
import java.util.function.Supplier;
9+
import java.util.stream.Collectors;
10+
import java.util.stream.Stream;
11+
12+
public class CloudLoggingServicesProvider implements Supplier<Stream<CfService>> {
13+
14+
private static final String DEFAULT_USER_PROVIDED_LABEL = "user-provided";
15+
private static final String DEFAULT_CLOUD_LOGGING_LABEL = "cloud-logging";
16+
private static final String DEFAULT_CLOUD_LOGGING_TAG = "Cloud Logging";
17+
18+
private final List<CfService> services;
19+
20+
public CloudLoggingServicesProvider(ConfigProperties config, CfEnv cfEnv) {
21+
String userProvidedLabel = getUserProvidedLabel(config);
22+
String cloudLoggingLabel = getCloudLoggingLabel(config);
23+
String cloudLoggingTag = getCloudLoggingTag(config);
24+
List<CfService> userProvided = cfEnv.findServicesByLabel(userProvidedLabel);
25+
List<CfService> managed = cfEnv.findServicesByLabel(cloudLoggingLabel);
26+
this.services = Stream.concat(userProvided.stream(), managed.stream())
27+
.filter(svc -> svc.existsByTagIgnoreCase(cloudLoggingTag))
28+
.collect(Collectors.toList());
29+
}
30+
31+
private String getUserProvidedLabel(ConfigProperties config) {
32+
return config.getString("otel.javaagent.extension.sap.cf.binding.user-provided.label", DEFAULT_USER_PROVIDED_LABEL);
33+
}
34+
35+
private String getCloudLoggingLabel(ConfigProperties config) {
36+
String fromOwnProperties = System.getProperty("com.sap.otel.extension.cloud-logging.label", DEFAULT_CLOUD_LOGGING_LABEL);
37+
return config.getString("otel.javaagent.extension.sap.cf.binding.cloud-logging.label", fromOwnProperties);
38+
}
39+
40+
private String getCloudLoggingTag(ConfigProperties config) {
41+
String fromOwnProperties = System.getProperty("com.sap.otel.extension.cloud-logging.tag", DEFAULT_CLOUD_LOGGING_TAG);
42+
return config.getString("otel.javaagent.extension.sap.cf.binding.cloud-logging.tag", fromOwnProperties);
43+
}
44+
45+
@Override
46+
public Stream<CfService> get() {
47+
return services.stream();
48+
}
49+
}

0 commit comments

Comments
 (0)