Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GlobalOpenTelemetry trigger of autoconfiguration is opt-in #5010

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/all/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ dependencies {
testImplementation("edu.berkeley.cs.jqf:jqf-fuzz")
testImplementation("com.google.guava:guava-testlib")
}

tasks.test {
// Configure environment variable for ConfigUtilTest
environment("CONFIG_KEY", "environment")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.api;

import io.opentelemetry.api.internal.ConfigUtil;
import io.opentelemetry.api.internal.GuardedBy;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.MeterBuilder;
Expand Down Expand Up @@ -44,6 +45,9 @@
@SuppressWarnings("StaticAssignmentOfThrowable")
public final class GlobalOpenTelemetry {

private static final String GLOBAL_AUTOCONFIGURE_ENABLED_PROPERTY =
"otel.java.global-autoconfigure.enabled";

private static final Logger logger = Logger.getLogger(GlobalOpenTelemetry.class.getName());

private static final Object mutex = new Object();
Expand Down Expand Up @@ -219,15 +223,29 @@ private static OpenTelemetry maybeAutoConfigureAndSetGlobal() {
return null;
}

// If autoconfigure module is present but global autoconfigure disabled log a warning and return
boolean globalAutoconfigureEnabled =
Boolean.parseBoolean(ConfigUtil.getString(GLOBAL_AUTOCONFIGURE_ENABLED_PROPERTY));
if (!globalAutoconfigureEnabled) {
logger.log(
Level.INFO,
"AutoConfiguredOpenTelemetrySdk found on classpath but automatic configuration is disabled."
+ " To enable, run your JVM with -D"
+ GLOBAL_AUTOCONFIGURE_ENABLED_PROPERTY
+ "=true");
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is null the right response here, rather than the no-op implementation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the caller of this...I think we might be doing the wrong thing right now. If this method returns null, we're calling set with the no-op impl. That will mean (I think?) that we will always end up returning the no-op, because of the check in the set method. I think this is wrong because we do want people to be able to set after calling a uninitialized get.

Copy link
Member Author

@jack-berg jack-berg Dec 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will mean (I think?) that we will always end up returning the no-op, because of the check in the set method.

That is correct. Here's a summary of the behavior as of right now on main:

  • If GlobalOpenTelemetry.get() is called before GlobalOpenTelemetry.set(), an OpenTelemetry instance is resolved and set using GlobalOpenTelemetry.set().
  • If autoconfigure is on the classpath and autoconfigure initializes without error, GlobalOpenTelemetry will be set to an autoconfigured instance of OpenTelemetrySdk.
  • If autoconfigure is not on the classpath, or some error occurs during initialization, GlobalOpenTelemetry will be set to OpenTelemetry.noop().

I think this is wrong because we do want people to be able to set after calling a uninitialized get.

I'm not so sure. The current behavior is inconvenient because it forces users to either call GlobalOpenTelemetry.set OR have autoconfiguration do so before any call to GlobalOpenTelemetry.get. Trying to call GlobalOpenTelemetry.set later triggers an exception which can only be resolved by adjusting app initialization ordering. But the benefit of this is that GlobalOpenTelemetry.get always returns the same value, regardless of the order in which its called. I.e. you can't have one caller receive a OpenTelemetry.noop() and another caller receive an autoconfigured OpenTelemetrySdk. This is a nice invariant to be able to rely on.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. I can see that side of things as well (wanting in invariant). I guess it boils down to: in the case of incorrect initialization ordering, do you want a) nothing to be emitted from the SDK at all or b) at least some things emitted, from the instruments/tracers/etc that are initialized after the SDK initialization has taken place.

I don't know that there's an easy answer to this one. 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup it's a good question and is kind of a design philosophy question between fail fast or allow partial success.

I'm prefer to the fail fast because I think the absence of all telemetry provides a really strong signal to go and fix the issue.

If we do want to change this behavior should probably do it in a different PR to separate concerns.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'll be able to make sure no one will call get before I create the OpenTelemetry object.
I will need to doublecheck this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'll be able to make sure no one will call get before I create the OpenTelemetry object.

You don't need to. If you rely on GlobalOpenTelemetry.get() calling AutoConfiguredOpenTelemetrySdk.initialize(), you will just need to opt in to that behavior with an environment variable or system property.

The current behavior should still be possible, but it shouldn't come at the expense of other scenarios that don't want the side affect. It makes sense to the AutoConfiguredOpenTelemetrySdk.initialize() side affect opt-in rather than opt-out because autoconfigure users should be comfortable adding environment variables / system properties so it shouldn't be a problem be a big lift to add one more.

}

try {
Method initialize = openTelemetrySdkAutoConfiguration.getMethod("initialize");
Object autoConfiguredSdk = initialize.invoke(null);
Method getOpenTelemetrySdk =
openTelemetrySdkAutoConfiguration.getMethod("getOpenTelemetrySdk");
return (OpenTelemetry) getOpenTelemetrySdk.invoke(autoConfiguredSdk);
return new ObfuscatedOpenTelemetry(
(OpenTelemetry) getOpenTelemetrySdk.invoke(autoConfiguredSdk));
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new IllegalStateException(
"OpenTelemetrySdkAutoConfiguration detected on classpath "
"AutoConfiguredOpenTelemetrySdk detected on classpath "
+ "but could not invoke initialize method. This is a bug in OpenTelemetry.",
e);
} catch (InvocationTargetException t) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.internal;

