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.brave:brave-bom:5.13.7",
|
||||||
"io.zipkin.reporter2:zipkin-reporter-bom:2.16.3",
|
"io.zipkin.reporter2:zipkin-reporter-bom:2.16.3",
|
||||||
"org.junit:junit-bom:5.8.2",
|
"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(
|
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")
|
||||||
include(":sdk-extensions:autoconfigure-spi")
|
include(":sdk-extensions:autoconfigure-spi")
|
||||||
include(":sdk-extensions:aws")
|
include(":sdk-extensions:aws")
|
||||||
|
include(":sdk-extensions:metric-incubator")
|
||||||
include(":sdk-extensions:resources")
|
include(":sdk-extensions:resources")
|
||||||
include(":sdk-extensions:tracing-incubator")
|
include(":sdk-extensions:tracing-incubator")
|
||||||
include(":sdk-extensions:jaeger-remote-sampler")
|
include(":sdk-extensions:jaeger-remote-sampler")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue