From 4fed8c3fd5532d6778d3276e55e32e5ed4343d8c Mon Sep 17 00:00:00 2001 From: Karsten Schnitter Date: Mon, 11 Dec 2023 17:13:40 +0100 Subject: [PATCH 1/3] Add OpenTelemetry Signal Exporters for Cloud Logging Register signal exporters for logs, metrics and traces, that forward signals to SAP Cloud Logging, if an appropriate service binding is found or do a non operation otherwise. This allows developers to explicitly configure the export, e.g. by `otel.logs.exporter=cloud-logging`. Additional exporters can be added with comma-separated labels. Signed-off-by: Karsten Schnitter --- .../dependency-reduced-pom.xml | 12 +- .../pom.xml | 9 +- ...oggingConfigurationCustomizerProvider.java | 2 + .../ext/exporter/CloudLoggingCredentials.java | 87 ++++++++++++ .../CloudLoggingLogsExporterProvider.java | 80 +++++++++++ .../CloudLoggingMetricsExporterProvider.java | 129 ++++++++++++++++++ .../CloudLoggingServicesProvider.java | 49 +++++++ .../CloudLoggingSpanExporterProvider.java | 83 +++++++++++ .../ext/exporter/MultiMetricExporter.java | 97 +++++++++++++ .../ext/exporter/NoopLogRecordExporter.java | 30 ++++ .../ext/exporter/NoopMetricExporter.java | 46 +++++++ .../agent/ext/exporter/NoopSpanExporter.java | 32 +++++ ...logs.ConfigurableLogRecordExporterProvider | 1 + ...metrics.ConfigurableMetricExporterProvider | 1 + ...pi.traces.ConfigurableSpanExporterProvider | 1 + 15 files changed, 657 insertions(+), 2 deletions(-) create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentials.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingLogsExporterProvider.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingMetricsExporterProvider.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingServicesProvider.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingSpanExporterProvider.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/MultiMetricExporter.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopLogRecordExporter.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopMetricExporter.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/NoopSpanExporter.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider 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 4af6f8dd..4abce9af 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 a4be8a84..6abaff7e 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 9cad03ad..b02d6577 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/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 00000000..a023d3da --- /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,87 @@ +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 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 parseCredentials(CfCredentials cfCredentials) { + CloudLoggingCredentials parsed = new CloudLoggingCredentials(); + parsed.endpoint = CLOUD_LOGGING_ENDPOINT_PREFIX + cfCredentials.getString(CRED_OTLP_ENDPOINT); + 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; + } + + 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; + } +} 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 00000000..33159b28 --- /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,80 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +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.Objects; +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; + + public CloudLoggingLogsExporterProvider() { + this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get()); + } + + public CloudLoggingLogsExporterProvider(Function> serviceProvider) { + this.servicesProvider = serviceProvider; + } + + 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 = CloudLoggingCredentials.parseCredentials(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 00000000..4f4da259 --- /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,129 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +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.CfEnv; +import io.pivotal.cfenv.core.CfService; + +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +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; + + public CloudLoggingMetricsExporterProvider() { + this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get()); + } + + public CloudLoggingMetricsExporterProvider(Function> serviceProvider) { + this.servicesProvider = serviceProvider; + } + + 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 new MultiMetricExporter(getAggregationTemporalitySelector(config), getDefaultAggregationSelector(config), exporters); + } + + private MetricExporter createExporter(ConfigProperties config, CfService service) { + LOG.info("Creating metrics exporter for service binding " + service.getName() + " (" + service.getLabel() + ")"); + CloudLoggingCredentials credentials = CloudLoggingCredentials.parseCredentials(service.getCredentials()); + 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/CloudLoggingServicesProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingServicesProvider.java new file mode 100644 index 00000000..1e35c2b2 --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingServicesProvider.java @@ -0,0 +1,49 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +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; + +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/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 00000000..a58af2d1 --- /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,83 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; +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.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.pivotal.cfenv.core.CfEnv; +import io.pivotal.cfenv.core.CfService; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; +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; + + public CloudLoggingSpanExporterProvider() { + this(config -> new CloudLoggingServicesProvider(config, new CfEnv()).get()); + } + + public CloudLoggingSpanExporterProvider(Function> serviceProvider) { + this.servicesProvider = serviceProvider; + } + + 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() + ")"); + CloudLoggingCredentials credentials = CloudLoggingCredentials.parseCredentials(service.getCredentials()); + 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 00000000..9074f288 --- /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,97 @@ +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; + + MultiMetricExporter(AggregationTemporalitySelector aggregationTemporalitySelector, + DefaultAggregationSelector defaultAggregationSelector, + List metricExporters) { + this.aggregationTemporalitySelector = aggregationTemporalitySelector; + this.defaultAggregationSelector = defaultAggregationSelector; + this.metricExporters = 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 00000000..e83dc966 --- /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 00000000..862fed5a --- /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,46 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +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; +import java.util.logging.Level; +import java.util.logging.Logger; + +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 00000000..4a2378c6 --- /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,32 @@ +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 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 00000000..811c203e --- /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 00000000..b44bf5c0 --- /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 00000000..68a755a1 --- /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 From d2770a5818ec05c6219d051e63fd296d4990e91c Mon Sep 17 00:00:00 2001 From: Karsten Schnitter Date: Tue, 12 Dec 2023 14:53:23 +0100 Subject: [PATCH 2/3] Add Unit Tests Signed-off-by: Karsten Schnitter --- .../exporter/CloudLoggingCredentialsTest.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/test/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/CloudLoggingCredentialsTest.java 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 00000000..062df8a8 --- /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,98 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.pivotal.cfenv.core.CfCredentials; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; +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.*; +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"; + + @Test + public void validCredentials() { + Map credData = getValidCredData(); + CfCredentials cfCredentials = new CfCredentials(credData); + CloudLoggingCredentials credentials = CloudLoggingCredentials.parseCredentials(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 = CloudLoggingCredentials.parseCredentials(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 = CloudLoggingCredentials.parseCredentials(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 = CloudLoggingCredentials.parseCredentials(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 = CloudLoggingCredentials.parseCredentials(cfCredentials); + assertFalse("Credentials should be invalid", credentials.validate()); + } + + @Test + public void parsesCorrectly() { + Map credData = getValidCredData(); + CfCredentials cfCredentials = new CfCredentials(credData); + CloudLoggingCredentials credentials = CloudLoggingCredentials.parseCredentials(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-----")); + } + + @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; + } + +} From 4892920a85e4aab65dba9549feefd25f77358bb0 Mon Sep 17 00:00:00 2001 From: Karsten Schnitter Date: Mon, 11 Dec 2023 17:15:58 +0100 Subject: [PATCH 3/3] Add OpenTelemetry Metrics Exporters for Dynatrace Register a metrics exporters, that forward data to Dynatrace, if an appropriate service binding is found or do a non-operation otherwise. This allows developers to explicitly configure the export, e.g. by `otel.metrics.exporter=dynatrace`. Additional exporters can be added with comma-separated labels. Signed-off-by: Karsten Schnitter --- .../DynatraceMetricsExporterProvider.java | 126 ++++++++++++++++++ .../exporter/DynatraceServiceProvider.java | 46 +++++++ ...metrics.ConfigurableMetricExporterProvider | 3 +- 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceMetricsExporterProvider.java create mode 100644 cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceServiceProvider.java 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 00000000..98dc8020 --- /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,126 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder; +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.CfEnv; +import io.pivotal.cfenv.core.CfService; + +import java.time.Duration; +import java.util.Locale; +import java.util.function.Function; +import java.util.logging.Logger; + +import static io.opentelemetry.sdk.metrics.Aggregation.explicitBucketHistogram; + +public class DynatraceMetricsExporterProvider implements ConfigurableMetricExporterProvider { + + private static final Logger LOG = Logger.getLogger(DynatraceMetricsExporterProvider.class.getName()); + public static final String CRED_DYNATRACE_APIURL = "apiurl"; + public static final String DT_APIURL_METRICS_SUFFIX = "/v2/otlp/v1/metrics"; + + private final Function serviceProvider; + + public DynatraceMetricsExporterProvider() { + this(config -> new DynatraceServiceProvider(config, new CfEnv()).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); + } + } + + @Override + public String getName() { + return "dynatrace"; + } + + + private static boolean isBlank(String text) { + return text == null || text.trim().isEmpty(); + } + + @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(); + builder.setEndpoint(apiUrl + DT_APIURL_METRICS_SUFFIX) + .setCompression(getCompression(config)) + .addHeader("Authorization", "ApiToken " + 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/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceServiceProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceServiceProvider.java new file mode 100644 index 00000000..b0d4a6ce --- /dev/null +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceServiceProvider.java @@ -0,0 +1,46 @@ +package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter; + +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.Stream; + +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, CfEnv cfEnv) { + String userProvidedLabel = getUserProvidedLabel(config); + String dynatraceLabel = getDynatraceLabel(config); + String dynatraceTag = getDynatraceTag(config); + List userProvided = cfEnv.findServicesByLabel(userProvidedLabel); + List managed = cfEnv.findServicesByLabel(dynatraceLabel); + this.service = Stream.concat(userProvided.stream(), managed.stream()) + .filter(svc -> svc.existsByTagIgnoreCase(dynatraceTag)).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/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 b44bf5c0..82d65fa4 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