import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;

/**
* Configuration utilities.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public final class ConfigUtil {

private ConfigUtil() {}

/**
* Return the system property or environment variable for the {@code key}.
*
* <p>Normalize the {@code key} using {@link #normalizePropertyKey(String)}. Match to system
* property keys also normalized with {@link #normalizePropertyKey(String)}. Match to environment
* variable keys normalized with {@link #normalizeEnvironmentVariableKey(String)}. System
* properties take priority over environment variables.
*
* @param key the property key
* @return the system property if not null, or the environment variable if not null, or null
*/
@Nullable
public static String getString(String key) {
String normalizedKey = normalizePropertyKey(key);
String systemProperty =
System.getProperties().entrySet().stream()
.filter(entry -> normalizedKey.equals(normalizePropertyKey(entry.getKey().toString())))
.map(entry -> entry.getValue().toString())
.findFirst()
.orElse(null);
if (systemProperty != null) {
return systemProperty;
}
return System.getenv().entrySet().stream()
.filter(entry -> normalizedKey.equals(normalizeEnvironmentVariableKey(entry.getKey())))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);
}

/**
* Normalize an environment variable key by converting to lower case and replacing "_" with ".".
*/
public static String normalizeEnvironmentVariableKey(String key) {
return key.toLowerCase(Locale.ROOT).replace("_", ".");
}

/** Normalize a property key by converting to lower case and replacing "-" with ".". */
public static String normalizePropertyKey(String key) {
return key.toLowerCase(Locale.ROOT).replace("-", ".");
}

