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:
jack-berg 2022-02-18 11:56:21 -06:00 committed by GitHub
parent 71351a26b5
commit 4a67130738
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 882 additions and 1 deletions

View File

@ -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(

View File

@ -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

View File

@ -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")
}

View File

@ -0,0 +1 @@
otel.release=alpha

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,5 @@
- selector:
view:
name: name1
description: description1
aggregation: sum

View File

@ -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:

View File

@ -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

View File

@ -0,0 +1,6 @@
- selector:
instrument_type: OBSERVABLE_COUNTER
view:
attribute_keys:
- foo
- bar

View File

@ -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")