diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionCallbackRegistry.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionCallbackRegistry.java new file mode 100644 index 000000000000..857e63d5af8b --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionCallbackRegistry.java @@ -0,0 +1,176 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.config; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +// MutableConfigProvider: If MutableConfigProvider was supported, this registry could be enhanced +// to: +// - Register as a listener with the MutableConfigProvider to receive immediate notifications on +// config changes +// - Eliminate the need for periodic polling (executor) and instead react to provider-driven change +// events +// - Support dynamic config updates without the 30-second polling delay + +/** + * Registry for callbacks that are invoked when configuration option values change. + * + *
This singleton can be loaded by multiple classloaders, creating separate instances that each + * monitor the globally consistent System properties. For instances that don't need periodic + * checking (e.g., only used for {@link #updateOption(String, String)}), {@link + * #shutdownPeriodicChecker()} can be called to stop the background thread. Typically this would + * mean the extension that loads this class to use {@link #updateOption(String, String)}) and no + * other capability of this class, should shutdown the periodic checker during the extension + * initialization task. If this is not done, there are no adverse effects other than the additional + * very low overhead thread. + * + *
This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public final class OptionCallbackRegistry {
+
+ private static final Logger logger = Logger.getLogger(OptionCallbackRegistry.class.getName());
+
+ private static final OptionCallbackRegistry INSTANCE = new OptionCallbackRegistry();
+
+ // Allow multiple registrations on any key
+ private final Map The listener receives the key and the new value. If the value becomes null, the listener is
+ * still invoked.
+ *
+ * MutableConfigProvider: With MutableConfigProvider, this method would register the listener
+ * directly with the provider for the specific key, ensuring immediate notification of changes
+ * without relying on polling.
+ *
+ * @param key the configuration key to monitor
+ * @param currentValue the current value of the key
+ * @param listener the listener to invoke on change
+ * @param isDeclarative if true, the currentValue is passed into {@link #updateOption(String,
+ * String)}. Where the config is declarative, this should be true, otherwise this should be
+ * false
+ */
+ public void registerCallback(
+ String key, String currentValue, OptionChangeListener listener, boolean isDeclarative) {
+ // If instrumentations could be unloaded, we would wrap listeners in a WeakReference to prevent
+ // memory leaks:
+ // callbacks.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(new
+ // WeakReference<>(listener));
+
+ callbacks.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(listener);
+ if (isDeclarative) {
+ updateOption(key, currentValue);
+ }
+ previousValues.put(key, currentValue);
+ }
+
+ /**
+ * Updates the value of an option. Registered callbacks will be notified asynchronously when the
+ * periodic check detects the change.
+ *
+ * MutableConfigProvider: If MutableConfigProvider was available, this method would be replaced
+ * by provider-driven updates. The MutableConfigProvider could notify this registry directly when
+ * configurations change, eliminating the need for manual updates via this method. Alternatively
+ * or additionally, this method could remain the primary runtime update mechanism, handling the
+ * required updates to MutableConfigProvider.
+ *
+ * @param key the option key
+ * @param value the new value, or {@code null} to remove the option
+ */
+ public void updateOption(String key, String value) {
+ if (value != null) {
+ // System.setProperty is thread-safe and provides consistent writes so no need to synchronize
+ System.setProperty(key, value);
+ } else {
+ // Remove the property if value is null
+ // System.clearProperty is thread-safe and provides consistent writes so no need to
+ // synchronize
+ System.clearProperty(key);
+ }
+ }
+
+ // MutableConfigProvider: This polling method could be replaced by direct callback from
+ // MutableConfigProvider.
+ // The provider could call a method like onConfigChanged(String key, String newValue) directly,
+ // eliminating the need to check all keys periodically and providing immediate updates.
+ private void checkForChanges() {
+ for (String key : callbacks.keySet()) {
+ String currentValue = getCurrentValue(key);
+ String previousValue = previousValues.get(key);
+ if (!java.util.Objects.equals(currentValue, previousValue)) {
+ previousValues.put(key, currentValue);
+ notifyCallbacks(key, currentValue, previousValue);
+ }
+ }
+ }
+
+ private static String getCurrentValue(String key) {
+ // System.getProperty is thread-safe and provides consistent reads so no need to synchronize
+ return System.getProperty(key);
+ }
+
+ private void notifyCallbacks(String key, @Nullable String newValue, @Nullable String oldValue) {
+ List This class is internal and is hence not for public use. Its APIs are unstable and can change
+ * at any time.
+ */
+public interface OptionChangeListener {
+
+ /**
+ * Called when the value of an option changes.
+ *
+ * @param key the option key
+ * @param newValue the new value, or {@code null} if the option was removed
+ * @param oldValue the previous value, or {@code null} if the option was not set
+ */
+ void onOptionChanged(String key, @Nullable String newValue, @Nullable String oldValue);
+}