/** Returns defaultValue if value is null, otherwise value. This is an internal method. */
public static <T> T defaultIfNull(@Nullable T value, T defaultValue) {
return value == null ? defaultValue : value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.internal;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.SetSystemProperty;

/** Relies on environment configuration in {@code ./api/all/build.gradle.kts}. */
class ConfigUtilTest {

@Test
@SetSystemProperty(key = "config.key", value = "system")
void getString_SystemPropertyPriority() {
assertThat(ConfigUtil.getString("config.key")).isEqualTo("system");
assertThat(ConfigUtil.getString("config-key")).isEqualTo("system");
assertThat(ConfigUtil.getString("other.config.key")).isEqualTo(null);
}

@Test
@SetSystemProperty(key = "CONFIG-KEY", value = "system")
void getString_SystemPropertyNormalized() {
assertThat(ConfigUtil.getString("config.key")).isEqualTo("system");
assertThat(ConfigUtil.getString("config-key")).isEqualTo("system");
assertThat(ConfigUtil.getString("other.config.key")).isEqualTo(null);
}

@Test
void getString_EnvironmentVariable() {
assertThat(ConfigUtil.getString("config.key")).isEqualTo("environment");
assertThat(ConfigUtil.getString("other.config.key")).isEqualTo(null);
}

@Test
void normalizeEnvironmentVariable() {
assertThat(ConfigUtil.normalizeEnvironmentVariableKey("CONFIG_KEY")).isEqualTo("config.key");
assertThat(ConfigUtil.normalizeEnvironmentVariableKey("config_key")).isEqualTo("config.key");
assertThat(ConfigUtil.normalizeEnvironmentVariableKey("config-key")).isEqualTo("config-key");
assertThat(ConfigUtil.normalizeEnvironmentVariableKey("configkey")).isEqualTo("configkey");
}

@Test
void normalizePropertyKey() {
assertThat(ConfigUtil.normalizePropertyKey("CONFIG_KEY")).isEqualTo("config_key");
assertThat(ConfigUtil.normalizePropertyKey("CONFIG.KEY")).isEqualTo("config.key");
assertThat(ConfigUtil.normalizePropertyKey("config-key")).isEqualTo("config.key");
assertThat(ConfigUtil.normalizePropertyKey("configkey")).isEqualTo("configkey");
}

@Test
void defaultIfnull() {
assertThat(ConfigUtil.defaultIfNull("val1", "val2")).isEqualTo("val1");
assertThat(ConfigUtil.defaultIfNull(null, "val2")).isEqualTo("val2");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

package io.opentelemetry.sdk.autoconfigure.spi;

import static io.opentelemetry.sdk.autoconfigure.spi.ConfigUtil.defaultIfNull;
import static io.opentelemetry.api.internal.ConfigUtil.defaultIfNull;

import java.time.Duration;
import java.util.List;
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;

import io.opentelemetry.api.internal.ConfigUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import java.time.Duration;
Expand All @@ -18,7 +19,6 @@
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
Expand All @@ -43,7 +43,7 @@ public final class DefaultConfigProperties implements ConfigProperties {
* and the {@code defaultProperties}.
*
* <p>Environment variables take priority over {@code defaultProperties}. System properties take
* priority over system properties.
* priority over environment variables.
*/
public static DefaultConfigProperties create(Map<String, String> defaultProperties) {
return new DefaultConfigProperties(System.getProperties(), System.getenv(), defaultProperties);
Expand All @@ -62,11 +62,13 @@ private DefaultConfigProperties(
Map<String, String> environmentVariables,
Map<String, String> defaultProperties) {
Map<String, String> config = new HashMap<>();
defaultProperties.forEach((name, value) -> config.put(normalize(name), value));
defaultProperties.forEach(
(name, value) -> config.put(ConfigUtil.normalizePropertyKey(name), value));
environmentVariables.forEach(
(name, value) -> config.put(name.toLowerCase(Locale.ROOT).replace('_', '.'), value));
(name, value) -> config.put(ConfigUtil.normalizeEnvironmentVariableKey(name), value));
systemProperties.forEach(
(key, value) -> config.put(normalize(key.toString()), value.toString()));
(key, value) ->
config.put(ConfigUtil.normalizePropertyKey(key.toString()), value.toString()));

this.config = config;
}
Expand All @@ -75,21 +77,21 @@ private DefaultConfigProperties(
DefaultConfigProperties previousProperties, Map<String, String> overrides) {
// previousProperties are already normalized, they can be copied as they are
Map<String, String> config = new HashMap<>(previousProperties.config);
overrides.forEach((name, value) -> config.put(normalize(name), value));
overrides.forEach((name, value) -> config.put(ConfigUtil.normalizePropertyKey(name), value));

this.config = config;
}

@Override
@Nullable
public String getString(String name) {
return config.get(normalize(name));
return config.get(ConfigUtil.normalizePropertyKey(name));
}

@Override
@Nullable
public Boolean getBoolean(String name) {
String value = config.get(normalize(name));
String value = config.get(ConfigUtil.normalizePropertyKey(name));
if (value == null || value.isEmpty()) {
return null;
}
Expand All @@ -100,7 +102,7 @@ public Boolean getBoolean(String name) {
@Nullable
@SuppressWarnings("UnusedException")
public Integer getInt(String name) {
String value = config.get(normalize(name));
String value = config.get(ConfigUtil.normalizePropertyKey(name));
if (value == null || value.isEmpty()) {
return null;
}
Expand All @@ -115,7 +117,7 @@ public Integer getInt(String name) {
@Nullable
@SuppressWarnings("UnusedException")
public Long getLong(String name) {
String value = config.get(normalize(name));
String value = config.get(ConfigUtil.normalizePropertyKey(name));
if (value == null || value.isEmpty()) {
return null;
}
Expand All @@ -130,7 +132,7 @@ public Long getLong(String name) {
@Nullable
@SuppressWarnings("UnusedException")
public Double getDouble(String name) {
String value = config.get(normalize(name));
String value = config.get(ConfigUtil.normalizePropertyKey(name));
if (value == null || value.isEmpty()) {
return null;
}
Expand All @@ -145,7 +147,7 @@ public Double getDouble(String name) {
@Nullable
@SuppressWarnings("UnusedException")
public Duration getDuration(String name) {
String value = config.get(normalize(name));
String value = config.get(ConfigUtil.normalizePropertyKey(name));
if (value == null || value.isEmpty()) {
return null;
}
Expand Down Expand Up @@ -174,7 +176,7 @@ public Duration getDuration(String name) {

@Override
public List<String> getList(String name) {
String value = config.get(normalize(name));
String value = config.get(ConfigUtil.normalizePropertyKey(name));
if (value == null) {
return Collections.emptyList();
}
Expand All @@ -188,7 +190,7 @@ public List<String> getList(String name) {
* @throws ConfigurationException if {@code name} contains duplicate entries
*/
public static Set<String> getSet(ConfigProperties config, String name) {
List<String> list = config.getList(normalize(name));
List<String> list = config.getList(ConfigUtil.normalizePropertyKey(name));
Set<String> set = new HashSet<>(list);
if (set.size() != list.size()) {
String duplicates =
Expand All @@ -206,7 +208,7 @@ public static Set<String> getSet(ConfigProperties config, String name) {

@Override
public Map<String, String> getMap(String name) {
return getList(normalize(name)).stream()
return getList(ConfigUtil.normalizePropertyKey(name)).stream()
.map(keyValuePair -> filterBlanksAndNulls(keyValuePair.split("=", 2)))
.map(
splitKeyValuePairs -> {
Expand Down Expand Up @@ -281,8 +283,4 @@ private static String getUnitString(String rawValue) {
// Pull everything after the last digit.
return rawValue.substring(lastDigitIndex + 1);
}

private static String normalize(String propertyName) {
return propertyName.toLowerCase(Locale.ROOT).replace('-', '.');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ void invalidSampler() {
@Test
@SetSystemProperty(key = "otel.traces.sampler", value = "traceidratio")
@SetSystemProperty(key = "otel.traces.sampler.arg", value = "bar")
@SetSystemProperty(key = "otel.java.global-autoconfigure.enabled", value = "true")
@SuppressLogger(GlobalOpenTelemetry.class)
void globalOpenTelemetryWhenError() {
assertThat(GlobalOpenTelemetry.get())
Expand Down
Loading