File based view configuration (#4163)
* Add experimental view config module * Rename view-config to metric-incubator * Switch naming from camelCase to snake_case * Extend with attribute key filter * Wire up to autoconfiguration * Use snakeyaml instead of jackson * PR feedback * PR feedback
This commit is contained in:
parent
71351a26b5
commit
4a67130738
|
|
@ -21,7 +21,8 @@ val DEPENDENCY_BOMS = listOf(
|
|||
"io.zipkin.brave:brave-bom:5.13.7",
|
||||
"io.zipkin.reporter2:zipkin-reporter-bom:2.16.3",
|
||||
"org.junit:junit-bom:5.8.2",
|
||||
"org.testcontainers:testcontainers-bom:1.16.3"
|
||||
"org.testcontainers:testcontainers-bom:1.16.3",
|
||||
"org.yaml:snakeyaml:1.30"
|
||||
)
|
||||
|
||||
val DEPENDENCY_SETS = listOf(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
# OpenTelemetry Metric Incubator
|
||||
|
||||
[![Javadocs][javadoc-image]][javadoc-url]
|
||||
|
||||
This artifact contains experimental code related to metrics.
|
||||
|
||||
## View File Configuration
|
||||
|
||||
Adds support for file based YAML configuration of Metric SDK Views.
|
||||
|
||||
For example, suppose `/Users/user123/view.yaml` has the following content:
|
||||
|
||||
```yaml
|
||||
- selector:
|
||||
instrument_name: my-instrument
|
||||
instrument_type: COUNTER
|
||||
meter_name: my-meter
|
||||
meter_version: 1.0.0
|
||||
meter_schema_url: http://example.com
|
||||
view:
|
||||
name: new-instrument-name
|
||||
description: new-description
|
||||
aggregation: histogram
|
||||
attribute_keys:
|
||||
- foo
|
||||
- bar
|
||||
```
|
||||
|
||||
The equivalent view configuration would be:
|
||||
|
||||
```
|
||||
SdkMeterProvider.builder()
|
||||
.registerView(
|
||||
InstrumentSelector.builder()
|
||||
.setInstrumentName("my-instrument")
|
||||
.setInstrumentType(InstrumentType.COUNTER)
|
||||
.setMeterSelector(
|
||||
MeterSelector.builder()
|
||||
.setName("my-meter")
|
||||
.setVersion("1.0.0")
|
||||
.setSchemaUrl("http://example.com")
|
||||
.build())
|
||||
.build(),
|
||||
View.builder()
|
||||
.setName("new-instrument")
|
||||
.setDescription("new-description")
|
||||
.setAggregation(Aggregation.histogram())
|
||||
.filterAttributes(key -> new HashSet<>(Arrays.asList("foo", "bar")).contains(key))
|
||||
.build());
|
||||
```
|
||||
|
||||
If using [autoconfigure](../autoconfigure) with this artifact on your classpath, it will automatically load a list of view config files specified via environment variable or system property:
|
||||
|
||||
| System property | Environment variable | Purpose |
|
||||
|---------------------------------------|---------------------------------------|------------------------------------------------------|
|
||||
| otel.experimental.metrics.view-config | OTEL_EXPERIMENTAL_METRICS_VIEW_CONFIG | List of files containing view configuration YAML [1] |
|
||||
|
||||
**[1]** In addition to absolute paths, resources on the classpath packaged with a jar can be loaded.
|
||||
For example, `otel.experimental.metrics.view-config=classpath:/my-view.yaml` loads the
|
||||
resource `/my-view.yaml`.
|
||||
|
||||
If not using autoconfigure, a file can be used to configure views as follows:
|
||||
|
||||
```
|
||||
SdkMeterProviderBuilder builder = SdkMeterProvider.builder();
|
||||
try (FileInputStream fileInputStream = new FileInputStream("/Users/user123/view.yaml")) {
|
||||
ViewConfig.registerViews(builder, fileInputStream);
|
||||
}
|
||||
```
|
||||
|
||||
Notes on usage:
|
||||
|
||||
- Many view configurations can live in one file. The YAML is parsed as an array of view
|
||||
configurations.
|
||||
- At least one selection field is required, but including all is not necessary. Any omitted fields
|
||||
will result in the default from `InstrumentSelector` being used.
|
||||
- At least one view field is required, but including all is not required. Any omitted fields will
|
||||
result in the default from `View` being used.
|
||||
- Advanced selection criteria, like regular expressions, is not yet supported.
|
||||
|
||||
[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-sdk-extension-metric-incubator
|
||||
|
||||
[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-extension-metric-incubator
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
plugins {
|
||||
id("otel.java-conventions")
|
||||
id("otel.publish-conventions")
|
||||
}
|
||||
|
||||
description = "OpenTelemetry SDK Metric Incubator"
|
||||
otelJava.moduleName.set("io.opentelemetry.sdk.extension.metric.incubator")
|
||||
|
||||
dependencies {
|
||||
implementation(project(":sdk:metrics"))
|
||||
implementation(project(":sdk-extensions:autoconfigure-spi"))
|
||||
|
||||
implementation("org.yaml:snakeyaml")
|
||||
|
||||
annotationProcessor("com.google.auto.value:auto-value")
|
||||
|
||||
testImplementation(project(":sdk:testing"))
|
||||
testImplementation(project(":sdk-extensions:autoconfigure"))
|
||||
testImplementation(project(":sdk:metrics-testing"))
|
||||
|
||||
testImplementation("com.google.guava:guava")
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
otel.release=alpha
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.viewconfig;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import io.opentelemetry.sdk.metrics.common.InstrumentType;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@AutoValue
|
||||
abstract class SelectorSpecification {
|
||||
|
||||
static AutoValue_SelectorSpecification.Builder builder() {
|
||||
return new AutoValue_SelectorSpecification.Builder();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
abstract String getInstrumentName();
|
||||
|
||||
@Nullable
|
||||
abstract InstrumentType getInstrumentType();
|
||||
|
||||
@Nullable
|
||||
abstract String getMeterName();
|
||||
|
||||
@Nullable
|
||||
abstract String getMeterVersion();
|
||||
|
||||
@Nullable
|
||||
abstract String getMeterSchemaUrl();
|
||||
|
||||
@AutoValue.Builder
|
||||
interface Builder {
|
||||
Builder instrumentName(@Nullable String instrumentName);
|
||||
|
||||
Builder instrumentType(@Nullable InstrumentType instrumentType);
|
||||
|
||||
Builder meterName(@Nullable String meterName);
|
||||
|
||||
Builder meterVersion(@Nullable String meterVersion);
|
||||
|
||||
Builder meterSchemaUrl(@Nullable String meterSchemaUrl);
|
||||
|
||||
SelectorSpecification build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.viewconfig;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
|
||||
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
|
||||
import io.opentelemetry.sdk.metrics.common.InstrumentType;
|
||||
import io.opentelemetry.sdk.metrics.view.Aggregation;
|
||||
import io.opentelemetry.sdk.metrics.view.InstrumentSelector;
|
||||
import io.opentelemetry.sdk.metrics.view.MeterSelector;
|
||||
import io.opentelemetry.sdk.metrics.view.View;
|
||||
import io.opentelemetry.sdk.metrics.view.ViewBuilder;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/**
|
||||
* Enables file based YAML configuration of Metric SDK {@link View}s.
|
||||
*
|
||||
* <p>For example, a YAML file with the following content:
|
||||
*
|
||||
* <pre>
|
||||
* - selector:
|
||||
* instrument_name: my-instrument
|
||||
* instrument_type: COUNTER
|
||||
* meter_name: my-meter
|
||||
* meter_version: 1.0.0
|
||||
* meter_schema_url: http://example.com
|
||||
* view:
|
||||
* name: new-instrument-name
|
||||
* description: new-description
|
||||
* aggregation: histogram
|
||||
* attribute_keys:
|
||||
* - foo
|
||||
* - bar
|
||||
* </pre>
|
||||
*
|
||||
* <p>Is equivalent to the following configuration:
|
||||
*
|
||||
* <pre>{@code
|
||||
* SdkMeterProvider.builder()
|
||||
* .registerView(
|
||||
* InstrumentSelector.builder()
|
||||
* .setInstrumentName("my-instrument")
|
||||
* .setInstrumentType(InstrumentType.COUNTER)
|
||||
* .setMeterSelector(
|
||||
* MeterSelector.builder()
|
||||
* .setName("my-meter")
|
||||
* .setVersion("1.0.0")
|
||||
* .setSchemaUrl("http://example.com")
|
||||
* .build())
|
||||
* .build(),
|
||||
* View.builder()
|
||||
* .setName("new-instrument")
|
||||
* .setDescription("new-description")
|
||||
* .setAggregation(Aggregation.histogram())
|
||||
* .filterAttributes(key -> new HashSet<>(Arrays.asList("foo", "bar")).contains(key))
|
||||
* .build());
|
||||
* }</pre>
|
||||
*/
|
||||
public final class ViewConfig {
|
||||
|
||||
private ViewConfig() {}
|
||||
|
||||
/**
|
||||
* Load the view configuration YAML from the {@code inputStream} and apply it to the {@link
|
||||
* SdkMeterProviderBuilder}.
|
||||
*
|
||||
* @throws ConfigurationException if unable to interpret {@code inputStream} contents
|
||||
*/
|
||||
public static void registerViews(
|
||||
SdkMeterProviderBuilder meterProviderBuilder, InputStream inputStream) {
|
||||
List<ViewConfigSpecification> viewConfigSpecs = loadViewConfig(inputStream);
|
||||
|
||||
for (ViewConfigSpecification viewConfigSpec : viewConfigSpecs) {
|
||||
meterProviderBuilder.registerView(
|
||||
toInstrumentSelector(viewConfigSpec.getSelectorSpecification()),
|
||||
toView(viewConfigSpec.getViewSpecification()));
|
||||
}
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
@SuppressWarnings("unchecked")
|
||||
static List<ViewConfigSpecification> loadViewConfig(InputStream inputStream) {
|
||||
Yaml yaml = new Yaml();
|
||||
try {
|
||||
List<ViewConfigSpecification> result = new ArrayList<>();
|
||||
List<Map<String, Object>> viewConfigs = yaml.load(inputStream);
|
||||
for (Map<String, Object> viewConfigSpecMap : viewConfigs) {
|
||||
Map<String, Object> selectorSpecMap =
|
||||
requireNonNull(
|
||||
getAsType(viewConfigSpecMap, "selector", Map.class), "selector is required");
|
||||
Map<String, Object> viewSpecMap =
|
||||
requireNonNull(getAsType(viewConfigSpecMap, "view", Map.class), "view is required");
|
||||
|
||||
InstrumentType instrumentType =
|
||||
Optional.ofNullable(getAsType(selectorSpecMap, "instrument_type", String.class))
|
||||
.map(InstrumentType::valueOf)
|
||||
.orElse(null);
|
||||
List<String> attributeKeys =
|
||||
Optional.ofNullable(
|
||||
((List<Object>) getAsType(viewSpecMap, "attribute_keys", List.class)))
|
||||
.map(objects -> objects.stream().map(String::valueOf).collect(toList()))
|
||||
.orElse(null);
|
||||
|
||||
result.add(
|
||||
ViewConfigSpecification.builder()
|
||||
.selectorSpecification(
|
||||
SelectorSpecification.builder()
|
||||
.instrumentName(getAsType(selectorSpecMap, "instrument_name", String.class))
|
||||
.instrumentType(instrumentType)
|
||||
.meterName(getAsType(selectorSpecMap, "meter_name", String.class))
|
||||
.meterVersion(getAsType(selectorSpecMap, "meter_version", String.class))
|
||||
.meterSchemaUrl(
|
||||
getAsType(selectorSpecMap, "meter_schema_url", String.class))
|
||||
.build())
|
||||
.viewSpecification(
|
||||
ViewSpecification.builder()
|
||||
.name(getAsType(viewSpecMap, "name", String.class))
|
||||
.description(getAsType(viewSpecMap, "description", String.class))
|
||||
.aggregation(getAsType(viewSpecMap, "aggregation", String.class))
|
||||
.attributeKeys(attributeKeys)
|
||||
.build())
|
||||
.build());
|
||||
}
|
||||
return result;
|
||||
} catch (RuntimeException e) {
|
||||
throw new ConfigurationException("Failed to parse view config", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T getAsType(Map<String, Object> map, String key, Class<T> type) {
|
||||
Object value = map.get(key);
|
||||
if (value != null && !type.isInstance(value)) {
|
||||
throw new IllegalStateException(
|
||||
"Expected "
|
||||
+ key
|
||||
+ " to be type "
|
||||
+ type.getName()
|
||||
+ " but was "
|
||||
+ value.getClass().getName());
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
static View toView(ViewSpecification viewSpec) {
|
||||
ViewBuilder builder = View.builder();
|
||||
String name = viewSpec.getName();
|
||||
if (name != null) {
|
||||
builder.setName(name);
|
||||
}
|
||||
String description = viewSpec.getDescription();
|
||||
if (description != null) {
|
||||
builder.setDescription(description);
|
||||
}
|
||||
String aggregation = viewSpec.getAggregation();
|
||||
if (aggregation != null) {
|
||||
builder.setAggregation(toAggregation(aggregation));
|
||||
}
|
||||
List<String> attributeKeys = viewSpec.getAttributeKeys();
|
||||
if (attributeKeys != null) {
|
||||
Set<String> keySet = new HashSet<>(attributeKeys);
|
||||
builder.filterAttributes(keySet::contains);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
static Aggregation toAggregation(String aggregation) {
|
||||
switch (aggregation) {
|
||||
case "sum":
|
||||
return Aggregation.sum();
|
||||
case "last_value":
|
||||
return Aggregation.lastValue();
|
||||
case "drop":
|
||||
return Aggregation.drop();
|
||||
case "histogram":
|
||||
return Aggregation.explicitBucketHistogram();
|
||||
default:
|
||||
throw new ConfigurationException("Unrecognized aggregation " + aggregation);
|
||||
}
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
static InstrumentSelector toInstrumentSelector(SelectorSpecification selectorSpec) {
|
||||
InstrumentSelector.Builder builder = InstrumentSelector.builder();
|
||||
String instrumentName = selectorSpec.getInstrumentName();
|
||||
if (instrumentName != null) {
|
||||
builder.setInstrumentName(instrumentName);
|
||||
}
|
||||
InstrumentType instrumentType = selectorSpec.getInstrumentType();
|
||||
if (instrumentType != null) {
|
||||
builder.setInstrumentType(instrumentType);
|
||||
}
|
||||
|
||||
MeterSelector.Builder meterBuilder = MeterSelector.builder();
|
||||
String meterName = selectorSpec.getMeterName();
|
||||
if (meterName != null) {
|
||||
meterBuilder.setName(meterName);
|
||||
}
|
||||
String meterVersion = selectorSpec.getMeterVersion();
|
||||
if (meterVersion != null) {
|
||||
meterBuilder.setVersion(meterVersion);
|
||||
}
|
||||
String meterSchemaUrl = selectorSpec.getMeterSchemaUrl();
|
||||
if (meterSchemaUrl != null) {
|
||||
meterBuilder.setSchemaUrl(meterSchemaUrl);
|
||||
}
|
||||
builder.setMeterSelector(meterBuilder.build());
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.viewconfig;
|
||||
|
||||
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
|
||||
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
|
||||
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
|
||||
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
|
||||
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
/** SPI implementation for loading view configuration YAML. */
|
||||
public final class ViewConfigCustomizer implements AutoConfigurationCustomizerProvider {
|
||||
|
||||
@Override
|
||||
public void customize(AutoConfigurationCustomizer autoConfiguration) {
|
||||
autoConfiguration.addMeterProviderCustomizer(ViewConfigCustomizer::customizeMeterProvider);
|
||||
}
|
||||
|
||||
// Visible for testing
|
||||
static SdkMeterProviderBuilder customizeMeterProvider(
|
||||
SdkMeterProviderBuilder meterProviderBuilder, ConfigProperties configProperties) {
|
||||
List<String> configFileLocations =
|
||||
configProperties.getList("otel.experimental.metrics.view.config");
|
||||
for (String configFileLocation : configFileLocations) {
|
||||
if (configFileLocation.startsWith("classpath:")) {
|
||||
String classpathLocation = configFileLocation.substring("classpath:".length());
|
||||
try (InputStream inputStream =
|
||||
ViewConfigCustomizer.class.getResourceAsStream(classpathLocation)) {
|
||||
if (inputStream == null) {
|
||||
throw new ConfigurationException(
|
||||
"Resource "
|
||||
+ classpathLocation
|
||||
+ " not found on classpath of classloader "
|
||||
+ ViewConfigCustomizer.class.getClassLoader().getClass().getName());
|
||||
}
|
||||
ViewConfig.registerViews(meterProviderBuilder, inputStream);
|
||||
} catch (IOException e) {
|
||||
throw new ConfigurationException(
|
||||
"An error occurred reading view config resource on classpath: " + classpathLocation,
|
||||
e);
|
||||
}
|
||||
} else {
|
||||
try (FileInputStream fileInputStream = new FileInputStream(configFileLocation)) {
|
||||
ViewConfig.registerViews(meterProviderBuilder, fileInputStream);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new ConfigurationException("View config file not found: " + configFileLocation, e);
|
||||
} catch (IOException e) {
|
||||
throw new ConfigurationException(
|
||||
"An error occurred reading view config file: " + configFileLocation, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return meterProviderBuilder;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.viewconfig;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
|
||||
@AutoValue
|
||||
abstract class ViewConfigSpecification {
|
||||
|
||||
static AutoValue_ViewConfigSpecification.Builder builder() {
|
||||
return new AutoValue_ViewConfigSpecification.Builder();
|
||||
}
|
||||
|
||||
abstract SelectorSpecification getSelectorSpecification();
|
||||
|
||||
abstract ViewSpecification getViewSpecification();
|
||||
|
||||
@AutoValue.Builder
|
||||
interface Builder {
|
||||
Builder selectorSpecification(SelectorSpecification selectorSpecification);
|
||||
|
||||
Builder viewSpecification(ViewSpecification viewSpecification);
|
||||
|
||||
ViewConfigSpecification build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.viewconfig;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@AutoValue
|
||||
abstract class ViewSpecification {
|
||||
|
||||
static AutoValue_ViewSpecification.Builder builder() {
|
||||
return new AutoValue_ViewSpecification.Builder();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
abstract String getName();
|
||||
|
||||
@Nullable
|
||||
abstract String getDescription();
|
||||
|
||||
@Nullable
|
||||
abstract String getAggregation();
|
||||
|
||||
@Nullable
|
||||
abstract List<String> getAttributeKeys();
|
||||
|
||||
@AutoValue.Builder
|
||||
interface Builder {
|
||||
Builder name(@Nullable String name);
|
||||
|
||||
Builder description(@Nullable String description);
|
||||
|
||||
Builder aggregation(@Nullable String aggregation);
|
||||
|
||||
Builder attributeKeys(@Nullable List<String> attributeKeys);
|
||||
|
||||
ViewSpecification build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
io.opentelemetry.sdk.viewconfig.ViewConfigCustomizer
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.viewconfig;
|
||||
|
||||
import static io.opentelemetry.sdk.testing.assertj.MetricAssertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
|
||||
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
|
||||
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
|
||||
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ViewConfigCustomizerTest {
|
||||
|
||||
@Test
|
||||
void customizeMeterProvider_Spi() {
|
||||
InMemoryMetricReader reader = InMemoryMetricReader.create();
|
||||
AutoConfiguredOpenTelemetrySdk.builder()
|
||||
.setResultAsGlobal(false)
|
||||
.addPropertiesSupplier(
|
||||
() -> {
|
||||
return ImmutableMap.of(
|
||||
"otel.traces.exporter",
|
||||
"none",
|
||||
"otel.experimental.metrics.view.config",
|
||||
"classpath:/view-config-customizer-test.yaml");
|
||||
})
|
||||
.addMeterProviderCustomizer(
|
||||
(meterProviderBuilder, configProperties) ->
|
||||
meterProviderBuilder.registerMetricReader(reader))
|
||||
.build()
|
||||
.getOpenTelemetrySdk()
|
||||
.getSdkMeterProvider()
|
||||
.get("test-meter")
|
||||
.counterBuilder("counter")
|
||||
.buildWithCallback(
|
||||
callback -> {
|
||||
// Attributes with keys baz and qux should be filtered out
|
||||
callback.record(
|
||||
1,
|
||||
Attributes.builder()
|
||||
.put("foo", "val")
|
||||
.put("bar", "val")
|
||||
.put("baz", "val")
|
||||
.put("qux", "val")
|
||||
.build());
|
||||
});
|
||||
|
||||
assertThat(reader.collectAllMetrics())
|
||||
.satisfiesExactly(
|
||||
metricData -> {
|
||||
assertThat(metricData)
|
||||
.hasLongSum()
|
||||
.points()
|
||||
.satisfiesExactly(
|
||||
point ->
|
||||
assertThat(point)
|
||||
.hasValue(1)
|
||||
.hasAttributes(
|
||||
Attributes.builder()
|
||||
.put("foo", "val")
|
||||
.put("bar", "val")
|
||||
.build()));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeMeterProvider_MultipleFiles() {
|
||||
ConfigProperties properties =
|
||||
withConfigFileLocations(
|
||||
"classpath:/view-config-customizer-test.yaml", "classpath:/full-config.yaml");
|
||||
|
||||
assertThatCode(
|
||||
() ->
|
||||
ViewConfigCustomizer.customizeMeterProvider(SdkMeterProvider.builder(), properties))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeMeterProvider_AbsolutePath() {
|
||||
ConfigProperties properties =
|
||||
withConfigFileLocations(
|
||||
ViewConfigTest.class.getResource("/view-config-customizer-test.yaml").getFile());
|
||||
|
||||
assertThatCode(
|
||||
() ->
|
||||
ViewConfigCustomizer.customizeMeterProvider(SdkMeterProvider.builder(), properties))
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeMeterProvider_Invalid() {
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
ViewConfigCustomizer.customizeMeterProvider(
|
||||
SdkMeterProvider.builder(),
|
||||
withConfigFileLocations("classpath:" + UUID.randomUUID())))
|
||||
.hasMessageMatching("Resource .* not found on classpath of classloader .*");
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
ViewConfigCustomizer.customizeMeterProvider(
|
||||
SdkMeterProvider.builder(), withConfigFileLocations("/" + UUID.randomUUID())))
|
||||
.hasMessageContaining("View config file not found:");
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
ViewConfigCustomizer.customizeMeterProvider(
|
||||
SdkMeterProvider.builder(),
|
||||
withConfigFileLocations("classpath:/empty-selector-config.yaml")))
|
||||
.hasMessageContaining("Failed to parse view config")
|
||||
.hasRootCauseMessage("selector is required");
|
||||
}
|
||||
|
||||
private static ConfigProperties withConfigFileLocations(String... fileLocations) {
|
||||
ConfigProperties properties = mock(ConfigProperties.class);
|
||||
when(properties.getList("otel.experimental.metrics.view.config"))
|
||||
.thenReturn(Arrays.asList(fileLocations));
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.sdk.viewconfig;
|
||||
|
||||
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.as;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import io.opentelemetry.api.common.AttributeKey;
|
||||
import io.opentelemetry.api.common.Attributes;
|
||||
import io.opentelemetry.context.Context;
|
||||
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
|
||||
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
|
||||
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
|
||||
import io.opentelemetry.sdk.metrics.common.InstrumentType;
|
||||
import io.opentelemetry.sdk.metrics.internal.view.ViewRegistryBuilder;
|
||||
import io.opentelemetry.sdk.metrics.view.Aggregation;
|
||||
import io.opentelemetry.sdk.metrics.view.InstrumentSelector;
|
||||
import io.opentelemetry.sdk.metrics.view.View;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ViewConfigTest {
|
||||
|
||||
@Test
|
||||
void registerViews_FullConfig() {
|
||||
SdkMeterProviderBuilder builder = SdkMeterProvider.builder();
|
||||
|
||||
ViewConfig.registerViews(builder, resourceFileInputStream("full-config.yaml"));
|
||||
|
||||
assertThat(builder)
|
||||
.extracting(
|
||||
"viewRegistryBuilder", as(InstanceOfAssertFactories.type(ViewRegistryBuilder.class)))
|
||||
.extracting("orderedViews", as(InstanceOfAssertFactories.list(Object.class)))
|
||||
.hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadViewConfig_FullConfig() {
|
||||
List<ViewConfigSpecification> viewConfigSpecs =
|
||||
ViewConfig.loadViewConfig(resourceFileInputStream("full-config.yaml"));
|
||||
|
||||
assertThat(viewConfigSpecs)
|
||||
.satisfiesExactly(
|
||||
viewConfigSpec -> {
|
||||
SelectorSpecification selectorSpec = viewConfigSpec.getSelectorSpecification();
|
||||
assertThat(selectorSpec.getInstrumentName()).isEqualTo("name1");
|
||||
assertThat(selectorSpec.getInstrumentType()).isEqualTo(InstrumentType.COUNTER);
|
||||
assertThat(selectorSpec.getMeterName()).isEqualTo("meterName1");
|
||||
assertThat(selectorSpec.getMeterVersion()).isEqualTo("1.0.0");
|
||||
assertThat(selectorSpec.getMeterSchemaUrl()).isEqualTo("http://example1.com");
|
||||
ViewSpecification viewSpec = viewConfigSpec.getViewSpecification();
|
||||
assertThat(viewSpec.getName()).isEqualTo("name1");
|
||||
assertThat(viewSpec.getDescription()).isEqualTo("description1");
|
||||
assertThat(viewSpec.getAggregation()).isEqualTo("sum");
|
||||
assertThat(viewSpec.getAttributeKeys()).containsExactly("foo", "bar");
|
||||
},
|
||||
viewConfigSpec -> {
|
||||
SelectorSpecification selectorSpec = viewConfigSpec.getSelectorSpecification();
|
||||
assertThat(selectorSpec.getInstrumentName()).isEqualTo("name2");
|
||||
assertThat(selectorSpec.getInstrumentType()).isEqualTo(InstrumentType.COUNTER);
|
||||
assertThat(selectorSpec.getMeterName()).isEqualTo("meterName2");
|
||||
assertThat(selectorSpec.getMeterVersion()).isEqualTo("2.0.0");
|
||||
assertThat(selectorSpec.getMeterSchemaUrl()).isEqualTo("http://example2.com");
|
||||
ViewSpecification viewSpec = viewConfigSpec.getViewSpecification();
|
||||
assertThat(viewSpec.getName()).isEqualTo("name2");
|
||||
assertThat(viewSpec.getDescription()).isEqualTo("description2");
|
||||
assertThat(viewSpec.getAggregation()).isEqualTo("last_value");
|
||||
assertThat(viewSpec.getAttributeKeys()).containsExactly("baz", "qux");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadViewConfig_Invalid() {
|
||||
assertThatThrownBy(
|
||||
() -> ViewConfig.loadViewConfig(resourceFileInputStream("empty-view-config.yaml")))
|
||||
.isInstanceOf(ConfigurationException.class)
|
||||
.hasMessageContaining("Failed to parse view config")
|
||||
.hasRootCauseMessage("view is required");
|
||||
assertThatThrownBy(
|
||||
() -> ViewConfig.loadViewConfig(resourceFileInputStream("empty-selector-config.yaml")))
|
||||
.isInstanceOf(ConfigurationException.class)
|
||||
.hasMessageContaining("Failed to parse view config")
|
||||
.hasRootCauseMessage("selector is required");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toView_Empty() {
|
||||
View view = ViewConfig.toView(ViewSpecification.builder().build());
|
||||
assertThat(view).isEqualTo(View.builder().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toView() {
|
||||
View view =
|
||||
ViewConfig.toView(
|
||||
ViewSpecification.builder()
|
||||
.name("name")
|
||||
.description("description")
|
||||
.aggregation("sum")
|
||||
.attributeKeys(Arrays.asList("foo", "bar"))
|
||||
.build());
|
||||
assertThat(view.getName()).isEqualTo("name");
|
||||
assertThat(view.getDescription()).isEqualTo("description");
|
||||
assertThat(view.getAggregation()).isEqualTo(Aggregation.sum());
|
||||
assertThat(
|
||||
view.getAttributesProcessor()
|
||||
.process(
|
||||
Attributes.builder()
|
||||
.put("foo", "val")
|
||||
.put("bar", "val")
|
||||
.put("baz", "val")
|
||||
.build(),
|
||||
Context.current()))
|
||||
.containsEntry("foo", "val")
|
||||
.containsEntry("bar", "val")
|
||||
.satisfies(
|
||||
(Consumer<Attributes>)
|
||||
attributes -> assertThat(attributes.get(AttributeKey.stringKey("baz"))).isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toAggregation() {
|
||||
assertThat(ViewConfig.toAggregation("sum")).isEqualTo(Aggregation.sum());
|
||||
assertThat(ViewConfig.toAggregation("last_value")).isEqualTo(Aggregation.lastValue());
|
||||
assertThat(ViewConfig.toAggregation("histogram")).isEqualTo(Aggregation.histogram());
|
||||
assertThat(ViewConfig.toAggregation("drop")).isEqualTo(Aggregation.drop());
|
||||
assertThatThrownBy(() -> ViewConfig.toAggregation("foo"))
|
||||
.isInstanceOf(ConfigurationException.class)
|
||||
.hasMessageContaining("Unrecognized aggregation foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInstrumentSelector_Empty() {
|
||||
InstrumentSelector selector =
|
||||
ViewConfig.toInstrumentSelector(SelectorSpecification.builder().build());
|
||||
assertThat(selector).isEqualTo(InstrumentSelector.builder().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void toInstrumentSelector() {
|
||||
InstrumentSelector selector =
|
||||
ViewConfig.toInstrumentSelector(
|
||||
SelectorSpecification.builder()
|
||||
.instrumentName("name")
|
||||
.instrumentType(InstrumentType.COUNTER)
|
||||
.meterName("meterName")
|
||||
.meterVersion("meterVersion")
|
||||
.meterSchemaUrl("http://example.com")
|
||||
.build());
|
||||
|
||||
assertThat(selector.getInstrumentNameFilter().test("name")).isTrue();
|
||||
assertThat(selector.getInstrumentNameFilter().test("name1")).isFalse();
|
||||
assertThat(selector.getInstrumentType()).isEqualTo(InstrumentType.COUNTER);
|
||||
assertThat(selector.getMeterSelector().getNameFilter().test("meterName")).isTrue();
|
||||
assertThat(selector.getMeterSelector().getNameFilter().test("meterName1")).isFalse();
|
||||
assertThat(selector.getMeterSelector().getVersionFilter().test("meterVersion")).isTrue();
|
||||
assertThat(selector.getMeterSelector().getVersionFilter().test("meterVersion1")).isFalse();
|
||||
assertThat(selector.getMeterSelector().getSchemaUrlFilter().test("http://example.com"))
|
||||
.isTrue();
|
||||
assertThat(selector.getMeterSelector().getSchemaUrlFilter().test("http://example1.com"))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
private static InputStream resourceFileInputStream(String resourceFileName) {
|
||||
URL resourceUrl = ViewConfigTest.class.getResource("/" + resourceFileName);
|
||||
if (resourceUrl == null) {
|
||||
throw new IllegalStateException("Could not find resource file: " + resourceFileName);
|
||||
}
|
||||
String path = resourceUrl.getFile();
|
||||
try {
|
||||
return new FileInputStream(path);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new IllegalStateException("File not found: " + path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
- selector:
|
||||
view:
|
||||
name: name1
|
||||
description: description1
|
||||
aggregation: sum
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
- selector:
|
||||
instrument_name: name1
|
||||
instrument_type: COUNTER
|
||||
meter_name: meterName1
|
||||
meter_version: 1.0.0
|
||||
meter_schema_url: http://example1.com
|
||||
view:
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
- selector:
|
||||
instrument_name: name1
|
||||
instrument_type: COUNTER
|
||||
meter_name: meterName1
|
||||
meter_version: 1.0.0
|
||||
meter_schema_url: http://example1.com
|
||||
view:
|
||||
name: name1
|
||||
description: description1
|
||||
aggregation: sum
|
||||
attribute_keys:
|
||||
- foo
|
||||
- bar
|
||||
- selector:
|
||||
instrument_name: name2
|
||||
instrument_type: COUNTER
|
||||
meter_name: meterName2
|
||||
meter_version: 2.0.0
|
||||
meter_schema_url: http://example2.com
|
||||
view:
|
||||
name: name2
|
||||
description: description2
|
||||
aggregation: last_value
|
||||
attribute_keys:
|
||||
- baz
|
||||
- qux
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
- selector:
|
||||
instrument_type: OBSERVABLE_COUNTER
|
||||
view:
|
||||
attribute_keys:
|
||||
- foo
|
||||
- bar
|
||||
|
|
@ -68,6 +68,7 @@ include(":sdk:trace-shaded-deps")
|
|||
include(":sdk-extensions:autoconfigure")
|
||||
include(":sdk-extensions:autoconfigure-spi")
|
||||
include(":sdk-extensions:aws")
|
||||
include(":sdk-extensions:metric-incubator")
|
||||
include(":sdk-extensions:resources")
|
||||
include(":sdk-extensions:tracing-incubator")
|
||||
include(":sdk-extensions:jaeger-remote-sampler")
|
||||
|
|
|
|||
Loading…
Reference in New Issue