diff --git a/cf-java-logging-support-opentelemetry-agent-extension/README.md b/cf-java-logging-support-opentelemetry-agent-extension/README.md index f14cc20..8329794 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/README.md +++ b/cf-java-logging-support-opentelemetry-agent-extension/README.md @@ -1,14 +1,15 @@ # OpenTelemetry Java Agent Extension for SAP Cloud Logging 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. +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. Thus, this extension provides a convenient auto-instrumentation for Java applications running on SAP BTP. -The extension provides two main features: +The extension provides the following main features: -* auto-configuration of the OpenTelemetry connection to SAP Cloud Logging -* adding resource attributes to describe the CF application +* additional exporters for logs, metrics and traces for [SAP Cloud Logging](https://discovery-center.cloud.sap/serviceCatalog/cloud-logging) +* 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 See the section on [configuration](#configuration) for further details. @@ -18,7 +19,7 @@ Any Java application can be instrumented with the OpenTelemetry Java Agent and t ```sh java -javaagent:/path/to/opentelemetry-javaagent-.jar \ - -Dotel.javaagent-extensions=/path/to/cf-java-logging-support-opentelemetry-agent-extension-.jar \ + -Dotel.javaagent-extensions=/path/to/cf-java-logging-support-opentelemetry-agent-extension-.jar \ # your Java application command ``` @@ -37,11 +38,45 @@ java -javaagent:BOOT-INF/lib/opentelemetry-javaagent-.jar \ See the [example manifest](../sample-spring-boot/manifest-otel-javaagent.yml), how this translates into a deployment description. -For the instrumentation to send observability data to SAP 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). +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): + +1. Use the `cloud-logging` 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.traces.exporter=cloud-logging + +#or + +export OTEL_LOGS_EXPORTER=cloud-logging +export OTEL_METRICS_EXPORTER=cloud-logging +export OTEL_TRACES_EXPORTER=cloud-logging +java #... +``` + +2. Use the default `otlp` exporter with the provided default configuration from the extension: + +```sh +-Dotel.logs.exporter=otlp \ +-Dotel.metrics.exporter=otlp # default value \ +-Dotel.traces.exporter=otlp # default value + +#or -Note, that the OpenTelemetry Java Agent currently only sends traces and metrics by default. -To enable logs, the additional property `-Dotel.logs.exporter=otlp` is required. +export OTEL_LOGS_EXPORTER=otlp +export OTEL_METRICS_EXPORTER=otlp # default value +export OTEL_TRACES_EXPORTER=otlp # default value +java #... +``` + +Note, that the OpenTelemetry Java Agent currently sends traces and metrics by default using the `otlp` exporter. +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). +See TODO for the difference between `cloud-logging` and `otlp` exporters. +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). ## Configuration @@ -49,15 +84,30 @@ The OpenTelemetry Java Agent supports a wide variety of [configuration options]( As the extension provides configuration via SPI, all its configuration takes lower precedence than other configuration options for OpenTelemetry. Users can easily overwrite any setting using environment variables or system properties. +### Using the Extension + +The extension needs to be started with the OpenTelemetry Java Agent as outlined in the [Quick Start Guide](#quickstart-guide). +You need to enable shipping data either by using the `cloud-logging` exporters or relying on the `otlp` exporters for each signal type. +Multiple different exporters can be configured with comma separation. +Using the custom `cloud-logging` exporter will enable you, to use the default `otlp` exporter for different services. +The extension will configure a default endpoint and credentials for the `otlp` endpoints, so no further configuration is required. + +Note, that the `cloud-logging` exporter is just a facade for the `otlp` exporter to allow configuration of multiple data sinks. +There is no custom network client provided by this extension. + ### Configuring the Extension The extension itself can be configured by specifying the following system properties: | Property | Default Value | Comment | |----------|---------------|---------| -| `com.sap.otel.extension.cloud-logging.label` | `cloud-logging` | The label of the managed service binding to bind to. | -| `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.resource.enabled` or `env(OTEL_JAVAAGENT_EXTENSION_SAP_CF_RESOURCE_ENABLED)` | `true` | Whether to add CF resource attributes to all events. | +| `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.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. | + +> The `otel.javaagent.extension.sap.*` properties are preferred over the `com.sap.otel.extension.*` properties, which are kept for compatibility. +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). @@ -90,7 +140,7 @@ The [OpenTelemetry Java Instrumentation project](https://github.com/open-telemet ## Using User-Provided Service Instances -The extension provides support not only for managed service instance of SAP Cloud Logging but also for user-provided service instances. +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. @@ -103,7 +153,7 @@ The extension requires four fields in the user-provided service credentials and | `ingest-otlp-cert`| The mTLS client certificate in PEM format matching the client key. Line breaks as `\n`. | | `server-ca` | The trusted mTLS server certificate in PEM format. Line breaks as `\n`. | -If you have a SAP Cloud Logging service key, you can generate the required JSON file with jq: +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: ```bash cf service-key cls test \ @@ -119,5 +169,17 @@ Using this file, you can create the required user-provided service: ``` Note, that you can easily feed arbitrary credentials to the extension. -It does not need to be SAP Cloud Logging. +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. + +## 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. +The difference is just during the bootstrapping phase. +The main differences are: + +* The `cloud-logging` exporter will send data to all found bindings to SAP Cloud Logging. +The auto-instrumentation of the `otlp` exporter will just configure the first binding it finds priotizing user-provided services. +* 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. +* 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. +The `cloud-logging` exporter needs to be configured explictly as does the `otlp` exporter for logs. diff --git a/cf-java-logging-support-opentelemetry-agent-extension/dependency-reduced-pom.xml b/cf-java-logging-support-opentelemetry-agent-extension/dependency-reduced-pom.xml index 4af6f8d..4abce9a 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/dependency-reduced-pom.xml +++ b/cf-java-logging-support-opentelemetry-agent-extension/dependency-reduced-pom.xml @@ -32,6 +32,10 @@ io.opentelemetry com.fasterxml.jackson.core + com.squareup.okhttp3 + com.squareup.okio + org.jetbrains.kotlin + org.jetbrains @@ -58,7 +62,13 @@ io.opentelemetry opentelemetry-sdk-extension-autoconfigure 1.31.0 - provided + compile + + + io.opentelemetry + opentelemetry-exporter-otlp + 1.31.0 + compile org.slf4j diff --git a/cf-java-logging-support-opentelemetry-agent-extension/pom.xml b/cf-java-logging-support-opentelemetry-agent-extension/pom.xml index a4be8a8..6abaff7 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/pom.xml +++ b/cf-java-logging-support-opentelemetry-agent-extension/pom.xml @@ -49,7 +49,10 @@ io.opentelemetry opentelemetry-sdk-extension-autoconfigure - provided + + + io.opentelemetry + opentelemetry-exporter-otlp io.pivotal.cfenv @@ -83,6 +86,10 @@ io.opentelemetry com.fasterxml.jackson.core + com.squareup.okhttp3 + com.squareup.okio + org.jetbrains.kotlin + org.jetbrains 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 9cad03a..b02d657 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 @@ -13,6 +13,8 @@ public class CloudLoggingConfigurationCustomizerProvider implements AutoConfigur public void customize(AutoConfigurationCustomizer autoConfiguration) { autoConfiguration .addPropertiesSupplier(new CloudLoggingBindingPropertiesSupplier(cfEnv)); + + // ConfigurableLogRecordExporterProvider } } diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/attributes/CloudFoundryResourceCustomizer.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/attributes/CloudFoundryResourceCustomizer.java index fee1487..3a2795e 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/attributes/CloudFoundryResourceCustomizer.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/attributes/CloudFoundryResourceCustomizer.java @@ -34,7 +34,7 @@ public Resource apply(Resource resource, ConfigProperties configProperties) { CfApplication cfApp = cfEnv.getApp(); ResourceBuilder rb = Resource.builder(); rb.put("service.name", cfApp.getApplicationName()); - rb.put("sap.cf.source_id", getString(cfApp, "source_id")); + rb.put("sap.cf.source_id", cfApp.getApplicationId()); rb.put("sap.cf.instance_id", cfApp.getInstanceIndex()); rb.put("sap.cf.app_id", cfApp.getApplicationId()); rb.put("sap.cf.app_name", cfApp.getApplicationName()); @@ -42,6 +42,8 @@ public Resource apply(Resource resource, ConfigProperties configProperties) { rb.put("sap.cf.space_name", cfApp.getSpaceName()); rb.put("sap.cf.org_id", getString(cfApp, "organization_id")); rb.put("sap.cf.org_name", getString(cfApp, "organization_name")); + rb.put("sap.cf.process.id", getString(cfApp, "process_id")); + rb.put("sap.cf.process.type", getString(cfApp, "process_type")); return rb.build(); } 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 207f711..98bf9ce 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 @@ -1,5 +1,7 @@ package com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.pivotal.cfenv.core.CfEnv; import io.pivotal.cfenv.core.CfService; @@ -17,18 +19,34 @@ public class CloudLoggingBindingPropertiesSupplier implements Supplier> { private static final Logger LOG = Logger.getLogger(CloudLoggingBindingPropertiesSupplier.class.getName()); - private static final String CLOUD_LOGGING_LABEL = System.getProperty("com.sap.otel.extension.cloud-logging.label", "cloud-logging"); - private static final String CLOUD_LOGGING_TAG = System.getProperty("com.sap.otel.extension.cloud-logging.tag", "Cloud Logging"); - private static final String USER_PROVIDED_LABEL = "user-provided"; - public static final String OTLP_ENDPOINT = "ingest-otlp-endpoint"; - public static final String OTLP_CLIENT_KEY = "ingest-otlp-key"; - public static final String OTLP_CLIENT_CERT = "ingest-otlp-cert"; - public static final String OTLP_SERVER_CERT = "server-ca"; + private static final String OTLP_ENDPOINT = "ingest-otlp-endpoint"; + private static final String OTLP_CLIENT_KEY = "ingest-otlp-key"; + private static final String OTLP_CLIENT_CERT = "ingest-otlp-cert"; + private static final String OTLP_SERVER_CERT = "server-ca"; - private final CfEnv cfEnv; + private final CloudLoggingServicesProvider cloudLoggingServicesProvider; public CloudLoggingBindingPropertiesSupplier(CfEnv cfEnv) { - this.cfEnv = cfEnv; + Map 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); + } + + private static boolean isBlank(String text) { + return text == null || text.trim().isEmpty(); + } + + private static File writeFile(String prefix, String suffix, String content) throws IOException { + File file = File.createTempFile(prefix, suffix); + file.deleteOnExit(); + try (FileWriter writer = new FileWriter(file)) { + writer.append(content); + LOG.fine("Created temporary file " + file.getAbsolutePath()); + } + return file; } /** @@ -41,10 +59,7 @@ public CloudLoggingBindingPropertiesSupplier(CfEnv cfEnv) { */ @Override public Map get() { - Stream userProvided = cfEnv.findServicesByLabel(USER_PROVIDED_LABEL).stream(); - Stream managed = cfEnv.findServicesByLabel(CLOUD_LOGGING_LABEL).stream(); - return Stream.concat(userProvided, managed) - .filter(svc -> svc.existsByTagIgnoreCase(CLOUD_LOGGING_TAG)) + return cloudLoggingServicesProvider.get() .findFirst() .map(this::createEndpointConfiguration).orElseGet(Collections::emptyMap); } @@ -94,18 +109,4 @@ private Map createEndpointConfiguration(CfService svc) { } } - private static boolean isBlank(String text) { - return text == null || text.trim().isEmpty(); - } - - private static File writeFile(String prefix, String suffix, String content) throws IOException { - File file = File.createTempFile(prefix, suffix); - file.deleteOnExit(); - try (FileWriter writer = new FileWriter(file)) { - writer.append(content); - LOG.fine("Created temporary file " + file.getAbsolutePath()); - } - return file; - } - } 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 new file mode 100644 index 0000000..0ef8a0c --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProvider.java @@ -0,0 +1,49 @@ +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 java.util.stream.Collectors; +import java.util.stream.Stream; + +public class CloudLoggingServicesProvider implements Supplier> { + + private static final String DEFAULT_USER_PROVIDED_LABEL = "user-provided"; + private static final String DEFAULT_CLOUD_LOGGING_LABEL = "cloud-logging"; + private static final String DEFAULT_CLOUD_LOGGING_TAG = "Cloud Logging"; + + 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()); + } + + private String getUserProvidedLabel(ConfigProperties config) { + return config.getString("otel.javaagent.extension.sap.cf.binding.user-provided.label", DEFAULT_USER_PROVIDED_LABEL); + } + + private String getCloudLoggingLabel(ConfigProperties config) { + String fromOwnProperties = System.getProperty("com.sap.otel.extension.cloud-logging.label", DEFAULT_CLOUD_LOGGING_LABEL); + return config.getString("otel.javaagent.extension.sap.cf.binding.cloud-logging.label", fromOwnProperties); + } + + private String getCloudLoggingTag(ConfigProperties config) { + String fromOwnProperties = System.getProperty("com.sap.otel.extension.cloud-logging.tag", DEFAULT_CLOUD_LOGGING_TAG); + return config.getString("otel.javaagent.extension.sap.cf.binding.cloud-logging.tag", fromOwnProperties); + } + + @Override + public Stream get() { + return services.stream(); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java new file mode 100644 index 0000000..30dd39d --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java @@ -0,0 +1,96 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.pivotal.cfenv.core.CfCredentials; + +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; + +class CloudLoggingCredentials { + + private static final Logger LOG = Logger.getLogger(CloudLoggingCredentials.class.getName()); + + private static final Parser PARSER = new Parser(); + + private static final String CRED_OTLP_ENDPOINT = "ingest-otlp-endpoint"; + private static final String CRED_OTLP_CLIENT_KEY = "ingest-otlp-key"; + private static final String CRED_OTLP_CLIENT_CERT = "ingest-otlp-cert"; + private static final String CRED_OTLP_SERVER_CERT = "server-ca"; + private static final String CLOUD_LOGGING_ENDPOINT_PREFIX = "https://"; + + + private String endpoint; + private byte[] clientKey; + private byte[] clientCert; + private byte[] serverCert; + + private CloudLoggingCredentials() { + } + + static CloudLoggingCredentials.Parser parser() { + return PARSER; + } + + private static byte[] getPEMBytes(CfCredentials credentials, String key) { + String raw = credentials.getString(key); + return raw == null ? null : raw.trim().replace("\\n", "\n").getBytes(StandardCharsets.UTF_8); + } + + private static boolean isBlank(String text) { + return text == null || text.trim().isEmpty(); + } + + private static boolean isNullOrEmpty(byte[] bytes) { + return bytes == null || bytes.length == 0; + } + + public boolean validate() { + if (isBlank(endpoint)) { + LOG.warning("Credential \"" + CRED_OTLP_ENDPOINT + "\" not found. Skipping cloud-logging exporter configuration"); + return false; + } + + if (isNullOrEmpty(clientKey)) { + LOG.warning("Credential \"" + CRED_OTLP_CLIENT_KEY + "\" not found. Skipping cloud-logging exporter configuration"); + return false; + } + + if (isNullOrEmpty(clientCert)) { + LOG.warning("Credential \"" + CRED_OTLP_CLIENT_CERT + "\" not found. Skipping cloud-logging exporter configuration"); + return false; + } + + if (isNullOrEmpty(serverCert)) { + LOG.warning("Credential \"" + CRED_OTLP_SERVER_CERT + "\" not found. Skipping cloud-logging exporter configuration"); + return false; + } + return true; + } + + public String getEndpoint() { + return endpoint; + } + + public byte[] getClientKey() { + return clientKey; + } + + public byte[] getClientCert() { + return clientCert; + } + + public byte[] getServerCert() { + return serverCert; + } + + static class Parser { + CloudLoggingCredentials parse(CfCredentials cfCredentials) { + CloudLoggingCredentials parsed = new CloudLoggingCredentials(); + String rawEndpoint = cfCredentials.getString(CRED_OTLP_ENDPOINT); + parsed.endpoint = isBlank(rawEndpoint) ? null : CLOUD_LOGGING_ENDPOINT_PREFIX + rawEndpoint; + parsed.clientKey = getPEMBytes(cfCredentials, CRED_OTLP_CLIENT_KEY); + parsed.clientCert = getPEMBytes(cfCredentials, CRED_OTLP_CLIENT_CERT); + parsed.serverCert = getPEMBytes(cfCredentials, CRED_OTLP_SERVER_CERT); + return parsed; + } + } +} 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 new file mode 100644 index 0000000..72971e6 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java @@ -0,0 +1,82 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider; +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +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; +import java.util.List; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class CloudLoggingLogsExporterProvider implements ConfigurableLogRecordExporterProvider { + + private static final Logger LOG = Logger.getLogger(CloudLoggingLogsExporterProvider.class.getName()); + + private final Function> servicesProvider; + private final CloudLoggingCredentials.Parser credentialParser; + + public CloudLoggingLogsExporterProvider() { + this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get(), CloudLoggingCredentials.parser()); + } + + CloudLoggingLogsExporterProvider(Function> serviceProvider, CloudLoggingCredentials.Parser credentialParser) { + this.servicesProvider = serviceProvider; + this.credentialParser = credentialParser; + } + + private static String getCompression(ConfigProperties config) { + String compression = config.getString("otel.exporter.cloud-logging.logs.compression"); + return compression != null ? compression : config.getString("otel.exporter.cloud-logging.compression", "gzip"); + } + + private static Duration getTimeOut(ConfigProperties config) { + Duration timeout = config.getDuration("otel.exporter.cloud-logging.logs.timeout"); + return timeout != null ? timeout : config.getDuration("otel.exporter.cloud-logging.timeout"); + } + + @Override + public String getName() { + return "cloud-logging"; + } + + @Override + public LogRecordExporter createExporter(ConfigProperties config) { + List exporters = servicesProvider.apply(config) + .map(svc -> createExporter(config, svc)) + .filter(exp -> !(exp instanceof NoopLogRecordExporter)) + .collect(Collectors.toList()); + return LogRecordExporter.composite(exporters); + } + + private LogRecordExporter createExporter(ConfigProperties config, CfService service) { + LOG.info("Creating logs exporter for service binding " + service.getName() + " (" + service.getLabel() + ")"); + CloudLoggingCredentials credentials = credentialParser.parse(service.getCredentials()); + if (!credentials.validate()) { + return NoopLogRecordExporter.getInstance(); + } + + OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder(); + builder.setEndpoint(credentials.getEndpoint()) + .setCompression(getCompression(config)) + .setClientTls(credentials.getClientKey(), credentials.getClientCert()) + .setTrustedCertificates(credentials.getServerCert()) + .setRetryPolicy(RetryPolicy.getDefault()); + + Duration timeOut = getTimeOut(config); + if (timeOut != null) { + builder.setTimeout(timeOut); + } + + LOG.info("Created logs exporter for service binding " + service.getName() + " (" + service.getLabel() + ")"); + return builder.build(); + } +} 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 new file mode 100644 index 0000000..b5ec3d4 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java @@ -0,0 +1,133 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; +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.CfCredentials; +import io.pivotal.cfenv.core.CfEnv; +import io.pivotal.cfenv.core.CfService; + +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.opentelemetry.sdk.metrics.Aggregation.explicitBucketHistogram; + +public class CloudLoggingMetricsExporterProvider implements ConfigurableMetricExporterProvider { + + private static final Logger LOG = Logger.getLogger(CloudLoggingMetricsExporterProvider.class.getName()); + + private final Function> servicesProvider; + private final CloudLoggingCredentials.Parser credentialParser; + + public CloudLoggingMetricsExporterProvider() { + this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get(), CloudLoggingCredentials.parser()); + } + + CloudLoggingMetricsExporterProvider(Function> serviceProvider, CloudLoggingCredentials.Parser credentialParser) { + this.servicesProvider = serviceProvider; + this.credentialParser = credentialParser; + } + + private static String getCompression(ConfigProperties config) { + String compression = config.getString("otel.exporter.cloud-logging.metrics.compression"); + return compression != null ? compression : config.getString("otel.exporter.cloud-logging.compression", "gzip"); + } + + private static Duration getTimeOut(ConfigProperties config) { + Duration timeout = config.getDuration("otel.exporter.cloud-logging.metrics.timeout"); + return timeout != null ? timeout : config.getDuration("otel.exporter.cloud-logging.timeout"); + } + + private static AggregationTemporalitySelector getAggregationTemporalitySelector(ConfigProperties config) { + String temporalityStr = config.getString("otel.exporter.cloud-logging.metrics.temporality.preference"); + if (temporalityStr == null) { + return AggregationTemporalitySelector.alwaysCumulative(); + } + AggregationTemporalitySelector temporalitySelector; + switch (temporalityStr.toLowerCase(Locale.ROOT)) { + case "cumulative": + return AggregationTemporalitySelector.alwaysCumulative(); + case "delta": + return AggregationTemporalitySelector.deltaPreferred(); + case "lowmemory": + return AggregationTemporalitySelector.lowMemory(); + default: + throw new ConfigurationException("Unrecognized aggregation temporality: " + temporalityStr); + } + } + + private static DefaultAggregationSelector getDefaultAggregationSelector(ConfigProperties config) { + String defaultHistogramAggregation = + config.getString("otel.exporter.cloud-logging.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); + } + } + + @Override + public String getName() { + return "cloud-logging"; + } + + @Override + public MetricExporter createExporter(ConfigProperties config) { + List exporters = servicesProvider.apply(config) + .map(svc -> createExporter(config, svc)) + .filter(exp -> !(exp instanceof NoopMetricExporter)) + .collect(Collectors.toList()); + return MultiMetricExporter.composite(exporters, getAggregationTemporalitySelector(config), getDefaultAggregationSelector(config)); + } + + private MetricExporter createExporter(ConfigProperties config, CfService service) { + LOG.info("Creating metrics exporter for service binding " + service.getName() + " (" + service.getLabel() + ")"); + CfCredentials cfCredentials = service.getCredentials(); + CloudLoggingCredentials credentials = credentialParser.parse(cfCredentials); + if (!credentials.validate()) { + return NoopMetricExporter.getInstance(); + } + + OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.builder(); + builder.setEndpoint(credentials.getEndpoint()) + .setCompression(getCompression(config)) + .setClientTls(credentials.getClientKey(), credentials.getClientCert()) + .setTrustedCertificates(credentials.getServerCert()) + .setRetryPolicy(RetryPolicy.getDefault()) + .setAggregationTemporalitySelector(getAggregationTemporalitySelector(config)) + .setDefaultAggregationSelector(getDefaultAggregationSelector(config)); + + Duration timeOut = getTimeOut(config); + if (timeOut != null) { + builder.setTimeout(timeOut); + } + + LOG.info("Created metrics exporter for service binding " + service.getName() + " (" + service.getLabel() + ")"); + return builder.build(); + } +} 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 new file mode 100644 index 0000000..6bbcd0b --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java @@ -0,0 +1,84 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import com.sap.hcf.cf.logging.opentelemetry.agent.ext.binding.CloudLoggingServicesProvider; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +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; +import java.util.List; +import java.util.function.Function; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class CloudLoggingSpanExporterProvider implements ConfigurableSpanExporterProvider { + + private static final Logger LOG = Logger.getLogger(CloudLoggingSpanExporterProvider.class.getName()); + + private final Function> servicesProvider; + private final CloudLoggingCredentials.Parser credentialParser; + + public CloudLoggingSpanExporterProvider() { + this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get(), CloudLoggingCredentials.parser()); + } + + CloudLoggingSpanExporterProvider(Function> serviceProvider, CloudLoggingCredentials.Parser credentialParser) { + this.servicesProvider = serviceProvider; + this.credentialParser = credentialParser; + } + + private static String getCompression(ConfigProperties config) { + String compression = config.getString("otel.exporter.cloud-logging.traces.compression"); + return compression != null ? compression : config.getString("otel.exporter.cloud-logging.compression", "gzip"); + } + + private static Duration getTimeOut(ConfigProperties config) { + Duration timeout = config.getDuration("otel.exporter.cloud-logging.traces.timeout"); + return timeout != null ? timeout : config.getDuration("otel.exporter.cloud-logging.timeout"); + } + + @Override + public String getName() { + return "cloud-logging"; + } + + @Override + public SpanExporter createExporter(ConfigProperties config) { + List exporters = servicesProvider.apply(config) + .map(svc -> createExporter(config, svc)) + .filter(exp -> !(exp instanceof NoopSpanExporter)) + .collect(Collectors.toList()); + return SpanExporter.composite(exporters); + } + + private SpanExporter createExporter(ConfigProperties config, CfService service) { + LOG.info("Creating span exporter for service binding " + service.getName() + " (" + service.getLabel() + ")"); + CfCredentials cfCredentials = service.getCredentials(); + CloudLoggingCredentials credentials = credentialParser.parse(cfCredentials); + if (!credentials.validate()) { + return NoopSpanExporter.getInstance(); + } + + OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder(); + builder.setEndpoint(credentials.getEndpoint()) + .setCompression(getCompression(config)) + .setClientTls(credentials.getClientKey(), credentials.getClientCert()) + .setTrustedCertificates(credentials.getServerCert()) + .setRetryPolicy(RetryPolicy.getDefault()); + + Duration timeOut = getTimeOut(config); + if (timeOut != null) { + builder.setTimeout(timeOut); + } + + LOG.info("Created span exporter for service binding " + service.getName() + " (" + service.getLabel() + ")"); + return builder.build(); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/MultiMetricExporter.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/MultiMetricExporter.java new file mode 100644 index 0000000..47d4031 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/MultiMetricExporter.java @@ -0,0 +1,113 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +class MultiMetricExporter implements MetricExporter { + + private static final Logger LOG = Logger.getLogger(MultiMetricExporter.class.getName()); + + private final AggregationTemporalitySelector aggregationTemporalitySelector; + private final DefaultAggregationSelector defaultAggregationSelector; + private final List metricExporters; + + private MultiMetricExporter(AggregationTemporalitySelector aggregationTemporalitySelector, + DefaultAggregationSelector defaultAggregationSelector, + List metricExporters) { + this.aggregationTemporalitySelector = aggregationTemporalitySelector; + this.defaultAggregationSelector = defaultAggregationSelector; + this.metricExporters = metricExporters; + } + + static MetricExporter composite(List metricExporters, AggregationTemporalitySelector aggregationTemporalitySelector, DefaultAggregationSelector defaultAggregationSelector) { + if (metricExporters == null || metricExporters.isEmpty()) { + return NoopMetricExporter.getInstance(); + } + if (metricExporters.size() == 1) { + return metricExporters.get(0); + } + if (aggregationTemporalitySelector == null) { + aggregationTemporalitySelector = metricExporters.get(0); + } + if (defaultAggregationSelector == null) { + defaultAggregationSelector = metricExporters.get(0); + } + return new MultiMetricExporter(aggregationTemporalitySelector, defaultAggregationSelector, metricExporters); + } + + public CompletableResultCode export(Collection metrics) { + List results = new ArrayList<>(metricExporters.size()); + for (MetricExporter metricExporter : metricExporters) { + CompletableResultCode exportResult; + try { + exportResult = metricExporter.export(metrics); + results.add(exportResult); + } catch (RuntimeException e) { + LOG.log(Level.WARNING, "Exception thrown by the export.", e); + results.add(CompletableResultCode.ofFailure()); + } + } + return CompletableResultCode.ofAll(results); + } + + public CompletableResultCode flush() { + List results = new ArrayList<>(this.metricExporters.size()); + for (MetricExporter metricExporter : metricExporters) { + CompletableResultCode flushResult; + try { + flushResult = metricExporter.flush(); + results.add(flushResult); + } catch (RuntimeException e) { + LOG.log(Level.WARNING, "Exception thrown by the flush.", e); + results.add(CompletableResultCode.ofFailure()); + } + } + return CompletableResultCode.ofAll(results); + } + + public CompletableResultCode shutdown() { + List results = new ArrayList<>(this.metricExporters.size()); + for (MetricExporter metricExporter : metricExporters) { + CompletableResultCode shutdownResult; + try { + shutdownResult = metricExporter.shutdown(); + results.add(shutdownResult); + } catch (RuntimeException e) { + LOG.log(Level.WARNING, "Exception thrown by the shutdown.", e); + results.add(CompletableResultCode.ofFailure()); + } + } + return CompletableResultCode.ofAll(results); + } + + public String toString() { + return "MultiMetricExporter" + + metricExporters.stream() + .map(Object::toString) + .collect(Collectors.joining(",", "{metricsExporters=", "}")); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return aggregationTemporalitySelector.getAggregationTemporality(instrumentType); + } + + @Override + public Aggregation getDefaultAggregation(InstrumentType instrumentType) { + return defaultAggregationSelector.getDefaultAggregation(instrumentType); + } + +} \ No newline at end of file diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopLogRecordExporter.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopLogRecordExporter.java new file mode 100644 index 0000000..e83dc96 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopLogRecordExporter.java @@ -0,0 +1,30 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; + +import java.util.Collection; + +class NoopLogRecordExporter implements LogRecordExporter { + private static final LogRecordExporter INSTANCE = new NoopLogRecordExporter(); + + NoopLogRecordExporter() { + } + + static LogRecordExporter getInstance() { + return INSTANCE; + } + + public CompletableResultCode export(Collection logs) { + return CompletableResultCode.ofSuccess(); + } + + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopMetricExporter.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopMetricExporter.java new file mode 100644 index 0000000..0a53be8 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopMetricExporter.java @@ -0,0 +1,43 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; + +import java.util.Collection; + +public class NoopMetricExporter implements MetricExporter { + + private static final MetricExporter INSTANCE = new NoopMetricExporter(); + + NoopMetricExporter() { + } + + static MetricExporter getInstance() { + return INSTANCE; + } + + + @Override + public CompletableResultCode export(Collection metrics) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return AggregationTemporalitySelector.alwaysCumulative().getAggregationTemporality(instrumentType); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopSpanExporter.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopSpanExporter.java new file mode 100644 index 0000000..c3b073b --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopSpanExporter.java @@ -0,0 +1,30 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +import java.util.Collection; + +class NoopSpanExporter implements SpanExporter { + private static final SpanExporter INSTANCE = new NoopSpanExporter(); + + NoopSpanExporter() { + } + + static SpanExporter getInstance() { + return INSTANCE; + } + + public CompletableResultCode export(Collection logs) { + return CompletableResultCode.ofSuccess(); + } + + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider b/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider new file mode 100644 index 0000000..811c203 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider @@ -0,0 +1 @@ +com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.CloudLoggingLogsExporterProvider \ No newline at end of file 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 new file mode 100644 index 0000000..b44bf5c --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider @@ -0,0 +1 @@ +com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.CloudLoggingMetricsExporterProvider \ No newline at end of file diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider b/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider new file mode 100644 index 0000000..68a755a --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider @@ -0,0 +1 @@ +com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.CloudLoggingSpanExporterProvider \ 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/attributes/CloudFoundryResourceCustomizerTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/attributes/CloudFoundryResourceCustomizerTest.java index 7e86ab2..0894640 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/attributes/CloudFoundryResourceCustomizerTest.java +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/attributes/CloudFoundryResourceCustomizerTest.java @@ -45,8 +45,10 @@ public void fillsResourceFromVcapApplication() { applicationData.put("application_name", "test-application"); applicationData.put("space_name", "test-space"); applicationData.put("organization_name", "test-org"); - applicationData.put("source_id", "test-source"); + applicationData.put("application_id", "test-app-id"); applicationData.put("instance_index", 42); + applicationData.put("process_id", "test-process-id"); + applicationData.put("process_type", "test-process-type"); when(cfEnv.getApp()).thenReturn(new CfApplication(applicationData)); CloudFoundryResourceCustomizer customizer = new CloudFoundryResourceCustomizer(cfEnv); @@ -55,7 +57,9 @@ public void fillsResourceFromVcapApplication() { assertEquals("test-application", resource.getAttribute(AttributeKey.stringKey("sap.cf.app_name"))); assertEquals("test-space", resource.getAttribute(AttributeKey.stringKey("sap.cf.space_name"))); assertEquals("test-org", resource.getAttribute(AttributeKey.stringKey("sap.cf.org_name"))); - assertEquals("test-source", resource.getAttribute(AttributeKey.stringKey("sap.cf.source_id"))); + assertEquals("test-app-id", resource.getAttribute(AttributeKey.stringKey("sap.cf.source_id"))); assertEquals(42, resource.getAttribute(AttributeKey.longKey("sap.cf.instance_id")).longValue()); + assertEquals("test-process-id", resource.getAttribute(AttributeKey.stringKey("sap.cf.process.id"))); + assertEquals("test-process-type", resource.getAttribute(AttributeKey.stringKey("sap.cf.process.type"))); } } \ 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 8a92971..9492572 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 @@ -10,10 +10,7 @@ import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertTrue; public class CloudLoggingBindingPropertiesSupplierTest { @@ -22,6 +19,13 @@ public class CloudLoggingBindingPropertiesSupplierTest { 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 void assertFileContent(String expected, String filename) throws IOException { + String contents = Files.readAllLines(Paths.get(filename)) + .stream() + .collect(Collectors.joining("\n")); + assertThat(contents, is(equalTo(expected))); + } + @Test public void emptyWithoutBindings() { CfEnv cfEnv = new CfEnv("", ""); @@ -114,11 +118,4 @@ public void emptyWithoutServerCert() { Map properties = propertiesSupplier.get(); assertTrue(properties.isEmpty()); } - - private static void assertFileContent(String expected, String filename) throws IOException { - String contents = Files.readAllLines(Paths.get(filename)) - .stream() - .collect(Collectors.joining("\n")); - assertThat(contents, is(equalTo(expected))); - } } \ 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 new file mode 100644 index 0000000..701dc94 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/binding/CloudLoggingServicesProviderTest.java @@ -0,0 +1,72 @@ +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.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; + +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\"}" + + "]}"; + + @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")); + } + + @Test + 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")); + } + + @Test + 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")); + } + +} \ 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/CloudLoggingCredentialsTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentialsTest.java new file mode 100644 index 0000000..c79961a --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentialsTest.java @@ -0,0 +1,97 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.pivotal.cfenv.core.CfCredentials; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CloudLoggingCredentialsTest { + + private static final String VALID_CLIENT_CERT = "-----BEGIN CERTIFICATE-----\n" + + "Base-64-Encoded Certificate\n" + + "-----END CERTIFICATE-----\n"; + + private static final String VALID_CLIENT_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "Base-64-Encoded Private Key\n" + + "-----END PRIVATE KEY-----\n"; + + private static final String VALID_SERVER_CERT = "-----BEGIN CERTIFICATE-----\n" + + "Base-64-Encoded Server Certificate\n" + + "-----END CERTIFICATE-----\n"; + + private static final CloudLoggingCredentials.Parser PARSER = CloudLoggingCredentials.parser(); + + @NotNull + private static Map getValidCredData() { + Map credData = new HashMap<>(); + credData.put("ingest-otlp-endpoint", "test-endpoint"); + credData.put("ingest-otlp-cert", VALID_CLIENT_CERT); + credData.put("ingest-otlp-key", VALID_CLIENT_KEY); + credData.put("server-ca", VALID_SERVER_CERT); + return credData; + } + + @Test + public void validCredentials() { + Map credData = getValidCredData(); + CfCredentials cfCredentials = new CfCredentials(credData); + CloudLoggingCredentials credentials = PARSER.parse(cfCredentials); + assertTrue("Credentials should be valid", credentials.validate()); + } + + @Test + public void missingEndpoint() { + Map credData = getValidCredData(); + credData.remove("ingest-otlp-endpoint"); + CfCredentials cfCredentials = new CfCredentials(credData); + CloudLoggingCredentials credentials = PARSER.parse(cfCredentials); + assertFalse("Credentials should be invalid", credentials.validate()); + } + + @Test + public void missingClientKey() { + Map credData = getValidCredData(); + credData.remove("ingest-otlp-key"); + CfCredentials cfCredentials = new CfCredentials(credData); + CloudLoggingCredentials credentials = PARSER.parse(cfCredentials); + assertFalse("Credentials should be invalid", credentials.validate()); + } + + @Test + public void missingClientCert() { + Map credData = getValidCredData(); + credData.remove("ingest-otlp-cert"); + CfCredentials cfCredentials = new CfCredentials(credData); + CloudLoggingCredentials credentials = PARSER.parse(cfCredentials); + assertFalse("Credentials should be invalid", credentials.validate()); + } + + @Test + public void missingServerCert() { + Map credData = getValidCredData(); + credData.remove("server-ca"); + CfCredentials cfCredentials = new CfCredentials(credData); + CloudLoggingCredentials credentials = PARSER.parse(cfCredentials); + assertFalse("Credentials should be invalid", credentials.validate()); + } + + @Test + public void parsesCorrectly() { + Map credData = getValidCredData(); + CfCredentials cfCredentials = new CfCredentials(credData); + CloudLoggingCredentials credentials = PARSER.parse(cfCredentials); + assertThat(credentials.getEndpoint(), equalTo("https://test-endpoint")); + assertThat(new String(credentials.getClientCert(), StandardCharsets.UTF_8), equalTo("-----BEGIN CERTIFICATE-----\nBase-64-Encoded Certificate\n-----END CERTIFICATE-----")); + assertThat(new String(credentials.getClientKey(), StandardCharsets.UTF_8), equalTo("-----BEGIN PRIVATE KEY-----\nBase-64-Encoded Private Key\n-----END PRIVATE KEY-----")); + assertThat(new String(credentials.getServerCert(), StandardCharsets.UTF_8), equalTo("-----BEGIN CERTIFICATE-----\nBase-64-Encoded Server Certificate\n-----END CERTIFICATE-----")); + } + +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProviderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProviderTest.java new file mode 100644 index 0000000..a20afe7 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProviderTest.java @@ -0,0 +1,103 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +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.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.util.Collections; +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.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloudLoggingLogsExporterProviderTest { + + @Mock + private Function> servicesProvider; + + @Mock + private CloudLoggingCredentials.Parser credentialParser; + + @Mock + private ConfigProperties config; + + @InjectMocks + private CloudLoggingLogsExporterProvider exporterProvider; + + @Before + public void setUp() { + 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(ConfigurableLogRecordExporterProvider.class); + Stream providers = StreamSupport.stream(loader.spliterator(), false); + assertTrue(CloudLoggingLogsExporterProvider.class.getName() + " not loaded via SPI", + providers.anyMatch(p -> p instanceof CloudLoggingLogsExporterProvider)); + } + + @Test + public void registersNoopExporterWithoutBindings() { + when(servicesProvider.apply(config)).thenReturn(Stream.empty()); + LogRecordExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), containsString("Noop")); + } + + @Test + public void registersNoopExporterWithInvalidBindings() { + CfService genericCfService = new CfService(Collections.emptyMap()); + when(servicesProvider.apply(config)).thenReturn(Stream.of(genericCfService)); + CloudLoggingCredentials cloudLoggingCredentials = mock(CloudLoggingCredentials.class); + when(credentialParser.parse(any())).thenReturn(cloudLoggingCredentials); + when(cloudLoggingCredentials.validate()).thenReturn(false); + LogRecordExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), containsString("Noop")); + } + + @Test + public void registersExportersWithValidBindings() throws IOException { + CfService genericCfService = new CfService(Collections.emptyMap()); + CfService cloudLoggingService = new CfService(Collections.emptyMap()); + when(servicesProvider.apply(config)).thenReturn(Stream.of(genericCfService, cloudLoggingService)); + CloudLoggingCredentials invalidCredentials = mock(CloudLoggingCredentials.class); + when(invalidCredentials.validate()).thenReturn(false); + CloudLoggingCredentials validCredentials = mock(CloudLoggingCredentials.class); + when(validCredentials.validate()).thenReturn(true); + when(validCredentials.getEndpoint()).thenReturn("https://otlp-example.sap"); + when(validCredentials.getClientCert()).thenReturn(PEMUtil.read("certificate.pem")); + when(validCredentials.getClientKey()).thenReturn(PEMUtil.read("private.pem")); + when(validCredentials.getServerCert()).thenReturn(PEMUtil.read("certificate.pem")); + when(credentialParser.parse(any())).thenReturn(invalidCredentials).thenReturn(validCredentials); + LogRecordExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), both(containsString("OtlpGrpcLogRecordExporter")).and(containsString("https://otlp-example.sap"))); + } + +} \ 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/CloudLoggingMetricsExporterProviderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProviderTest.java new file mode 100644 index 0000000..6eccff8 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProviderTest.java @@ -0,0 +1,103 @@ +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.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.util.Collections; +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.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloudLoggingMetricsExporterProviderTest { + + @Mock + private Function> servicesProvider; + + @Mock + private CloudLoggingCredentials.Parser credentialParser; + + @Mock + private ConfigProperties config; + + @InjectMocks + private CloudLoggingMetricsExporterProvider exporterProvider; + + @Before + public void setUp() { + 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(CloudLoggingMetricsExporterProvider.class.getName() + " not loaded via SPI", + providers.anyMatch(p -> p instanceof CloudLoggingMetricsExporterProvider)); + } + + @Test + public void registersNoopExporterWithoutBindings() { + when(servicesProvider.apply(config)).thenReturn(Stream.empty()); + MetricExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), containsString("Noop")); + } + + @Test + public void registersNoopExporterWithInvalidBindings() { + CfService genericCfService = new CfService(Collections.emptyMap()); + when(servicesProvider.apply(config)).thenReturn(Stream.of(genericCfService)); + CloudLoggingCredentials cloudLoggingCredentials = mock(CloudLoggingCredentials.class); + when(credentialParser.parse(any())).thenReturn(cloudLoggingCredentials); + when(cloudLoggingCredentials.validate()).thenReturn(false); + MetricExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), containsString("Noop")); + } + + @Test + public void registersExportersWithValidBindings() throws IOException { + CfService genericCfService = new CfService(Collections.emptyMap()); + CfService cloudLoggingService = new CfService(Collections.emptyMap()); + when(servicesProvider.apply(config)).thenReturn(Stream.of(genericCfService, cloudLoggingService)); + CloudLoggingCredentials invalidCredentials = mock(CloudLoggingCredentials.class); + when(invalidCredentials.validate()).thenReturn(false); + CloudLoggingCredentials validCredentials = mock(CloudLoggingCredentials.class); + when(validCredentials.validate()).thenReturn(true); + when(validCredentials.getEndpoint()).thenReturn("https://otlp-example.sap"); + when(validCredentials.getClientCert()).thenReturn(PEMUtil.read("certificate.pem")); + when(validCredentials.getClientKey()).thenReturn(PEMUtil.read("private.pem")); + when(validCredentials.getServerCert()).thenReturn(PEMUtil.read("certificate.pem")); + when(credentialParser.parse(any())).thenReturn(invalidCredentials).thenReturn(validCredentials); + MetricExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), both(containsString("OtlpGrpcMetricExporter")).and(containsString("https://otlp-example.sap"))); + } + +} \ 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/CloudLoggingSpanExporterProviderTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProviderTest.java new file mode 100644 index 0000000..b1057c4 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProviderTest.java @@ -0,0 +1,103 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; +import io.opentelemetry.sdk.trace.export.SpanExporter; +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.invocation.InvocationOnMock; +import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.util.Collections; +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.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CloudLoggingSpanExporterProviderTest { + + @Mock + private Function> servicesProvider; + + @Mock + private CloudLoggingCredentials.Parser credentialParser; + + @Mock + private ConfigProperties config; + + @InjectMocks + private CloudLoggingSpanExporterProvider exporterProvider; + + @Before + public void setUp() { + 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(ConfigurableSpanExporterProvider.class); + Stream providers = StreamSupport.stream(loader.spliterator(), false); + assertTrue(CloudLoggingSpanExporterProvider.class.getName() + " not loaded via SPI", + providers.anyMatch(p -> p instanceof CloudLoggingSpanExporterProvider)); + } + + @Test + public void registersNoopExporterWithoutBindings() { + when(servicesProvider.apply(config)).thenReturn(Stream.empty()); + SpanExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), containsString("Noop")); + } + + @Test + public void registersNoopExporterWithInvalidBindings() { + CfService genericCfService = new CfService(Collections.emptyMap()); + when(servicesProvider.apply(config)).thenReturn(Stream.of(genericCfService)); + CloudLoggingCredentials cloudLoggingCredentials = mock(CloudLoggingCredentials.class); + when(credentialParser.parse(any())).thenReturn(cloudLoggingCredentials); + when(cloudLoggingCredentials.validate()).thenReturn(false); + SpanExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), containsString("Noop")); + } + + @Test + public void registersExportersWithValidBindings() throws IOException { + CfService genericCfService = new CfService(Collections.emptyMap()); + CfService cloudLoggingService = new CfService(Collections.emptyMap()); + when(servicesProvider.apply(config)).thenReturn(Stream.of(genericCfService, cloudLoggingService)); + CloudLoggingCredentials invalidCredentials = mock(CloudLoggingCredentials.class); + when(invalidCredentials.validate()).thenReturn(false); + CloudLoggingCredentials validCredentials = mock(CloudLoggingCredentials.class); + when(validCredentials.validate()).thenReturn(true); + when(validCredentials.getEndpoint()).thenReturn("https://otlp-example.sap"); + when(validCredentials.getClientCert()).thenReturn(PEMUtil.read("certificate.pem")); + when(validCredentials.getClientKey()).thenReturn(PEMUtil.read("private.pem")); + when(validCredentials.getServerCert()).thenReturn(PEMUtil.read("certificate.pem")); + when(credentialParser.parse(any())).thenReturn(invalidCredentials).thenReturn(validCredentials); + SpanExporter exporter = exporterProvider.createExporter(config); + assertThat(exporter, is(notNullValue())); + assertThat(exporter.toString(), both(containsString("OtlpGrpcSpanExporter")).and(containsString("https://otlp-example.sap"))); + } + +} \ 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/MultiMetricExporterTest.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/MultiMetricExporterTest.java new file mode 100644 index 0000000..778ab10 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/MultiMetricExporterTest.java @@ -0,0 +1,135 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import org.junit.Test; + +import java.util.Collection; +import java.util.Collections; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.*; + +public class MultiMetricExporterTest { + + @Test + public void createsNoopExporterOnNullExporterList() { + assertThat(MultiMetricExporter.composite(emptyList(), null, null), is(instanceOf(NoopMetricExporter.class))); + } + + @Test + public void createsNoopExporterOnEmptyExporterList() { + assertThat(MultiMetricExporter.composite(emptyList(), null, null), is(instanceOf(NoopMetricExporter.class))); + } + + @Test + public void returnsSingleExporterOnOneEntryExporterList() { + MetricExporter exporter = mock(MetricExporter.class); + assertThat(MultiMetricExporter.composite(singletonList(exporter), null, null), is(exporter)); + } + + @Test + public void delegatesExport() { + Collection metrics = Collections.emptyList(); + MetricExporter exporter1 = mock(MetricExporter.class); + when(exporter1.export(metrics)).thenReturn(CompletableResultCode.ofSuccess()); + MetricExporter exporter2 = mock(MetricExporter.class); + when(exporter2.export(metrics)).thenReturn(CompletableResultCode.ofSuccess()); + + MetricExporter metricExporter = MultiMetricExporter.composite(asList(exporter1, exporter2), null, null); + metricExporter.export(metrics); + + verify(exporter1).export(metrics); + verify(exporter2).export(metrics); + } + + + @Test + public void delegatesFlush() { + MetricExporter exporter1 = mock(MetricExporter.class); + when(exporter1.flush()).thenReturn(CompletableResultCode.ofSuccess()); + MetricExporter exporter2 = mock(MetricExporter.class); + when(exporter2.flush()).thenReturn(CompletableResultCode.ofSuccess()); + + MetricExporter metricExporter = MultiMetricExporter.composite(asList(exporter1, exporter2), null, null); + metricExporter.flush(); + + verify(exporter1).flush(); + verify(exporter2).flush(); + } + + + @Test + public void delegatesShutdwon() { + MetricExporter exporter1 = mock(MetricExporter.class); + when(exporter1.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + MetricExporter exporter2 = mock(MetricExporter.class); + when(exporter2.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); + + MetricExporter metricExporter = MultiMetricExporter.composite(asList(exporter1, exporter2), null, null); + metricExporter.shutdown(); + + verify(exporter1).shutdown(); + verify(exporter2).shutdown(); + } + + @Test + public void delegatesAggregationTemporality() { + MetricExporter exporter1 = mock(MetricExporter.class); + MetricExporter exporter2 = mock(MetricExporter.class); + + AggregationTemporalitySelector aggregationTemporalitySelector = mock(AggregationTemporalitySelector.class); + + MetricExporter metricExporter = MultiMetricExporter.composite(asList(exporter1, exporter2), aggregationTemporalitySelector, null); + metricExporter.getAggregationTemporality(InstrumentType.OBSERVABLE_GAUGE); + + verify(aggregationTemporalitySelector).getAggregationTemporality(InstrumentType.OBSERVABLE_GAUGE); + } + + @Test + public void delegatesDefaultAggregation() { + MetricExporter exporter1 = mock(MetricExporter.class); + MetricExporter exporter2 = mock(MetricExporter.class); + + DefaultAggregationSelector defaultAggregationSelector = mock(DefaultAggregationSelector.class); + + MetricExporter metricExporter = MultiMetricExporter.composite(asList(exporter1, exporter2), null, defaultAggregationSelector); + metricExporter.getDefaultAggregation(InstrumentType.OBSERVABLE_GAUGE); + + verify(defaultAggregationSelector).getDefaultAggregation(InstrumentType.OBSERVABLE_GAUGE); + } + + @Test + public void delegatesAggregationTemporalityToFirstExporterIfNoExplicitAggregation() { + MetricExporter exporter1 = mock(MetricExporter.class); + MetricExporter exporter2 = mock(MetricExporter.class); + + MetricExporter metricExporter = MultiMetricExporter.composite(asList(exporter1, exporter2), null, null); + + metricExporter.getAggregationTemporality(InstrumentType.OBSERVABLE_GAUGE); + + verify(exporter1).getAggregationTemporality(InstrumentType.OBSERVABLE_GAUGE); + } + + @Test + public void delegatesDefaultAggregationToFirstExporterIfNoExplicitAggregation() { + MetricExporter exporter1 = mock(MetricExporter.class); + MetricExporter exporter2 = mock(MetricExporter.class); + + MetricExporter metricExporter = MultiMetricExporter.composite(asList(exporter1, exporter2), null, null); + + metricExporter.getDefaultAggregation(InstrumentType.OBSERVABLE_GAUGE); + + verify(exporter1).getDefaultAggregation(InstrumentType.OBSERVABLE_GAUGE); + } + +} \ 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/PEMUtil.java b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/PEMUtil.java new file mode 100644 index 0000000..896ab21 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/PEMUtil.java @@ -0,0 +1,20 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +class PEMUtil { + @NotNull + static byte[] read(String resourceName) throws IOException { + try (InputStream is = PEMUtil.class.getClassLoader().getResourceAsStream(resourceName)) { + if (is == null) { + throw new FileNotFoundException("Resource " + resourceName + " not found."); + } + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + return reader.lines().collect(Collectors.joining("\n")).getBytes(StandardCharsets.UTF_8); + } + } +} diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/resources/certificate.pem b/cf-java-logging-support-opentelemetry-agent-extension/src/test/resources/certificate.pem new file mode 100644 index 0000000..f243434 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/resources/certificate.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDYzCCAksCFFfxHrrOeFLc3SuetPGuhqfFDBZOMA0GCSqGSIb3DQEBCwUAMG4x +CzAJBgNVBAYTAkRFMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MRAwDgYD +VQQKDAdUZXN0aW5nMQ0wCwYDVQQLDARUZXN0MSAwHgYDVQQDDBdjZi1qYXZhLWxv +Z2dpbmctc3VwcG9ydDAeFw0yMzEyMTQwNzA3MjJaFw0yNDEyMTMwNzA3MjJaMG4x +CzAJBgNVBAYTAkRFMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MRAwDgYD +VQQKDAdUZXN0aW5nMQ0wCwYDVQQLDARUZXN0MSAwHgYDVQQDDBdjZi1qYXZhLWxv +Z2dpbmctc3VwcG9ydDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK+V +L9twfgJODNYTAFgTcPK4D33DSrvxoj2KT6ddmC369xwioXTOXQtxTvzP9ypaW8D3 +rmqVQOYYj2L+2VvncEiBCXuaGb0rdaNYbOnuMdFBLbtWXlPFgVJR5js9zsxupANd +wFIYP6uFlhulyTXLBHF0tVTrhCoVdERxeF0/DUAyVid87ZVngqp+MFIZfFOKaflq +0iltypcE15nXmiIUQ3Ztf0pVgP34mRx5sE0Hz6hFObj/Yv09yq2UpuZ5gOPi86J1 +V9MimVLscsZ1oIfuPAtcAcDUc2ARaUpJHjVoHugUYKCg2pHeitAtCe9+EVdmHUTw +aqcYNPn6cQxO2c8nSFkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKB/tE6wdl2Je +zZAyj622wEFZI73m8RjADUsa4amiox1D4pC/xUvI6N7JInvH4ty98Z25ZMv9GlBx +pwb8Arq5qR+g5HvH7W3VB8Zd5ExrNpQ50F07LskFCcl/PT1YGgRCqg6IvwjLqoES +v8qlXJVWZuXuBhqjF1na1pc8M0ZUfbXVsRfJw9CnNw6z3++C9Wg1QJerWjtSXCRw +2CHCr07AiB+nf9FxVU2QMXoU1B85R+D2KSCSJBF+fKkwVdgNwXHULtx3pci43unZ +mzJ7BtnDZ1YcFUqptlzHa6HgdmwRmB7/zSazBYtuZztX5qW++0bcF19vaoJlv7Fy +XE3Ka4FlYw== +-----END CERTIFICATE----- diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/test/resources/private.pem b/cf-java-logging-support-opentelemetry-agent-extension/src/test/resources/private.pem new file mode 100644 index 0000000..7e0a814 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/test/resources/private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCvlS/bcH4CTgzW +EwBYE3DyuA99w0q78aI9ik+nXZgt+vccIqF0zl0LcU78z/cqWlvA965qlUDmGI9i +/tlb53BIgQl7mhm9K3WjWGzp7jHRQS27Vl5TxYFSUeY7Pc7MbqQDXcBSGD+rhZYb +pck1ywRxdLVU64QqFXREcXhdPw1AMlYnfO2VZ4KqfjBSGXxTimn5atIpbcqXBNeZ +15oiFEN2bX9KVYD9+JkcebBNB8+oRTm4/2L9PcqtlKbmeYDj4vOidVfTIplS7HLG +daCH7jwLXAHA1HNgEWlKSR41aB7oFGCgoNqR3orQLQnvfhFXZh1E8GqnGDT5+nEM +TtnPJ0hZAgMBAAECggEASKvxu9xqex+81lnU+Z9KT1t5Je9Pnkxbfycg4r+tPdKy +tlVrCAJlplfGfoLA5Smy8kJRUVHnI5Uku6+JXS2EXq3xs9PLNW2oaewuYAAzZE2a +1P/hWMDVTrIRHZHuZtSgU7hY4mGg8KgAwf6zMe2OMDtC1hwIfraUgcOJMubm4BuF +5gQo948q2mF0M8wYKPr3xzl9nXLaGiLXYzHKAf981qzzPxvMYnAyPwvCbSYeajE7 +sYJoxwWqAlrtSfoa8qDymMAVmRjGU/uqr7m9DlxwT79gz+rzHdvBpJEmhxwUTKzZ +MeiBaunpP/u0ilnWYaNja5GUmmrXIZghotY94/Y3RQKBgQDVOp6HWbCE/jcfCgiA +zCN0H/SmaR5mF2Hb+3WsULyl993MJbd7gpULE1XUGLkjISwapw19np4D3bWUxSY5 +9fCLTb3Yj69e7S2GW4DYHVYWLE/UkkwQ4EkSlugsRPmlKwbnER9G4N1sDtQp8l8e +mnBq8WIGe0VsjkKur2PigTFyBwKBgQDSzWvmd5Y85qW97LJBf/4IhmgShkqotwvj +beYhsJkO8coN6kW+wMK1pzt/U/JztZUijhkZDABmf8rW7Ybpc2EcVM4DLHmaowjr +O//KFf+u/buU+cvwq/seYeKlz8CPwliI6KFI6XByXZl//xlpJkmivrjoKwktWCkF +G3fKMw1anwKBgB2t1Rb90uYQBFgbq2dSArVNJV5sT0DijEp0S8K6Uie/wm2B6nIU +kO5DuLSW5F4RZfQnwGb1xwS+vWGBt3pl0x9wqLYV+dD6rlV/MwXhv3PCozHxUwke +Ts15l8NhrmXhAUr0RSg2cFt0DO/xvm+iC3e3NW+1hMpBWuK5ouuKE/nNAoGBALl8 +/hvOUImIHoEvPG78M8jGk97xAdLRNonJkz1Dynpm8XcrFHHT5cC3xVe1w1pjhjXg +uV7bu6J1gkN6wEK9Ps8SJDCQuvQBz4BFzD7C+re9FojxAK3mdtH2KdvjIbqdtQ/L +OtgQaNyJVd9V1cM7aIHmtoe8Dpgywe13jQGDyJVbAoGBAK6ys9rePjJ3uwW+CF1W +qhLopw7ZNoMyv+pOJMGVqTcnwaxZfToAA9guKrKxyusp3Zvzn/vnbTH5V0DhdRF4 +vVZq2KVbWptKIhkZkKpovsf1vQdCvLC9smgubptP80l/8HUpo0Jy4EsITJVR0BII +d1nVwQEHEp+iyFUmjkRiRmlY +-----END PRIVATE KEY----- diff --git a/sample-spring-boot/manifest-otel-javaagent.yml b/sample-spring-boot/manifest-otel-javaagent.yml index 434f2e1..f6a3500 100644 --- a/sample-spring-boot/manifest-otel-javaagent.yml +++ b/sample-spring-boot/manifest-otel-javaagent.yml @@ -15,7 +15,7 @@ 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.0.jar -Dotel.logs.exporter=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.0.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'']' services: - cls