From 8ad255438141e395f5edb86a909d2d3e1cb55b2f Mon Sep 17 00:00:00 2001 From: jackshirazi Date: Mon, 17 Nov 2025 18:17:03 +0000 Subject: [PATCH 1/4] Create OptionChangeListener.java --- .../config/OptionChangeListener.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionChangeListener.java diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionChangeListener.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionChangeListener.java new file mode 100644 index 000000000000..276392f1e3c2 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionChangeListener.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.incubator.config; + +import javax.annotation.Nullable; + +/** + * Listener interface for option value changes. + * + *

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); +} From 9b52656cb1d053210606d395015dbe3e66b17488 Mon Sep 17 00:00:00 2001 From: jackshirazi Date: Mon, 17 Nov 2025 18:18:27 +0000 Subject: [PATCH 2/4] Create OptionCallbackRegistry.java --- .../config/OptionCallbackRegistry.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionCallbackRegistry.java 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..ea249d1bc950 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionCallbackRegistry.java @@ -0,0 +1,160 @@ +/* + * 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> callbacks = new ConcurrentHashMap<>(); + + // Keep previous values so that only changes get notified + private final Map previousValues = new ConcurrentHashMap<>(); + + // MutableConfigProvider: This executor could be unnecessary if MutableConfigProvider was available, + // as we could register directly with the provider for immediate change notifications instead of polling + private final ScheduledExecutorService executor = + Executors.newSingleThreadScheduledExecutor( + r -> { + Thread t = new Thread(r, "otel-option-callback-checker"); + t.setDaemon(true); + return t; + }); + + private OptionCallbackRegistry() { + // Start periodic checking + // MutableConfigProvider: Instead of polling, register with MutableConfigProvider like: + // mutableConfigProvider.addChangeListener(this::onConfigChanged); + executor.scheduleWithFixedDelay(this::checkForChanges, 30, 30, TimeUnit.SECONDS); + } + + /** Returns the global OptionCallbackRegistry instance. */ + public static OptionCallbackRegistry getInstance() { + return INSTANCE; + } + + /** Shuts down the periodic checking executor. */ + public void shutdownPeriodicChecker() { + executor.shutdown(); + } + + /** + * Registers a listener to be invoked when the value of the specified option key changes. + * + *

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 listenerList = callbacks.get(key); + if (listenerList != null) { + // If we were using WeakReferences, this would need cleanup logic: + // listenerList.removeIf(ref -> ref.get() == null); + for (OptionChangeListener listener : listenerList) { + try { + listener.onOptionChanged(key, newValue, oldValue); + } catch (Throwable t) { + logger.info("Warning, exception thrown when trying to notify listener for key '" + key + "': " + t.getMessage()); + } + } + } + } +} From a19f768e7867c9b3a39b4d029a0b9af1cbf9735b Mon Sep 17 00:00:00 2001 From: jackshirazi Date: Mon, 17 Nov 2025 18:33:06 +0000 Subject: [PATCH 3/4] spotless --- .../api/incubator/config/OptionChangeListener.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionChangeListener.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionChangeListener.java index 276392f1e3c2..3838dd5eb8f5 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionChangeListener.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/config/OptionChangeListener.java @@ -10,8 +10,8 @@ /** * Listener interface for option value changes. * - *

This class is internal and is hence not for public use. Its APIs are unstable and can change at - * any time. + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. */ public interface OptionChangeListener { From d6415f6f4cadcf5a5ba1e751b21f082cc96d7b00 Mon Sep 17 00:00:00 2001 From: jackshirazi Date: Mon, 17 Nov 2025 18:34:16 +0000 Subject: [PATCH 4/4] spotless --- .../config/OptionCallbackRegistry.java | 110 ++++++++++-------- 1 file changed, 63 insertions(+), 47 deletions(-) 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 index ea249d1bc950..857e63d5af8b 100644 --- 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 @@ -5,7 +5,6 @@ package io.opentelemetry.instrumentation.api.incubator.config; - import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -16,24 +15,28 @@ 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 +// 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 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. + *

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 { @@ -47,8 +50,10 @@ public final class OptionCallbackRegistry { // Keep previous values so that only changes get notified private final Map previousValues = new ConcurrentHashMap<>(); - // MutableConfigProvider: This executor could be unnecessary if MutableConfigProvider was available, - // as we could register directly with the provider for immediate change notifications instead of polling + // MutableConfigProvider: This executor could be unnecessary if MutableConfigProvider was + // available, + // as we could register directly with the provider for immediate change notifications instead of + // polling private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor( r -> { @@ -75,23 +80,28 @@ public void shutdownPeriodicChecker() { } /** - * Registers a listener to be invoked when the value of the specified option key changes. - * - *

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)); + * Registers a listener to be invoked when the value of the specified option key changes. + * + *

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) { @@ -101,30 +111,32 @@ public void registerCallback(String key, String currentValue, OptionChangeListen } /** - * 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 - */ + * 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 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. + // 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() { @@ -152,7 +164,11 @@ private void notifyCallbacks(String key, @Nullable String newValue, @Nullable St try { listener.onOptionChanged(key, newValue, oldValue); } catch (Throwable t) { - logger.info("Warning, exception thrown when trying to notify listener for key '" + key + "': " + t.getMessage()); + logger.info( + "Warning, exception thrown when trying to notify listener for key '" + + key + + "': " + + t.getMessage()); } } }