Skip to content

Commit 0532230

Browse files
Allow metric filtering by name (#310)
* Allow metric filtering by name Implementation of a decorator pattern filtering the metrics data by the configured names. Includes are applied before excludes. Using an asterisk at the configured names ending transforms them into a prefix that is searched and filtered in the metric names. Signed-off-by: Karsten Schnitter <[email protected]> Co-authored-by: Christian Dinse <[email protected]>
1 parent 086a582 commit 0532230

File tree

5 files changed

+392
-17
lines changed

5 files changed

+392
-17
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,21 @@ java -javaagent:/path/to/opentelemetry-javaagent-<version>.jar \
142142

143143
The [OpenTelemetry Java Instrumentation project](https://github.com/open-telemetry/opentelemetry-java-instrumentation) provides detailed documentation on the configuration properties for [Logback](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-appender-1.0/javaagent) and [Log4j](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/log4j/log4j-appender-2.17/javaagent).
144144

145+
### Filtering Metrics
146+
147+
_This feature was introduced with version 4.1.0 of the extension._
148+
149+
You can filter which metrics are exported to Cloud Logging or Dynatrace by name using the following properties:
150+
151+
| Property | Description |
152+
|--------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
153+
| `otel.exporter.cloud-logging.metrics.include.names` or `otel.exporter.dynatrace.metrics.include.names` | A comma-separated list of metric names to be forwarded. This may include a wildcard "*" at the end of the name. |
154+
| `otel.exporter.cloud-logging.metrics.exclude.names` or `otel.exporter.dynatrace.metrics.exclude.names` | A comma-separated list of metric names to be rejected. This may include a wildcard "*" at the end of the name. |
155+
156+
Note, that the `include` filter is applied before the `exclude` filter.
157+
That means, if a metric matches both filters, it will be excluded.
158+
The configuration applies to both the `cloud-logging` and `dynatrace` exporters independently.
159+
145160
## Using User-Provided Service Instances
146161

147162
### SAP Cloud Logging

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
public class CloudLoggingMetricsExporterProvider implements ConfigurableMetricExporterProvider {
2929

30+
private static final String GENERIC_CONFIG_PREFIX = "otel.exporter.cloud-logging.";
31+
private static final String METRICS_CONFIG_PREFIX = "otel.exporter.cloud-logging.metrics.";
3032
private static final Logger LOG = Logger.getLogger(CloudLoggingMetricsExporterProvider.class.getName());
3133

3234
private final Function<ConfigProperties, Stream<CloudFoundryServiceInstance>> servicesProvider;
@@ -43,17 +45,17 @@ public CloudLoggingMetricsExporterProvider() {
4345
}
4446

4547
private static String getCompression(ConfigProperties config) {
46-
String compression = config.getString("otel.exporter.cloud-logging.metrics.compression");
47-
return compression != null ? compression : config.getString("otel.exporter.cloud-logging.compression", "gzip");
48+
String compression = config.getString(METRICS_CONFIG_PREFIX + "compression");
49+
return compression != null ? compression : config.getString(GENERIC_CONFIG_PREFIX + "compression", "gzip");
4850
}
4951

5052
private static Duration getTimeOut(ConfigProperties config) {
51-
Duration timeout = config.getDuration("otel.exporter.cloud-logging.metrics.timeout");
52-
return timeout != null ? timeout : config.getDuration("otel.exporter.cloud-logging.timeout");
53+
Duration timeout = config.getDuration(METRICS_CONFIG_PREFIX + "timeout");
54+
return timeout != null ? timeout : config.getDuration(GENERIC_CONFIG_PREFIX + "timeout");
5355
}
5456

5557
private static AggregationTemporalitySelector getAggregationTemporalitySelector(ConfigProperties config) {
56-
String temporalityStr = config.getString("otel.exporter.cloud-logging.metrics.temporality.preference");
58+
String temporalityStr = config.getString(METRICS_CONFIG_PREFIX + "temporality.preference");
5759
if (temporalityStr == null) {
5860
return AggregationTemporalitySelector.alwaysCumulative();
5961
}
@@ -71,8 +73,7 @@ private static AggregationTemporalitySelector getAggregationTemporalitySelector(
7173
}
7274

7375
private static DefaultAggregationSelector getDefaultAggregationSelector(ConfigProperties config) {
74-
String defaultHistogramAggregation =
75-
config.getString("otel.exporter.cloud-logging.metrics.default.histogram.aggregation");
76+
String defaultHistogramAggregation = config.getString(METRICS_CONFIG_PREFIX + "default.histogram.aggregation");
7677
if (defaultHistogramAggregation == null) {
7778
return DefaultAggregationSelector.getDefault()
7879
.with(InstrumentType.HISTOGRAM, Aggregation.defaultAggregation());
@@ -101,8 +102,11 @@ public MetricExporter createExporter(ConfigProperties config) {
101102
List<MetricExporter> exporters = servicesProvider.apply(config).map(svc -> createExporter(config, svc))
102103
.filter(exp -> !(exp instanceof NoopMetricExporter))
103104
.collect(Collectors.toList());
104-
return MultiMetricExporter.composite(exporters, getAggregationTemporalitySelector(config),
105-
getDefaultAggregationSelector(config));
105+
MetricExporter exporter = MultiMetricExporter.composite(exporters, getAggregationTemporalitySelector(config),
106+
getDefaultAggregationSelector(config));
107+
exporter = FilteringMetricExporter.wrap(exporter).withConfig(config).withPropertyPrefix(METRICS_CONFIG_PREFIX)
108+
.build();
109+
return exporter;
106110
}
107111

108112
private MetricExporter createExporter(ConfigProperties config, CloudFoundryServiceInstance service) {

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public class DynatraceMetricsExporterProvider implements ConfigurableMetricExpor
2828

2929
public static final String CRED_DYNATRACE_APIURL = "apiurl";
3030
public static final String DT_APIURL_METRICS_SUFFIX = "/v2/otlp/v1/metrics";
31+
32+
private static final String GENERIC_CONFIG_PREFIX = "otel.exporter.dynatrace.";
33+
private static final String METRICS_CONFIG_PREFIX = "otel.exporter.dynatrace.metrics.";
34+
3135
private static final Logger LOG = Logger.getLogger(DynatraceMetricsExporterProvider.class.getName());
3236
private static final AggregationTemporalitySelector ALWAYS_DELTA = instrumentType -> AggregationTemporality.DELTA;
3337
private final Function<ConfigProperties, CloudFoundryServiceInstance> serviceProvider;
@@ -41,17 +45,17 @@ public DynatraceMetricsExporterProvider(Function<ConfigProperties, CloudFoundryS
4145
}
4246

4347
private static String getCompression(ConfigProperties config) {
44-
String compression = config.getString("otel.exporter.dynatrace.metrics.compression");
45-
return compression != null ? compression : config.getString("otel.exporter.dynatrace.compression", "gzip");
48+
String compression = config.getString(METRICS_CONFIG_PREFIX + "compression");
49+
return compression != null ? compression : config.getString(GENERIC_CONFIG_PREFIX + "compression", "gzip");
4650
}
4751

4852
private static Duration getTimeOut(ConfigProperties config) {
49-
Duration timeout = config.getDuration("otel.exporter.dynatrace.metrics.timeout");
50-
return timeout != null ? timeout : config.getDuration("otel.exporter.dynatrace.timeout");
53+
Duration timeout = config.getDuration(METRICS_CONFIG_PREFIX + "timeout");
54+
return timeout != null ? timeout : config.getDuration(GENERIC_CONFIG_PREFIX + "timeout");
5155
}
5256

5357
private static AggregationTemporalitySelector getAggregationTemporalitySelector(ConfigProperties config) {
54-
String temporalityStr = config.getString("otel.exporter.dynatrace.metrics.temporality.preference");
58+
String temporalityStr = config.getString(METRICS_CONFIG_PREFIX + "temporality.preference");
5559
if (temporalityStr == null) {
5660
return ALWAYS_DELTA;
5761
}
@@ -71,8 +75,7 @@ private static AggregationTemporalitySelector getAggregationTemporalitySelector(
7175
}
7276

7377
private static DefaultAggregationSelector getDefaultAggregationSelector(ConfigProperties config) {
74-
String defaultHistogramAggregation =
75-
config.getString("otel.exporter.dynatrace.metrics.default.histogram.aggregation");
78+
String defaultHistogramAggregation = config.getString(METRICS_CONFIG_PREFIX + "default.histogram.aggregation");
7679
if (defaultHistogramAggregation == null) {
7780
return DefaultAggregationSelector.getDefault()
7881
.with(InstrumentType.HISTOGRAM, Aggregation.defaultAggregation());
@@ -147,7 +150,9 @@ public MetricExporter createExporter(ConfigProperties config) {
147150

148151
LOG.info(
149152
"Created metrics exporter for service binding " + cfService.getName() + " (" + cfService.getLabel() + ")");
150-
return builder.build();
153+
MetricExporter exporter = builder.build();
154+
return FilteringMetricExporter.wrap(exporter).withConfig(config).withPropertyPrefix(METRICS_CONFIG_PREFIX)
155+
.build();
151156
}
152157

153158
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package com.sap.hcf.cf.logging.opentelemetry.agent.ext.exporter;
2+
3+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
4+
import io.opentelemetry.sdk.common.CompletableResultCode;
5+
import io.opentelemetry.sdk.common.export.MemoryMode;
6+
import io.opentelemetry.sdk.metrics.Aggregation;
7+
import io.opentelemetry.sdk.metrics.InstrumentType;
8+
import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
9+
import io.opentelemetry.sdk.metrics.data.MetricData;
10+
import io.opentelemetry.sdk.metrics.export.MetricExporter;
11+
12+
import java.util.Collection;
13+
import java.util.HashSet;
14+
import java.util.List;
15+
import java.util.function.Predicate;
16+
17+
import static java.util.stream.Collectors.toCollection;
18+
import static java.util.stream.Collectors.toList;
19+
20+
public class FilteringMetricExporter implements MetricExporter {
21+
22+
private static final String INCLUDED_NAMES_KEY = "include.names";
23+
private static final String EXCLUDED_NAMES_KEY = "exclude.names";
24+
25+
private final MetricExporter delegate;
26+
private final Predicate<MetricData> predicate;
27+
28+
private FilteringMetricExporter(MetricExporter delegate, Predicate<MetricData> predicate) {
29+
this.delegate = delegate;
30+
this.predicate = predicate;
31+
}
32+
33+
@Override
34+
public CompletableResultCode export(Collection<MetricData> collection) {
35+
List<MetricData> filteredMetrics = collection.stream().filter(predicate).collect(toList());
36+
return delegate.export(filteredMetrics);
37+
}
38+
39+
@Override
40+
public CompletableResultCode flush() {
41+
return delegate.flush();
42+
}
43+
44+
@Override
45+
public CompletableResultCode shutdown() {
46+
return delegate.shutdown();
47+
}
48+
49+
@Override
50+
public void close() {
51+
delegate.close();
52+
}
53+
54+
@Override
55+
public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) {
56+
return delegate.getAggregationTemporality(instrumentType);
57+
}
58+
59+
@Override
60+
public Aggregation getDefaultAggregation(InstrumentType instrumentType) {
61+
return delegate.getDefaultAggregation(instrumentType);
62+
}
63+
64+
@Override
65+
public MemoryMode getMemoryMode() {
66+
return delegate.getMemoryMode();
67+
}
68+
69+
public static Builder wrap(MetricExporter delegate) {
70+
return new Builder(delegate);
71+
}
72+
73+
public static class Builder {
74+
75+
private final MetricExporter delegate;
76+
private ConfigProperties config;
77+
private String prefix = "";
78+
79+
public Builder(MetricExporter delegate) {
80+
this.delegate = delegate;
81+
}
82+
83+
public Builder withConfig(ConfigProperties config) {
84+
this.config = config;
85+
return this;
86+
}
87+
88+
public Builder withPropertyPrefix(String prefix) {
89+
this.prefix = prefix.endsWith(".") ? prefix : prefix + ".";
90+
return this;
91+
}
92+
93+
public MetricExporter build() {
94+
if (config == null) {
95+
return delegate;
96+
}
97+
98+
List<String> includedNames = config.getList(prefix + INCLUDED_NAMES_KEY);
99+
List<String> excludedNames = config.getList(prefix + EXCLUDED_NAMES_KEY);
100+
if (includedNames.isEmpty() && excludedNames.isEmpty()) {
101+
return delegate;
102+
}
103+
104+
Predicate<MetricData> predicate = metricData -> true;
105+
predicate = addInclusions(predicate, includedNames);
106+
predicate = addExclusions(predicate, excludedNames);
107+
return new FilteringMetricExporter(delegate, predicate);
108+
}
109+
110+
private static Predicate<MetricData> addInclusions(Predicate<MetricData> predicate,
111+
List<String> includedNames) {
112+
if (includedNames.isEmpty()) {
113+
return predicate;
114+
}
115+
116+
HashSet<String> names = getNames(includedNames);
117+
if (!names.isEmpty()) {
118+
predicate = predicate.and(metricData -> names.contains(metricData.getName()));
119+
}
120+
List<String> prefixes = getPrefixes(includedNames);
121+
if (!prefixes.isEmpty()) {
122+
predicate = predicate.and(
123+
metricData -> prefixes.stream().anyMatch(p -> metricData.getName().startsWith(p)));
124+
}
125+
return predicate;
126+
}
127+
128+
private static Predicate<MetricData> addExclusions(Predicate<MetricData> predicate,
129+
List<String> excludedNames) {
130+
if (excludedNames.isEmpty()) {
131+
return predicate;
132+
}
133+
134+
HashSet<String> names = getNames(excludedNames);
135+
if (!names.isEmpty()) {
136+
predicate = predicate.and(metricData -> !names.contains(metricData.getName()));
137+
}
138+
List<String> prefixes = getPrefixes(excludedNames);
139+
if (!prefixes.isEmpty()) {
140+
predicate = predicate.and(
141+
metricData -> prefixes.stream().anyMatch(p -> !metricData.getName().startsWith(p)));
142+
}
143+
return predicate;
144+
}
145+
146+
private static HashSet<String> getNames(List<String> names) {
147+
return names.stream().filter(n -> !n.endsWith("*")).collect(toCollection(HashSet::new));
148+
}
149+
150+
private static List<String> getPrefixes(List<String> names) {
151+
return names.stream().filter(n -> n.endsWith("*")).map(n -> n.substring(0, n.length() - 1))
152+
.collect(toList());
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)