diff --git a/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceMetricsExporterProvider.java b/cf-java-logging-support-opentelemetry-agent-extension/src/main/java/com/sap/hcf/cf/logging/opentelemetry/agent/ext/exporter/DynatraceMetricsExporterProvider.java new file mode 100644 index 0000000..9134321 --- /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,124 @@ +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.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(); + } + + OtlpGrpcMetricExporterBuilder builder = OtlpGrpcMetricExporter.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 0000000..b0d4a6c --- /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 b44bf5c..82d65fa 100644 --- a/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider +++ b/cf-java-logging-support-opentelemetry-agent-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider @@ -1 +1,2 @@ -com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.CloudLoggingMetricsExporterProvider \ No newline at end of file +com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.CloudLoggingMetricsExporterProvider +com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter.DynatraceMetricsExporterProvider \ No newline at end of file