Skip to content

Commit dcb11c1

Browse files
Provide Dynatrace Exporter (#170)
* Provide Dynatrace Exporter Adds a DynatraceMetricsExporter to the provided exporters. This exporter will parse CF bindings for a Dynatrace service binding. If found, it will export metrics to the API endpoint. For this to work, the API token field in the service credentials needs to be provided with `otel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name`. Signed-off-by: Karsten Schnitter <[email protected]>
1 parent 61b0b2b commit dcb11c1

17 files changed

+668
-128
lines changed

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

+45-14
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
# OpenTelemetry Java Agent Extension for SAP Cloud Logging
1+
# OpenTelemetry Java Agent Extension for SAP BTP Observability
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](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging).
5-
If such a binding is found, the OpenTelemetry Java Agent is configured to ship observability data to that service.
4+
The extension scans the service bindings of an application for [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging) and [Dynatrace](https://docs.dynatrace.com/docs/setup-and-configuration/setup-on-container-platforms/cloud-foundry/deploy-oneagent-on-sap-cloud-platform-for-application-only-monitoring).
5+
If such a binding is found, the OpenTelemetry Java Agent is configured to ship observability data to those services.
66
Thus, this extension provides a convenient auto-instrumentation for Java applications running on SAP BTP.
77

88
The extension provides the following main features:
99

1010
* additional exporters for logs, metrics and traces for [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging)
11+
* additional exporter for metrics for [Dynatrace](https://docs.dynatrace.com/docs/setup-and-configuration/setup-on-container-platforms/cloud-foundry/deploy-oneagent-on-sap-cloud-platform-for-application-only-monitoring)
1112
* auto-configuration of the generic OpenTelemetry connection to [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging)
1213
* adding resource attributes describing the CF application
1314

@@ -40,17 +41,17 @@ See the [example manifest](../sample-spring-boot/manifest-otel-javaagent.yml), h
4041

4142
Once the agent is attached to the JVM with the extension 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):
4243

43-
1. Use the `cloud-logging` exporters explicitly as provided by the extension.
44+
1. Use the `cloud-logging` and/or `dynatrace` exporters explicitly as provided by the extension.
4445
This can be achieved via system properties or environment variables:
4546
```sh
4647
-Dotel.logs.exporter=cloud-logging \
47-
-Dotel.metrics.exporter=cloud-logging \
48+
-Dotel.metrics.exporter=cloud-logging,dynatrace \
4849
-Dotel.traces.exporter=cloud-logging
4950

5051
#or
5152

5253
export OTEL_LOGS_EXPORTER=cloud-logging
53-
export OTEL_METRICS_EXPORTER=cloud-logging
54+
export OTEL_METRICS_EXPORTER=cloud-logging,dynatrace
5455
export OTEL_TRACES_EXPORTER=cloud-logging
5556
java #...
5657
```
@@ -59,7 +60,7 @@ java #...
5960

6061
```sh
6162
-Dotel.logs.exporter=otlp \
62-
-Dotel.metrics.exporter=otlp # default value \
63+
-Dotel.metrics.exporter=otlp \ # default value
6364
-Dotel.traces.exporter=otlp # default value
6465

6566
#or
@@ -75,8 +76,8 @@ That means, without any configuration the agent with the extension will forward
7576
The difference between `cloud-logging` and `otlp` exporters are explained in an own [section](#implementation-differences-between-cloud-logging-and-otlp-exporter).
7677
The benefit of the `cloud-logging` exporter is, that it can be combined with a different configuration of the `otlp` exporter.
7778

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).
79+
For the instrumentation to send observability data to [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging) or [Dynatrace](https://docs.dynatrace.com/docs/setup-and-configuration/setup-on-container-platforms/cloud-foundry/deploy-oneagent-on-sap-cloud-platform-for-application-only-monitoring), the application needs to be bound to a corresponding service instances.
80+
The service instances can be either managed or [user-provided](#using-user-provided-service-instances).
8081

8182
## Configuration
8283

@@ -103,16 +104,19 @@ The extension itself can be configured by specifying the following system proper
103104
|----------|---------------|---------|
104105
| `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. |
105106
| `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. |
107+
| `otel.javaagent.extension.sap.cf.binding.dynatrace.label` | `dynatrace` | The label of the managed service binding to bind to. |
108+
| `otel.javaagent.extension.sap.cf.binding.dynatrace.tag` | `dynatrace` | The tag of any service binding (managed or user-provided) to bind to. |
109+
| `otel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name` | | The name of the field containing the Dynatrace API token within the service binding credentials. This is required to send metrics to Dynatrace. |
106110
| `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. |
107111
| `otel.javaagent.extension.sap.cf.resource.enabled` | `true` | Whether to add CF resource attributes to all events. |
108112

109113
> The `otel.javaagent.extension.sap.*` properties are preferred over the `com.sap.otel.extension.*` properties, which are kept for compatibility.
110114
Each `otel.javaagent.extension.sap.*` property can also be provided as environment variable `OTEL_JAVAAGENT_EXTENSION_SAP_*`.
111115

112116
The extension will scan the environment variable `VCAP_SERVICES` for CF service bindings.
113-
User-provided bindings will take precedence over managed bindings of the configured label ("cloud-logging" by default).
114-
All matching bindings are filtered for the configured tag ("Cloud Logging" by default).
115-
The first binding will be taken for configuration for the OpenTelemetry exporter.
117+
User-provided bindings will take precedence over managed bindings of the configured label ("cloud-logging" or "dynatrace" by default).
118+
All matching bindings are filtered for the configured tag ("Cloud Logging" od "dynatrace" by default).
119+
The first Cloud Logging binding will be taken for configuration for the standard OpenTelemetry (otlp) exporter.
116120
Preferring user-provided services over managed service instances allows better control of the binding properties, e.g. syslog drains.
117121

118122
### Recommended Agent Configuration
@@ -140,14 +144,16 @@ The [OpenTelemetry Java Instrumentation project](https://github.com/open-telemet
140144

141145
## Using User-Provided Service Instances
142146

147+
### SAP Cloud Logging
148+
143149
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.
144150
This helps to fine-tune the configuration, e.g. leave out or reconfigure the syslog drain.
145151
Furthermore, this helps on sharing service instances across CF orgs or landscapes.
146152

147-
The extension requires four fields in the user-provided service credentials and needs to be tagged with the `com.sap.otel.extension.cloud-logging.tag` (default: `Cloud Logging`) documented in section [Configuration](#configuration).
153+
The extension requires four fields in the user-provided service credentials and needs to be tagged with the `otel.javaagent.extension.sap.cf.binding.cloud-logging.tag` (default: `Cloud Logging`) documented in section [Configuration](#configuration).
148154

149155
| Field name | Contents |
150-
|------------|---------|
156+
|------------|----------|
151157
| `ingest-otlp-endpoint` | The OTLP endpoint including port. It will be prefixed with `https://`. |
152158
| `ingest-otlp-key` | The mTLS client key in PCKS#8 format. Line breaks as `\n`. |
153159
| `ingest-otlp-cert`| The mTLS client certificate in PEM format matching the client key. Line breaks as `\n`. |
@@ -172,6 +178,31 @@ Note, that you can easily feed arbitrary credentials to the extension.
172178
It does not need to be [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging).
173179
You can even change the tag using the configuration parameters of the extension.
174180

181+
### Dynatrace
182+
183+
SAP BTP internally offers a managed Dynatrace service, that is recognized by the extension.
184+
Externally, user-provided service instances need to be created.
185+
The [Dynatrace documentation](https://docs.dynatrace.com/docs/setup-and-configuration/setup-on-container-platforms/cloud-foundry/deploy-oneagent-on-sap-cloud-platform-for-application-only-monitoring) explains, how to generate the necessary access url and tokens.
186+
The extension requires two fields in the user-provided service credentials and needs to be tagged with the `otel.javaagent.extension.sap.cf.binding.dynatrace.tag` (default: `dynatrace`) documented in section [Configuration](#configuration).
187+
188+
| Field name | Contents |
189+
|------------|----------|
190+
| `apiurl` | The Dynatrace API endpoint, e.g. `https://apm.example.com/e/<some-uuid>/api`. This url will be appended with `/v2/otlp/v1/metrics` to create the full endpoint url. |
191+
| `<your_token_field>` | The API token to be used with the above endpoint. Ensure, that it has the required permissions to ingest data over the endpoint. |
192+
193+
Do not forget to configure the name chosen for `<your_token_field>` via the respective configuration property:
194+
195+
```sh
196+
java #... \
197+
-Dotel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name=<your_token_field> \
198+
# ...
199+
200+
# or
201+
202+
OTEL_JAVAAGENT_EXTENSION_SAP_CF_BINDING_DYNATRACE_METRICS_TOKEN-NAME=<your_token_field>
203+
java #...
204+
```
205+
175206
## Implementation Differences between Cloud-Logging and OTLP Exporter
176207

177208
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.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class CloudLoggingConfigurationCustomizerProvider implements AutoConfigur
1212
@Override
1313
public void customize(AutoConfigurationCustomizer autoConfiguration) {
1414
autoConfiguration
15-
.addPropertiesSupplier(new CloudLoggingBindingPropertiesSupplier(cfEnv));
15+
.addPropertiesSupplier(new CloudLoggingBindingPropertiesSupplier());
1616

1717
// ConfigurableLogRecordExporterProvider
1818
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding;
2+
3+
import io.pivotal.cfenv.core.CfEnv;
4+
import io.pivotal.cfenv.core.CfService;
5+
6+
import java.util.List;
7+
import java.util.stream.Stream;
8+
9+
class CloudFoundryServicesAdapter {
10+
11+
private final CfEnv cfEnv;
12+
13+
CloudFoundryServicesAdapter(CfEnv cfEnv) {
14+
this.cfEnv = cfEnv;
15+
}
16+
17+
/**
18+
* Stream CfServices, that match the provided properties. Empty or null values are interpreted as not applicable. No
19+
* check will be performed during search. User-provided service instances will be preferred unless the
20+
* {@code userProvidedLabel is null or empty. Provided only null values will return all service instances.
21+
*
22+
* @param serviceLabels the labels of services
23+
* @param serviceTags the tags of services
24+
* @return a stream of service instances present in the CloudFoundry environment variable VCAP_SERVICES
25+
*/
26+
Stream<CfService> stream(List<String> serviceLabels, List<String> serviceTags) {
27+
Stream<CfService> services;
28+
if (serviceLabels == null || serviceLabels.isEmpty())
29+
services = cfEnv.findAllServices().stream();
30+
else {
31+
services = serviceLabels.stream().flatMap(l -> cfEnv.findServicesByLabel(l).stream());
32+
}
33+
if (serviceTags == null || serviceTags.isEmpty()) {
34+
return services;
35+
}
36+
return services.filter(svc -> svc.existsByTagIgnoreCase(serviceTags.toArray(new String[0])));
37+
}
38+
39+
}

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import java.util.function.Supplier;
1515
import java.util.logging.Level;
1616
import java.util.logging.Logger;
17-
import java.util.stream.Stream;
1817

1918
public class CloudLoggingBindingPropertiesSupplier implements Supplier<Map<String, String>> {
2019

@@ -26,13 +25,20 @@ public class CloudLoggingBindingPropertiesSupplier implements Supplier<Map<Strin
2625

2726
private final CloudLoggingServicesProvider cloudLoggingServicesProvider;
2827

29-
public CloudLoggingBindingPropertiesSupplier(CfEnv cfEnv) {
28+
public CloudLoggingBindingPropertiesSupplier() {
29+
this(new CloudLoggingServicesProvider(getDefaultProperties(), new CloudFoundryServicesAdapter(new CfEnv())));
30+
}
31+
32+
CloudLoggingBindingPropertiesSupplier(CloudLoggingServicesProvider cloudLoggingServicesProvider) {
33+
this.cloudLoggingServicesProvider = cloudLoggingServicesProvider;
34+
}
35+
36+
private static ConfigProperties getDefaultProperties() {
3037
Map<String, String> defaults = new HashMap<>();
3138
defaults.put("com.sap.otel.extension.cloud-logging.label", "cloud-logging");
3239
defaults.put("com.sap.otel.extension.cloud-logging.tag", "Cloud Logging");
3340
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);
41+
return DefaultConfigProperties.create(defaults);
3642
}
3743

3844
private static boolean isBlank(String text) {

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

+12-10
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66

77
import java.util.List;
88
import java.util.function.Supplier;
9-
import java.util.stream.Collectors;
109
import java.util.stream.Stream;
1110

11+
import static java.util.Arrays.asList;
12+
import static java.util.Collections.singletonList;
13+
import static java.util.stream.Collectors.toList;
14+
1215
public class CloudLoggingServicesProvider implements Supplier<Stream<CfService>> {
1316

1417
private static final String DEFAULT_USER_PROVIDED_LABEL = "user-provided";
@@ -17,15 +20,14 @@ public class CloudLoggingServicesProvider implements Supplier<Stream<CfService>>
1720

1821
private final List<CfService> services;
1922

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());
23+
public CloudLoggingServicesProvider(ConfigProperties config) {
24+
this(config, new CloudFoundryServicesAdapter(new CfEnv()));
25+
}
26+
27+
CloudLoggingServicesProvider(ConfigProperties config, CloudFoundryServicesAdapter adapter) {
28+
List<String> serviceLabels = asList(getUserProvidedLabel(config), getCloudLoggingLabel(config));
29+
List<String> serviceTags = singletonList(getCloudLoggingTag(config));
30+
this.services = adapter.stream(serviceLabels, serviceTags).collect(toList());
2931
}
3032

3133
private String getUserProvidedLabel(ConfigProperties config) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
10+
import static java.util.Arrays.asList;
11+
import static java.util.Collections.singletonList;
12+
13+
public class DynatraceServiceProvider implements Supplier<CfService> {
14+
15+
private static final String DEFAULT_USER_PROVIDED_LABEL = "user-provided";
16+
private static final String DEFAULT_DYNATRACE_LABEL = "dynatrace";
17+
private static final String DEFAULT_DYNATRACE_TAG = "dynatrace";
18+
19+
private final CfService service;
20+
21+
public DynatraceServiceProvider(ConfigProperties config) {
22+
this(config, new CloudFoundryServicesAdapter(new CfEnv()));
23+
}
24+
25+
DynatraceServiceProvider(ConfigProperties config, CloudFoundryServicesAdapter adapter) {
26+
List<String> serviceLabels = asList(getUserProvidedLabel(config), getDynatraceLabel(config));
27+
List<String> serviceTags = singletonList(getDynatraceTag(config));
28+
this.service = adapter.stream(serviceLabels, serviceTags).findFirst().orElse(null);
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 getDynatraceLabel(ConfigProperties config) {
36+
return config.getString("otel.javaagent.extension.sap.cf.binding.dynatrace.label", DEFAULT_DYNATRACE_LABEL);
37+
}
38+
39+
private String getDynatraceTag(ConfigProperties config) {
40+
return config.getString("otel.javaagent.extension.sap.cf.binding.dynatrace.tag", DEFAULT_DYNATRACE_TAG);
41+
}
42+
43+
@Override
44+
public CfService get() {
45+
return service;
46+
}
47+
}

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider;
88
import io.opentelemetry.sdk.common.export.RetryPolicy;
99
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
10-
import io.pivotal.cfenv.core.CfEnv;
1110
import io.pivotal.cfenv.core.CfService;
1211

1312
import java.time.Duration;
@@ -25,7 +24,7 @@ public class CloudLoggingLogsExporterProvider implements ConfigurableLogRecordEx
2524
private final CloudLoggingCredentials.Parser credentialParser;
2625

2726
public CloudLoggingLogsExporterProvider() {
28-
this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get(), CloudLoggingCredentials.parser());
27+
this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser());
2928
}
3029

3130
CloudLoggingLogsExporterProvider(Function<ConfigProperties, Stream<CfService>> serviceProvider, CloudLoggingCredentials.Parser credentialParser) {

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import io.opentelemetry.sdk.metrics.export.MetricExporter;
1515
import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil;
1616
import io.pivotal.cfenv.core.CfCredentials;
17-
import io.pivotal.cfenv.core.CfEnv;
1817
import io.pivotal.cfenv.core.CfService;
1918

2019
import java.time.Duration;
@@ -35,7 +34,7 @@ public class CloudLoggingMetricsExporterProvider implements ConfigurableMetricEx
3534
private final CloudLoggingCredentials.Parser credentialParser;
3635

3736
public CloudLoggingMetricsExporterProvider() {
38-
this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get(), CloudLoggingCredentials.parser());
37+
this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser());
3938
}
4039

4140
CloudLoggingMetricsExporterProvider(Function<ConfigProperties, Stream<CfService>> serviceProvider, CloudLoggingCredentials.Parser credentialParser) {

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import io.opentelemetry.sdk.common.export.RetryPolicy;
99
import io.opentelemetry.sdk.trace.export.SpanExporter;
1010
import io.pivotal.cfenv.core.CfCredentials;
11-
import io.pivotal.cfenv.core.CfEnv;
1211
import io.pivotal.cfenv.core.CfService;
1312

1413
import java.time.Duration;
@@ -26,7 +25,7 @@ public class CloudLoggingSpanExporterProvider implements ConfigurableSpanExporte
2625
private final CloudLoggingCredentials.Parser credentialParser;
2726

2827
public CloudLoggingSpanExporterProvider() {
29-
this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get(), CloudLoggingCredentials.parser());
28+
this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser());
3029
}
3130

3231
CloudLoggingSpanExporterProvider(Function<ConfigProperties, Stream<CfService>> serviceProvider, CloudLoggingCredentials.Parser credentialParser) {

0 commit comments

Comments
 (0)