diff --git a/cf-java-logging-support-opentelemetry-agent-extension/README.md b/cf-java-logging-support-opentelemetry-agent-extension/README.md index b282e9f..2f3d51f 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/README.md +++ b/cf-java-logging-support-opentelemetry-agent-extension/README.md @@ -1,13 +1,14 @@ -# OpenTelemetry Java Agent Extension for SAP Cloud Logging +# OpenTelemetry Java Agent Extension for SAP BTP Observability This module provides an extension for the [OpenTelemetry Java Agent](https://opentelemetry.io/docs/instrumentation/java/automatic/). -The extension scans the service bindings of an application for [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging). -If such a binding is found, the OpenTelemetry Java Agent is configured to ship observability data to that service. +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). +If such a binding is found, the OpenTelemetry Java Agent is configured to ship observability data to those services. Thus, this extension provides a convenient auto-instrumentation for Java applications running on SAP BTP. The extension provides the following main features: * additional exporters for logs, metrics and traces for [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging) +* 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) * auto-configuration of the generic OpenTelemetry connection to [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging) * adding resource attributes describing the CF application @@ -40,17 +41,17 @@ See the [example manifest](../sample-spring-boot/manifest-otel-javaagent.yml), h 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): -1. Use the `cloud-logging` exporters explicitly as provided by the extension. +1. Use the `cloud-logging` and/or `dynatrace` exporters explicitly as provided by the extension. This can be achieved via system properties or environment variables: ```sh -Dotel.logs.exporter=cloud-logging \ --Dotel.metrics.exporter=cloud-logging \ +-Dotel.metrics.exporter=cloud-logging,dynatrace \ -Dotel.traces.exporter=cloud-logging #or export OTEL_LOGS_EXPORTER=cloud-logging -export OTEL_METRICS_EXPORTER=cloud-logging +export OTEL_METRICS_EXPORTER=cloud-logging,dynatrace export OTEL_TRACES_EXPORTER=cloud-logging java #... ``` @@ -59,7 +60,7 @@ java #... ```sh -Dotel.logs.exporter=otlp \ --Dotel.metrics.exporter=otlp # default value \ +-Dotel.metrics.exporter=otlp \ # default value -Dotel.traces.exporter=otlp # default value #or @@ -75,8 +76,8 @@ That means, without any configuration the agent with the extension will forward The difference between `cloud-logging` and `otlp` exporters are explained in an own [section](#implementation-differences-between-cloud-logging-and-otlp-exporter). The benefit of the `cloud-logging` exporter is, that it can be combined with a different configuration of the `otlp` exporter. -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. -The service instance can be either managed or [user-provided](#using-user-provided-service-instances). +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. +The service instances can be either managed or [user-provided](#using-user-provided-service-instances). ## Configuration @@ -103,6 +104,9 @@ The extension itself can be configured by specifying the following system proper |----------|---------------|---------| | `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. | | `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. | +| `otel.javaagent.extension.sap.cf.binding.dynatrace.label` | `dynatrace` | The label of the managed service binding to bind to. | +| `otel.javaagent.extension.sap.cf.binding.dynatrace.tag` | `dynatrace` | The tag of any service binding (managed or user-provided) to bind to. | +| `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. | | `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. | | `otel.javaagent.extension.sap.cf.resource.enabled` | `true` | Whether to add CF resource attributes to all events. | @@ -110,9 +114,9 @@ The extension itself can be configured by specifying the following system proper Each `otel.javaagent.extension.sap.*` property can also be provided as environment variable `OTEL_JAVAAGENT_EXTENSION_SAP_*`. The extension will scan the environment variable `VCAP_SERVICES` for CF service bindings. -User-provided bindings will take precedence over managed bindings of the configured label ("cloud-logging" by default). -All matching bindings are filtered for the configured tag ("Cloud Logging" by default). -The first binding will be taken for configuration for the OpenTelemetry exporter. +User-provided bindings will take precedence over managed bindings of the configured label ("cloud-logging" or "dynatrace" by default). +All matching bindings are filtered for the configured tag ("Cloud Logging" od "dynatrace" by default). +The first Cloud Logging binding will be taken for configuration for the standard OpenTelemetry (otlp) exporter. Preferring user-provided services over managed service instances allows better control of the binding properties, e.g. syslog drains. ### Recommended Agent Configuration @@ -140,14 +144,16 @@ The [OpenTelemetry Java Instrumentation project](https://github.com/open-telemet ## Using User-Provided Service Instances +### SAP Cloud Logging + 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. This helps to fine-tune the configuration, e.g. leave out or reconfigure the syslog drain. Furthermore, this helps on sharing service instances across CF orgs or landscapes. -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). +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). | Field name | Contents | -|------------|---------| +|------------|----------| | `ingest-otlp-endpoint` | The OTLP endpoint including port. It will be prefixed with `https://`. | | `ingest-otlp-key` | The mTLS client key in PCKS#8 format. Line breaks as `\n`. | | `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. It does not need to be [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging). You can even change the tag using the configuration parameters of the extension. +### Dynatrace + +SAP BTP internally offers a managed Dynatrace service, that is recognized by the extension. +Externally, user-provided service instances need to be created. +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. +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). + +| Field name | Contents | +|------------|----------| +| `apiurl` | The Dynatrace API endpoint, e.g. `https://apm.example.com/e//api`. This url will be appended with `/v2/otlp/v1/metrics` to create the full endpoint url. | +| `` | The API token to be used with the above endpoint. Ensure, that it has the required permissions to ingest data over the endpoint. | + +Do not forget to configure the name chosen for `` via the respective configuration property: + +```sh +java #... \ +-Dotel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name= \ +# ... + +# or + +OTEL_JAVAAGENT_EXTENSION_SAP_CF_BINDING_DYNATRACE_METRICS_TOKEN-NAME= +java #... +``` + ## Implementation Differences between Cloud-Logging and OTLP Exporter 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. diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java index b02d657..5052f82 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/CloudLoggingConfigurationCustomizerProvider.java @@ -12,7 +12,7 @@ public class CloudLoggingConfigurationCustomizerProvider implements AutoConfigur @Override public void customize(AutoConfigurationCustomizer autoConfiguration) { autoConfiguration - .addPropertiesSupplier(new CloudLoggingBindingPropertiesSupplier(cfEnv)); + .addPropertiesSupplier(new CloudLoggingBindingPropertiesSupplier()); // ConfigurableLogRecordExporterProvider } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudFoundryServicesAdapter.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudFoundryServicesAdapter.java new file mode 100644 index 0000000..03c8f1f --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudFoundryServicesAdapter.java @@ -0,0 +1,39 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding; + +import io.pivotal.cfenv.core.CfEnv; +import io.pivotal.cfenv.core.CfService; + +import java.util.List; +import java.util.stream.Stream; + +class CloudFoundryServicesAdapter { + + private final CfEnv cfEnv; + + CloudFoundryServicesAdapter(CfEnv cfEnv) { + this.cfEnv = cfEnv; + } + + /** + * Stream CfServices, that match the provided properties. Empty or null values are interpreted as not applicable. No + * check will be performed during search. User-provided service instances will be preferred unless the + * {@code userProvidedLabel is null or empty. Provided only null values will return all service instances. + * + * @param serviceLabels the labels of services + * @param serviceTags the tags of services + * @return a stream of service instances present in the CloudFoundry environment variable VCAP_SERVICES + */ + Stream stream(List serviceLabels, List serviceTags) { + Stream services; + if (serviceLabels == null || serviceLabels.isEmpty()) + services = cfEnv.findAllServices().stream(); + else { + services = serviceLabels.stream().flatMap(l -> cfEnv.findServicesByLabel(l).stream()); + } + if (serviceTags == null || serviceTags.isEmpty()) { + return services; + } + return services.filter(svc -> svc.existsByTagIgnoreCase(serviceTags.toArray(new String[0]))); + } + +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingBindingPropertiesSupplier.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingBindingPropertiesSupplier.java index 98bf9ce..2b11b6d 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingBindingPropertiesSupplier.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingBindingPropertiesSupplier.java @@ -14,7 +14,6 @@ import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Stream; public class CloudLoggingBindingPropertiesSupplier implements Supplier> { @@ -26,13 +25,20 @@ public class CloudLoggingBindingPropertiesSupplier implements Supplier defaults = new HashMap<>(); defaults.put("com.sap.otel.extension.cloud-logging.label", "cloud-logging"); defaults.put("com.sap.otel.extension.cloud-logging.tag", "Cloud Logging"); defaults.put("otel.javaagent.extension.sap.cf.binding.user-provided.label", "user-provided"); - ConfigProperties configProperties = DefaultConfigProperties.create(defaults); - this.cloudLoggingServicesProvider = new CloudLoggingServicesProvider(configProperties, cfEnv); + return DefaultConfigProperties.create(defaults); } private static boolean isBlank(String text) { diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProvider.java index 0ef8a0c..4ed5616 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProvider.java @@ -6,9 +6,12 @@ import java.util.List; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.toList; + public class CloudLoggingServicesProvider implements Supplier> { private static final String DEFAULT_USER_PROVIDED_LABEL = "user-provided"; @@ -17,15 +20,14 @@ public class CloudLoggingServicesProvider implements Supplier> private final List services; - public CloudLoggingServicesProvider(ConfigProperties config, CfEnv cfEnv) { - String userProvidedLabel = getUserProvidedLabel(config); - String cloudLoggingLabel = getCloudLoggingLabel(config); - String cloudLoggingTag = getCloudLoggingTag(config); - List userProvided = cfEnv.findServicesByLabel(userProvidedLabel); - List managed = cfEnv.findServicesByLabel(cloudLoggingLabel); - this.services = Stream.concat(userProvided.stream(), managed.stream()) - .filter(svc -> svc.existsByTagIgnoreCase(cloudLoggingTag)) - .collect(Collectors.toList()); + public CloudLoggingServicesProvider(ConfigProperties config) { + this(config, new CloudFoundryServicesAdapter(new CfEnv())); + } + + CloudLoggingServicesProvider(ConfigProperties config, CloudFoundryServicesAdapter adapter) { + List serviceLabels = asList(getUserProvidedLabel(config), getCloudLoggingLabel(config)); + List serviceTags = singletonList(getCloudLoggingTag(config)); + this.services = adapter.stream(serviceLabels, serviceTags).collect(toList()); } private String getUserProvidedLabel(ConfigProperties config) { diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/DynatraceServiceProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/DynatraceServiceProvider.java new file mode 100644 index 0000000..cfd2c13 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/DynatraceServiceProvider.java @@ -0,0 +1,47 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.pivotal.cfenv.core.CfEnv; +import io.pivotal.cfenv.core.CfService; + +import java.util.List; +import java.util.function.Supplier; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + +public class DynatraceServiceProvider implements Supplier { + + private static final String DEFAULT_USER_PROVIDED_LABEL = "user-provided"; + private static final String DEFAULT_DYNATRACE_LABEL = "dynatrace"; + private static final String DEFAULT_DYNATRACE_TAG = "dynatrace"; + + private final CfService service; + + public DynatraceServiceProvider(ConfigProperties config) { + this(config, new CloudFoundryServicesAdapter(new CfEnv())); + } + + DynatraceServiceProvider(ConfigProperties config, CloudFoundryServicesAdapter adapter) { + List serviceLabels = asList(getUserProvidedLabel(config), getDynatraceLabel(config)); + List serviceTags = singletonList(getDynatraceTag(config)); + this.service = adapter.stream(serviceLabels, serviceTags).findFirst().orElse(null); + } + + private String getUserProvidedLabel(ConfigProperties config) { + return config.getString("otel.javaagent.extension.sap.cf.binding.user-provided.label", DEFAULT_USER_PROVIDED_LABEL); + } + + private String getDynatraceLabel(ConfigProperties config) { + return config.getString("otel.javaagent.extension.sap.cf.binding.dynatrace.label", DEFAULT_DYNATRACE_LABEL); + } + + private String getDynatraceTag(ConfigProperties config) { + return config.getString("otel.javaagent.extension.sap.cf.binding.dynatrace.tag", DEFAULT_DYNATRACE_TAG); + } + + @Override + public CfService get() { + return service; + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java index 72971e6..d9ff17c 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java @@ -7,7 +7,6 @@ import io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider; import io.opentelemetry.sdk.common.export.RetryPolicy; import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import io.pivotal.cfenv.core.CfEnv; import io.pivotal.cfenv.core.CfService; import java.time.Duration; @@ -25,7 +24,7 @@ public class CloudLoggingLogsExporterProvider implements ConfigurableLogRecordEx private final CloudLoggingCredentials.Parser credentialParser; public CloudLoggingLogsExporterProvider() { - this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get(), CloudLoggingCredentials.parser()); + this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser()); } CloudLoggingLogsExporterProvider(Function> serviceProvider, CloudLoggingCredentials.Parser credentialParser) { diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java index b5ec3d4..6d3e27d 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java @@ -14,7 +14,6 @@ import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil; import io.pivotal.cfenv.core.CfCredentials; -import io.pivotal.cfenv.core.CfEnv; import io.pivotal.cfenv.core.CfService; import java.time.Duration; @@ -35,7 +34,7 @@ public class CloudLoggingMetricsExporterProvider implements ConfigurableMetricEx private final CloudLoggingCredentials.Parser credentialParser; public CloudLoggingMetricsExporterProvider() { - this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get(), CloudLoggingCredentials.parser()); + this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser()); } CloudLoggingMetricsExporterProvider(Function> serviceProvider, CloudLoggingCredentials.Parser credentialParser) { diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java index 6bbcd0b..1c88b22 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java @@ -8,7 +8,6 @@ import io.opentelemetry.sdk.common.export.RetryPolicy; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.pivotal.cfenv.core.CfCredentials; -import io.pivotal.cfenv.core.CfEnv; import io.pivotal.cfenv.core.CfService; import java.time.Duration; @@ -26,7 +25,7 @@ public class CloudLoggingSpanExporterProvider implements ConfigurableSpanExporte private final CloudLoggingCredentials.Parser credentialParser; public CloudLoggingSpanExporterProvider() { - this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get(), CloudLoggingCredentials.parser()); + this(config -> new CloudLoggingServicesProvider(config).get(), CloudLoggingCredentials.parser()); } CloudLoggingSpanExporterProvider(Function> serviceProvider, CloudLoggingCredentials.Parser credentialParser) { diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceMetricsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceMetricsExporterProvider.java new file mode 100644 index 0000000..b2ae59e --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceMetricsExporterProvider.java @@ -0,0 +1,122 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.DynatraceServiceProvider; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; +import io.opentelemetry.sdk.common.export.RetryPolicy; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil; +import io.pivotal.cfenv.core.CfService; + +import java.time.Duration; +import java.util.function.Function; +import java.util.logging.Logger; + +import static io.opentelemetry.sdk.metrics.Aggregation.explicitBucketHistogram; + +public class DynatraceMetricsExporterProvider implements ConfigurableMetricExporterProvider { + + public static final String CRED_DYNATRACE_APIURL = "apiurl"; + public static final String DT_APIURL_METRICS_SUFFIX = "/v2/otlp/v1/metrics"; + private static final Logger LOG = Logger.getLogger(DynatraceMetricsExporterProvider.class.getName()); + private final Function serviceProvider; + + public DynatraceMetricsExporterProvider() { + this(config -> new DynatraceServiceProvider(config).get()); + } + + public DynatraceMetricsExporterProvider(Function serviceProvider) { + this.serviceProvider = serviceProvider; + } + + private static String getCompression(ConfigProperties config) { + String compression = config.getString("otel.exporter.dynatrace.metrics.compression"); + return compression != null ? compression : config.getString("otel.exporter.dynatrace.compression", "gzip"); + } + + private static Duration getTimeOut(ConfigProperties config) { + Duration timeout = config.getDuration("otel.exporter.dynatrace.metrics.timeout"); + return timeout != null ? timeout : config.getDuration("otel.exporter.dynatrace.timeout"); + } + + private static DefaultAggregationSelector getDefaultAggregationSelector(ConfigProperties config) { + String defaultHistogramAggregation = + config.getString("otel.exporter.dynatrace.metrics.default.histogram.aggregation"); + if (defaultHistogramAggregation == null) { + return DefaultAggregationSelector.getDefault().with(InstrumentType.HISTOGRAM, Aggregation.defaultAggregation()); + } + if (AggregationUtil.aggregationName(Aggregation.base2ExponentialBucketHistogram()) + .equalsIgnoreCase(defaultHistogramAggregation)) { + return + DefaultAggregationSelector.getDefault() + .with(InstrumentType.HISTOGRAM, Aggregation.base2ExponentialBucketHistogram()); + } else if (AggregationUtil.aggregationName(explicitBucketHistogram()) + .equalsIgnoreCase(defaultHistogramAggregation)) { + return DefaultAggregationSelector.getDefault().with(InstrumentType.HISTOGRAM, Aggregation.explicitBucketHistogram()); + } else { + throw new ConfigurationException( + "Unrecognized default histogram aggregation: " + defaultHistogramAggregation); + } + } + + private static boolean isBlank(String text) { + return text == null || text.trim().isEmpty(); + } + + @Override + public String getName() { + return "dynatrace"; + } + + @Override + public MetricExporter createExporter(ConfigProperties config) { + CfService cfService = serviceProvider.apply(config); + if (cfService == null) { + LOG.info("No dynatrace service binding found. Skipping metrics exporter registration."); + return NoopMetricExporter.getInstance(); + } + + LOG.info("Creating metrics exporter for service binding " + cfService.getName() + " (" + cfService.getLabel() + ")"); + + String apiUrl = cfService.getCredentials().getString(CRED_DYNATRACE_APIURL); + if (isBlank(apiUrl)) { + LOG.warning("Credential \"" + CRED_DYNATRACE_APIURL + "\" not found. Skipping dynatrace exporter configuration"); + return NoopMetricExporter.getInstance(); + } + String tokenName = config.getString("otel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name"); + if (isBlank(tokenName)) { + LOG.warning("Configuration \"otel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name\" not found. Skipping dynatrace exporter configuration"); + return NoopMetricExporter.getInstance(); + } + String apiToken = cfService.getCredentials().getString(tokenName); + if (isBlank(apiUrl)) { + LOG.warning("Credential \"" + tokenName + "\" not found. Skipping dynatrace exporter configuration"); + return NoopMetricExporter.getInstance(); + } + + OtlpHttpMetricExporterBuilder builder = OtlpHttpMetricExporter.builder(); + System.out.println(apiToken); + builder.setEndpoint(apiUrl + DT_APIURL_METRICS_SUFFIX) + .setCompression(getCompression(config)) + .addHeader("Authorization", "Api-Token " + apiToken) + .setRetryPolicy(RetryPolicy.getDefault()) + .setAggregationTemporalitySelector(AggregationTemporalitySelector.alwaysCumulative()) + .setDefaultAggregationSelector(getDefaultAggregationSelector(config)); + + Duration timeOut = getTimeOut(config); + if (timeOut != null) { + builder.setTimeout(timeOut); + } + + LOG.info("Created metrics exporter for service binding " + cfService.getName() + " (" + cfService.getLabel() + ")"); + return builder.build(); + } + +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider b/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider index b44bf5c..82d65fa 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider @@ -1 +1,2 @@ -com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.CloudLoggingMetricsExporterProvider \ No newline at end of file +com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.CloudLoggingMetricsExporterProvider +com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.DynatraceMetricsExporterProvider \ No newline at end of file diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudFoundryServicesAdapterTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudFoundryServicesAdapterTest.java new file mode 100644 index 0000000..2077d81 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudFoundryServicesAdapterTest.java @@ -0,0 +1,113 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding; + +import io.pivotal.cfenv.core.CfEnv; +import io.pivotal.cfenv.core.CfService; +import org.hamcrest.FeatureMatcher; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class CloudFoundryServicesAdapterTest { + private static final String DEFAULT_VCAP_APPLICATION = "{}"; + private static final String DEFAULT_VCAP_SERVICES = "{" + + "\"managed-find-me-service\":[" + + "{\"label\":\"managed-find-me-service\", \"tags\":[\"Find Me!\"],\"name\":\"managed-find-me1\"}," + + "{\"label\":\"managed-find-me-service\", \"tags\":[\"Find Me!\"],\"name\":\"managed-find-me2\"}," + + "{\"label\":\"managed-find-me-service\", \"tags\":[\"You can't see me!\"],\"name\":\"managed-other\"}" + + "]," + + "\"managed-notice-me-not-service\":[" + + "{\"label\":\"managed-notice-me-not-service\", \"tags\":[\"Find Me!\"],\"name\":\"managed-other1\"}," + + "{\"label\":\"managed-notice-me-not-service\", \"tags\":[\"You can't see me!\"],\"name\":\"managed-other2\"}" + + "]," + + "\"user-provided\":[" + + "{\"label\":\"user-provided\", \"tags\":[\"Find Me!\"],\"name\":\"ups-find-me1\"}," + + "{\"label\":\"user-provided\", \"tags\":[\"Find Me!\"],\"name\":\"ups-find-me2\"}," + + "{\"label\":\"user-provided\", \"tags\":[\"You can't see me!\"],\"name\":\"ups-other\"}" + + "]}"; + + private static final CfEnv DEFAULT_CF_ENV = new CfEnv(DEFAULT_VCAP_APPLICATION, DEFAULT_VCAP_SERVICES); + private static final CloudFoundryServicesAdapter DEFAULT_ADAPTER = new CloudFoundryServicesAdapter(DEFAULT_CF_ENV); + + @NotNull + private static FeatureMatcher withName(String expected) { + return new FeatureMatcher(equalTo(expected), "name", "name") { + @Override + protected String featureValueOf(CfService cfService) { + return cfService.getName(); + } + }; + } + + @Test + public void getsAllServicesWithNullParameters() { + List services = DEFAULT_ADAPTER.stream(null, null).collect(toList()); + assertThat(services, allOf( + hasItem(withName("managed-find-me1")), + hasItem(withName("managed-find-me2")), + hasItem(withName("managed-other")), + hasItem(withName("managed-other1")), + hasItem(withName("managed-other2")), + hasItem(withName("ups-find-me1")), + hasItem(withName("ups-find-me2")), + hasItem(withName("ups-other")) + )); + } + + @Test + public void filtersBySingleLabel() { + List services = DEFAULT_ADAPTER.stream(Collections.singletonList("managed-find-me-service"), emptyList()).collect(toList()); + assertThat(services, allOf( + hasItem(withName("managed-find-me1")), + hasItem(withName("managed-find-me2")), + hasItem(withName("managed-other")) + )); + assertThat(services, hasSize(3)); + } + + @Test + public void priotizesByServiceLabel() { + List services = DEFAULT_ADAPTER.stream(asList("user-provided", "managed-find-me-service"), emptyList()).collect(toList()); + assertThat(services, contains( + withName("ups-find-me1"), + withName("ups-find-me2"), + withName("ups-other"), + withName("managed-find-me1"), + withName("managed-find-me2"), + withName("managed-other") + )); + } + + @Test + public void filtersBySingleTag() { + List services = DEFAULT_ADAPTER.stream(emptyList(), Collections.singletonList("Find Me!")).collect(toList()); + assertThat(services, allOf( + hasItem(withName("managed-find-me1")), + hasItem(withName("managed-find-me2")), + hasItem(withName("managed-other1")), + hasItem(withName("ups-find-me1")), + hasItem(withName("ups-find-me2")) + )); + assertThat(services, hasSize(5)); + } + + @Test + public void standardUseCase() { + List services = DEFAULT_ADAPTER.stream(asList("user-provided", "managed-find-me-service"), Collections.singletonList("Find Me!")).collect(toList()); + assertThat(services, contains( + withName("ups-find-me1"), + withName("ups-find-me2"), + withName("managed-find-me1"), + withName("managed-find-me2") + )); + } + + +} \ No newline at end of file diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingBindingPropertiesSupplierTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingBindingPropertiesSupplierTest.java index 9492572..6039d1d 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingBindingPropertiesSupplierTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingBindingPropertiesSupplierTest.java @@ -1,23 +1,48 @@ package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding; -import io.pivotal.cfenv.core.CfEnv; +import io.pivotal.cfenv.core.CfService; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) public class CloudLoggingBindingPropertiesSupplierTest { - private static final String VALID_CREDENTIALS = "{\"ingest-otlp-endpoint\":\"test-endpoint\", \"ingest-otlp-key\":\"test-client-key\", \"ingest-otlp-cert\":\"test-client-cert\", \"server-ca\":\"test-server-cert\"}"; - private static final String USER_PROVIDED_VALID = "{\"label\":\"user-provided\", \"name\":\"test-name\", \"tags\":[\"Cloud Logging\"], \"credentials\":" + VALID_CREDENTIALS + "}"; - private static final String MANAGED_VALID = "{\"label\":\"cloud-logging\", \"name\":\"test-name\", \"tags\":[\"Cloud Logging\"], \"credentials\":" + VALID_CREDENTIALS + "}"; + private static final Map CREDENTIALS = Collections.unmodifiableMap(new HashMap() {{ + put("ingest-otlp-endpoint", "test-endpoint"); + put("ingest-otlp-key", "test-client-key"); + put("ingest-otlp-cert", "test-client-cert"); + put("server-ca", "test-server-cert"); + }}); + + private static final Map BINDING = Collections.unmodifiableMap(new HashMap() {{ + put("label", "user-provided"); + put("name", "test-name"); + put("tags", Collections.singletonList("Cloud Logging")); + put("credentials", CREDENTIALS); + }}); + + @Mock + private CloudLoggingServicesProvider servicesProvider; + + @InjectMocks + private CloudLoggingBindingPropertiesSupplier propertiesSupplier; private static void assertFileContent(String expected, String filename) throws IOException { String contents = Files.readAllLines(Paths.get(filename)) @@ -26,53 +51,27 @@ private static void assertFileContent(String expected, String filename) throws I assertThat(contents, is(equalTo(expected))); } + private static CfService createCfService(Map properties, Map credentials) { + return new CfService(new HashMap(properties) {{ + put("credentials", credentials); + }}); + } + @Test public void emptyWithoutBindings() { - CfEnv cfEnv = new CfEnv("", ""); - CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(cfEnv); + when(servicesProvider.get()).thenReturn(Stream.empty()); Map properties = propertiesSupplier.get(); assertTrue(properties.isEmpty()); } @Test - public void extractsUserProvidedBinding() throws Exception { - CfEnv cfEnv = new CfEnv("", "{\"user-provided\":[" + USER_PROVIDED_VALID + "]}"); - CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(cfEnv); - Map properties = propertiesSupplier.get(); - assertThat(properties, hasEntry("otel.exporter.otlp.endpoint", "https://test-endpoint")); - assertThat(properties, hasKey("otel.exporter.otlp.client.key")); - assertFileContent("test-client-key", properties.get("otel.exporter.otlp.client.key")); - assertThat(properties, hasKey("otel.exporter.otlp.client.key")); - assertFileContent("test-client-key", properties.get("otel.exporter.otlp.client.key")); - assertThat(properties, hasKey("otel.exporter.otlp.client.certificate")); - assertFileContent("test-client-cert", properties.get("otel.exporter.otlp.client.certificate")); - assertThat(properties, hasKey("otel.exporter.otlp.certificate")); - assertFileContent("test-server-cert", properties.get("otel.exporter.otlp.certificate")); - } + public void extractsBinding() throws Exception { + when(servicesProvider.get()).thenReturn(Stream.of(createCfService(BINDING, CREDENTIALS))); + CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(servicesProvider); - @Test - public void extractsManagedBinding() throws Exception { - CfEnv cfEnv = new CfEnv("", "{\"cloud-logging\":[" + MANAGED_VALID + "]}"); - CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(cfEnv); Map properties = propertiesSupplier.get(); - assertThat(properties, hasEntry("otel.exporter.otlp.endpoint", "https://test-endpoint")); - assertThat(properties, hasKey("otel.exporter.otlp.client.key")); - assertFileContent("test-client-key", properties.get("otel.exporter.otlp.client.key")); - assertThat(properties, hasKey("otel.exporter.otlp.client.key")); - assertFileContent("test-client-key", properties.get("otel.exporter.otlp.client.key")); - assertThat(properties, hasKey("otel.exporter.otlp.client.certificate")); - assertFileContent("test-client-cert", properties.get("otel.exporter.otlp.client.certificate")); - assertThat(properties, hasKey("otel.exporter.otlp.certificate")); - assertFileContent("test-server-cert", properties.get("otel.exporter.otlp.certificate")); - } - @Test - public void prefersUserProvidedOverManaged() throws Exception { - String markedService = USER_PROVIDED_VALID.replace("test-endpoint", "user-endpoint"); - CfEnv cfEnv = new CfEnv("", "{\"cloud-logging\":[" + MANAGED_VALID + "], \"user-provided\":[" + markedService + "]}"); - CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(cfEnv); - Map properties = propertiesSupplier.get(); - assertThat(properties, hasEntry("otel.exporter.otlp.endpoint", "https://user-endpoint")); + assertThat(properties, hasEntry("otel.exporter.otlp.endpoint", "https://test-endpoint")); assertThat(properties, hasKey("otel.exporter.otlp.client.key")); assertFileContent("test-client-key", properties.get("otel.exporter.otlp.client.key")); assertThat(properties, hasKey("otel.exporter.otlp.client.key")); @@ -85,37 +84,53 @@ public void prefersUserProvidedOverManaged() throws Exception { @Test public void emptyWithoutEndpoint() { - String markedService = USER_PROVIDED_VALID.replace("test-endpoint", ""); - CfEnv cfEnv = new CfEnv("", "{\"user-provided\":[" + markedService + "]}"); - CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(cfEnv); + HashMap credentials = new HashMap(CREDENTIALS) {{ + remove("ingest-otlp-endpoint"); + }}; + when(servicesProvider.get()).thenReturn(Stream.of(createCfService(BINDING, credentials))); + CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(servicesProvider); + Map properties = propertiesSupplier.get(); + assertTrue(properties.isEmpty()); } @Test public void emptyWithoutClientCert() { - String markedService = USER_PROVIDED_VALID.replace("test-client-cert", ""); - CfEnv cfEnv = new CfEnv("", "{\"user-provided\":[" + markedService + "]}"); - CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(cfEnv); + HashMap credentials = new HashMap(CREDENTIALS) {{ + remove("ingest-otlp-cert"); + }}; + when(servicesProvider.get()).thenReturn(Stream.of(createCfService(BINDING, credentials))); + CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(servicesProvider); + Map properties = propertiesSupplier.get(); + assertTrue(properties.isEmpty()); } @Test public void emptyWithoutClientKey() { - String markedService = USER_PROVIDED_VALID.replace("test-client-key", ""); - CfEnv cfEnv = new CfEnv("", "{\"user-provided\":[" + markedService + "]}"); - CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(cfEnv); + HashMap credentials = new HashMap(CREDENTIALS) {{ + remove("ingest-otlp-key"); + }}; + when(servicesProvider.get()).thenReturn(Stream.of(createCfService(BINDING, credentials))); + CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(servicesProvider); + Map properties = propertiesSupplier.get(); + assertTrue(properties.isEmpty()); } @Test public void emptyWithoutServerCert() { - String markedService = USER_PROVIDED_VALID.replace("test-server-cert", ""); - CfEnv cfEnv = new CfEnv("", "{\"user-provided\":[" + markedService + "]}"); - CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(cfEnv); + HashMap credentials = new HashMap(CREDENTIALS) {{ + remove("server-ca"); + }}; + when(servicesProvider.get()).thenReturn(Stream.of(createCfService(BINDING, credentials))); + CloudLoggingBindingPropertiesSupplier propertiesSupplier = new CloudLoggingBindingPropertiesSupplier(servicesProvider); + Map properties = propertiesSupplier.get(); + assertTrue(properties.isEmpty()); } } \ No newline at end of file diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProviderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProviderTest.java index 701dc94..73a9083 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProviderTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProviderTest.java @@ -1,47 +1,47 @@ package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding; -import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider; import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; -import io.pivotal.cfenv.core.CfEnv; import io.pivotal.cfenv.core.CfService; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.stream.Stream; +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.contains; +import static org.mockito.Matchers.anyListOf; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +@RunWith(MockitoJUnitRunner.class) public class CloudLoggingServicesProviderTest { - private static final String DEFAULT_VCAP_APPLICATION = "{}"; - private static final String DEFAULT_VCAP_SERVICES = "{" + - "\"cloud-logging\":[" + - "{\"label\":\"cloud-logging\", \"tags\":[\"Cloud Logging\"],\"name\":\"managed-service1\"}," + - "{\"label\":\"cloud-logging\", \"tags\":[\"Cloud Logging\"],\"name\":\"managed-service2\"}" + - "]," + - "\"not-cloud-logging\":[" + - "{\"label\":\"not-cloud-logging\", \"tags\":[\"Cloud Logging\"],\"name\":\"managed-other\"}" + - "]," + - "\"user-provided\":[" + - "{\"label\":\"cloud-logging\", \"tags\":[\"Cloud Logging\"],\"name\":\"ups-cloud-logging\"}," + - "{\"label\":\"cloud-logging\", \"tags\":[\"NOT Cloud Logging\"],\"name\":\"ups-other\"}" + - "]}"; + @Mock + private CloudFoundryServicesAdapter adapter; + + @Mock + private CfService mockService; + + @Before + public void setUp() throws Exception { + when(adapter.stream(anyListOf(String.class), anyListOf(String.class))).thenReturn(Stream.of(mockService)); + } @Test public void defaultLabelsAndTags() { DefaultConfigProperties emptyProperties = DefaultConfigProperties.createFromMap(Collections.emptyMap()); - CfEnv cfEnv = new CfEnv(DEFAULT_VCAP_APPLICATION, DEFAULT_VCAP_SERVICES); - CloudLoggingServicesProvider servicesProvider = new CloudLoggingServicesProvider(emptyProperties, cfEnv); - List serviceNames = servicesProvider.get().map(CfService::getName).collect(Collectors.toList()); - assertThat(serviceNames, hasSize(3)); - assertThat(serviceNames, hasItem("managed-service1")); - assertThat(serviceNames, hasItem("managed-service2")); - assertThat(serviceNames, hasItem("ups-cloud-logging")); + CloudLoggingServicesProvider provider = new CloudLoggingServicesProvider(emptyProperties, adapter); + + assertThat(provider.get().collect(toList()), contains(mockService)); + verify(adapter).stream(asList("user-provided", "cloud-logging"), Collections.singletonList("Cloud Logging")); } @Test @@ -49,12 +49,11 @@ public void customLabel() { Map properties = new HashMap<>(); properties.put("otel.javaagent.extension.sap.cf.binding.cloud-logging.label", "not-cloud-logging"); properties.put("otel.javaagent.extension.sap.cf.binding.user-provided.label", "unknown-label"); - DefaultConfigProperties emptyProperties = DefaultConfigProperties.createFromMap(properties); - CfEnv cfEnv = new CfEnv(DEFAULT_VCAP_APPLICATION, DEFAULT_VCAP_SERVICES); - CloudLoggingServicesProvider servicesProvider = new CloudLoggingServicesProvider(emptyProperties, cfEnv); - List serviceNames = servicesProvider.get().map(CfService::getName).collect(Collectors.toList()); - assertThat(serviceNames, hasSize(1)); - assertThat(serviceNames, hasItem("managed-other")); + DefaultConfigProperties config = DefaultConfigProperties.createFromMap(properties); + CloudLoggingServicesProvider provider = new CloudLoggingServicesProvider(config, adapter); + + assertThat(provider.get().collect(toList()), contains(mockService)); + verify(adapter).stream(asList("unknown-label", "not-cloud-logging"), Collections.singletonList("Cloud Logging")); } @Test @@ -62,11 +61,10 @@ public void customTag() { Map properties = new HashMap<>(); properties.put("otel.javaagent.extension.sap.cf.binding.cloud-logging.tag", "NOT Cloud Logging"); DefaultConfigProperties emptyProperties = DefaultConfigProperties.createFromMap(properties); - CfEnv cfEnv = new CfEnv(DEFAULT_VCAP_APPLICATION, DEFAULT_VCAP_SERVICES); - CloudLoggingServicesProvider servicesProvider = new CloudLoggingServicesProvider(emptyProperties, cfEnv); - List serviceNames = servicesProvider.get().map(CfService::getName).collect(Collectors.toList()); - assertThat(serviceNames, hasSize(1)); - assertThat(serviceNames, hasItem("ups-other")); + CloudLoggingServicesProvider provider = new CloudLoggingServicesProvider(emptyProperties, adapter); + + assertThat(provider.get().collect(toList()), contains(mockService)); + verify(adapter).stream(asList("user-provided", "cloud-logging"), Collections.singletonList("NOT Cloud Logging")); } } \ No newline at end of file diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/DynatraceServicesProviderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/DynatraceServicesProviderTest.java new file mode 100644 index 0000000..179f215 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/DynatraceServicesProviderTest.java @@ -0,0 +1,69 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding; + +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; +import io.pivotal.cfenv.core.CfService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.anyListOf; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DynatraceServicesProviderTest { + + @Mock + private CloudFoundryServicesAdapter adapter; + + @Mock + private CfService mockService; + + @Before + public void setUp() throws Exception { + when(adapter.stream(anyListOf(String.class), anyListOf(String.class))).thenReturn(Stream.of(mockService)); + } + + @Test + public void defaultLabelsAndTags() { + DefaultConfigProperties emptyProperties = DefaultConfigProperties.createFromMap(Collections.emptyMap()); + DynatraceServiceProvider provider = new DynatraceServiceProvider(emptyProperties, adapter); + + assertThat(provider.get(), is(mockService)); + verify(adapter).stream(asList("user-provided", "dynatrace"), Collections.singletonList("dynatrace")); + } + + @Test + public void customLabel() { + Map properties = new HashMap<>(); + properties.put("otel.javaagent.extension.sap.cf.binding.dynatrace.label", "not-dynatrace"); + properties.put("otel.javaagent.extension.sap.cf.binding.user-provided.label", "unknown-label"); + DefaultConfigProperties config = DefaultConfigProperties.createFromMap(properties); + DynatraceServiceProvider provider = new DynatraceServiceProvider(config, adapter); + + assertThat(provider.get(), is(mockService)); + verify(adapter).stream(asList("unknown-label", "not-dynatrace"), Collections.singletonList("dynatrace")); + } + + @Test + public void customTag() { + Map properties = new HashMap<>(); + properties.put("otel.javaagent.extension.sap.cf.binding.dynatrace.tag", "NOT dynatrace"); + DefaultConfigProperties emptyProperties = DefaultConfigProperties.createFromMap(properties); + DynatraceServiceProvider provider = new DynatraceServiceProvider(emptyProperties, adapter); + + assertThat(provider.get(), is(mockService)); + verify(adapter).stream(asList("user-provided", "dynatrace"), Collections.singletonList("NOT dynatrace")); + } + +} \ No newline at end of file diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceMetricsExporterProviderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceMetricsExporterProviderTest.java new file mode 100644 index 0000000..c7c64e8 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceMetricsExporterProviderTest.java @@ -0,0 +1,98 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.pivotal.cfenv.core.CfService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class DynatraceMetricsExporterProviderTest { + + @Mock + private Function servicesProvider; + + @Mock + private ConfigProperties config; + + @InjectMocks + private DynatraceMetricsExporterProvider exporterProvider; + + @Before + public void setUp() { + when(config.getString("otel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name")).thenReturn("ingest-token"); + when(config.getString(any(), any())).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return invocation.getArguments()[1]; + } + }); + + } + + @Test + public void canLoadViaSPI() { + ServiceLoader loader = ServiceLoader.load(ConfigurableMetricExporterProvider.class); + Stream providers = StreamSupport.stream(loader.spliterator(), false); + assertTrue(DynatraceMetricsExporterProviderTest.class.getName() + " not loaded via SPI", + providers.anyMatch(p -> p instanceof DynatraceMetricsExporterProvider)); + } + + @Test + public void registersNoopExporterWithoutBindings() { + when(servicesProvider.apply(config)).thenReturn(null); + MetricExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), containsString("Noop")); + } + + @Test + public void registersNoopExporterWithInvalidBindings() { + CfService genericCfService = new CfService(Collections.emptyMap()); + Mockito.when(servicesProvider.apply(config)).thenReturn(genericCfService); + MetricExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), containsString("Noop")); + } + + @Test + public void registersExportersWithValidBindings() throws IOException { + Map credentials = new HashMap() {{ + put("apiurl", "https://example.dt/api"); + put("ingest-token", "secret"); + }}; + CfService dynatraceService = new CfService(new HashMap() {{ + put("name", "test-dt"); + put("label", "dynatrace"); + put("credentials", credentials); + }}); + when(servicesProvider.apply(config)).thenReturn(dynatraceService); + MetricExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), both(containsString("OtlpHttpMetricExporter")).and(containsString("https://example.dt/api/v2/otlp/v1/metrics,"))); + } + +} \ No newline at end of file diff --git a/sample-spring-boot/manifest-otel-javaagent.yml b/sample-spring-boot/manifest-otel-javaagent.yml index 34e72b4..6af20a3 100644 --- a/sample-spring-boot/manifest-otel-javaagent.yml +++ b/sample-spring-boot/manifest-otel-javaagent.yml @@ -8,6 +8,7 @@ applications: path: target/sample-app-spring-boot-3.8.1.jar buildpack: sap_java_buildpack memory: 256M + random-route: true env: # Set LOG_*: true to activate logging of respective field LOG_SENSITIVE_CONNECTION_DATA: false @@ -15,7 +16,8 @@ applications: LOG_REFERER: false JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']" JBP_CONFIG_SAP_MACHINE_JRE: '{ use_offline_repository: false, version: 17.+ }' - JBP_CONFIG_JAVA_OPTS: '[from_environment: false, java_opts: ''-javaagent:BOOT-INF/lib/opentelemetry-javaagent-1.31.0.jar -Dotel.javaagent.extensions=BOOT-INF/lib/cf-java-logging-support-opentelemetry-agent-extension-3.8.1.jar -Dotel.logs.exporter=cloud-logging,otlp -Dotel.instrumentation.logback-appender.experimental.capture-mdc-attributes=* -Dotel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes=true -Dotel.instrumentation.logback-appender.experimental.capture-code-attributes=true -Dotel.instrumentation.logback-appender.experimental-log-attributes=true -Dotel.experimental.resource.disabled-keys=process.command_line,process.command_args,process.executable.path'']' + JBP_CONFIG_JAVA_OPTS: '[from_environment: false, java_opts: ''-javaagent:BOOT-INF/lib/opentelemetry-javaagent-1.31.0.jar -Dotel.javaagent.extensions=BOOT-INF/lib/cf-java-logging-support-opentelemetry-agent-extension-3.8.1.jar -Dotel.logs.exporter=cloud-logging -Dotel.metrics.exporter=cloud-logging,dynatrace -Dotel.traces.exporter=cloud-logging -Dotel.instrumentation.logback-appender.experimental.capture-mdc-attributes=* -Dotel.instrumentation.logback-appender.experimental.capture-key-value-pair-attributes=true -Dotel.instrumentation.logback-appender.experimental.capture-code-attributes=true -Dotel.instrumentation.logback-appender.experimental-log-attributes=true -Dotel.experimental.resource.disabled-keys=process.command_line,process.command_args,process.executable.path -Dotel.javaagent.extension.sap.cf.binding.dynatrace.metrics.token-name=ingest_token'']' services: - - cls + - cloud-logging + - dynatrace-service