io.opentelemetry.sdk.extension.incubator.fileconfig.FileConfiguration Maven / Gradle / Ivy
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.extension.incubator.fileconfig;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfiguration;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.snakeyaml.engine.v2.api.Load;
import org.snakeyaml.engine.v2.api.LoadSettings;
import org.snakeyaml.engine.v2.common.ScalarStyle;
import org.snakeyaml.engine.v2.constructor.StandardConstructor;
import org.snakeyaml.engine.v2.exceptions.ConstructorException;
import org.snakeyaml.engine.v2.exceptions.YamlEngineException;
import org.snakeyaml.engine.v2.nodes.MappingNode;
import org.snakeyaml.engine.v2.nodes.Node;
import org.snakeyaml.engine.v2.nodes.NodeTuple;
import org.snakeyaml.engine.v2.nodes.ScalarNode;
import org.snakeyaml.engine.v2.schema.CoreSchema;
/**
* Configure {@link OpenTelemetrySdk} from YAML configuration files conforming to the schema in open-telemetry/opentelemetry-configuration.
*
* @see #parseAndCreate(InputStream)
*/
public final class FileConfiguration {
private static final Logger logger = Logger.getLogger(FileConfiguration.class.getName());
private static final Pattern ENV_VARIABLE_REFERENCE =
Pattern.compile("\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)}");
private static final ObjectMapper MAPPER;
static {
MAPPER =
new ObjectMapper()
// Create empty object instances for keys which are present but have null values
.setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
// Boxed primitives which are present but have null values should be set to null, rather than
// empty instances
MAPPER.configOverride(String.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
MAPPER.configOverride(Integer.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
MAPPER.configOverride(Double.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
MAPPER.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
}
private FileConfiguration() {}
/**
* Combines {@link #parse(InputStream)} and {@link #create(OpenTelemetryConfiguration)}.
*
* @throws ConfigurationException if unable to parse or interpret
*/
public static OpenTelemetrySdk parseAndCreate(InputStream inputStream) {
OpenTelemetryConfiguration configurationModel = parse(inputStream);
return create(configurationModel);
}
/**
* Interpret the {@code configurationModel} to create {@link OpenTelemetrySdk} instance
* corresponding to the configuration.
*
* @param configurationModel the configuration model
* @return the {@link OpenTelemetrySdk}
* @throws ConfigurationException if unable to interpret
*/
public static OpenTelemetrySdk create(OpenTelemetryConfiguration configurationModel) {
List closeables = new ArrayList<>();
try {
return OpenTelemetryConfigurationFactory.getInstance()
.create(
configurationModel,
SpiHelper.create(FileConfiguration.class.getClassLoader()),
closeables);
} catch (RuntimeException e) {
logger.info(
"Error encountered interpreting configuration model. Closing partially configured components.");
for (Closeable closeable : closeables) {
try {
logger.fine("Closing " + closeable.getClass().getName());
closeable.close();
} catch (IOException ex) {
logger.warning(
"Error closing " + closeable.getClass().getName() + ": " + ex.getMessage());
}
}
if (e instanceof ConfigurationException) {
throw e;
}
throw new ConfigurationException("Unexpected configuration error", e);
}
}
/**
* Parse the {@code configuration} YAML and return the {@link OpenTelemetryConfiguration}.
*
* Before parsing, environment variable substitution is performed as described in {@link
* EnvSubstitutionConstructor}.
*
* @throws ConfigurationException if unable to parse
*/
public static OpenTelemetryConfiguration parse(InputStream configuration) {
try {
return parse(configuration, System.getenv());
} catch (RuntimeException e) {
throw new ConfigurationException("Unable to parse configuration input stream", e);
}
}
// Visible for testing
static OpenTelemetryConfiguration parse(
InputStream configuration, Map environmentVariables) {
Object yamlObj = loadYaml(configuration, environmentVariables);
return MAPPER.convertValue(yamlObj, OpenTelemetryConfiguration.class);
}
// Visible for testing
static Object loadYaml(InputStream inputStream, Map environmentVariables) {
LoadSettings settings = LoadSettings.builder().setSchema(new CoreSchema()).build();
Load yaml = new Load(settings, new EnvSubstitutionConstructor(settings, environmentVariables));
return yaml.loadFromInputStream(inputStream);
}
/**
* Convert the {@code model} to a generic {@link StructuredConfigProperties}, which can be used to
* read configuration not part of the model.
*
* @param model the configuration model
* @return a generic {@link StructuredConfigProperties} representation of the model
*/
public static StructuredConfigProperties toConfigProperties(OpenTelemetryConfiguration model) {
return toConfigProperties((Object) model);
}
static StructuredConfigProperties toConfigProperties(Object model) {
Map configurationMap =
MAPPER.convertValue(model, new TypeReference