Add a ConfigProvider callback for runtime instrumentation option changes#8076
Add a ConfigProvider callback for runtime instrumentation option changes#8076jackshirazi wants to merge 16 commits intoopen-telemetry:mainfrom
Conversation
Codecov Report❌ Patch coverage is ❌ Your patch check has failed because the patch coverage (74.72%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #8076 +/- ##
============================================
+ Coverage 90.31% 90.73% +0.41%
- Complexity 7725 7996 +271
============================================
Files 850 896 +46
Lines 23259 24189 +930
Branches 2364 2430 +66
============================================
+ Hits 21007 21948 +941
+ Misses 1528 1486 -42
- Partials 724 755 +31 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
jack-berg
left a comment
There was a problem hiding this comment.
These contracts look like a good start to me. Missing implementation corresponding spec though so need to talk about how to proceed with that.
open-telemetry/opentelemetry-specification#4899 |
|
@jack-berg I need some help here. I implemented the Also there is the issue of replacing a single value vs replacing a node subtree. It looks like I need two new APIs for that
I've implemented both in the PR for now. Examples for calling them would be and There is a difference in the effect here. If there was another key at the "include" level, eg then the |
|
|
||
| private SdkConfigProvider(DeclarativeConfigProperties openTelemetryConfigModel) { | ||
| this.instrumentationConfig = openTelemetryConfigModel.get("instrumentation/development"); | ||
| this.openTelemetryConfigModel = new AtomicReference<>(requireNonNull(openTelemetryConfigModel)); |
There was a problem hiding this comment.
Why does openTelemetryConfigModel need to be an AtomicReference?
There was a problem hiding this comment.
The config could get mutated from any thread, and indeed from multiple threads concurrently
There was a problem hiding this comment.
Would a volatile be sufficient?
There was a problem hiding this comment.
if it were a volatile, in the updateConfig() two concurrent updates could both concurrently set openTelemetryConfigModel to the new root at the same time. One of the updates would be lost. With the atomic reference in the while(true) loop, the changes are retained from all concurrent updates. So I'd say with the current implementation to avoid losing updates, no. Maybe a different implementation would allow that.
| private final AtomicReference<DeclarativeConfigProperties> openTelemetryConfigModel; | ||
| private final ConcurrentMap<String, CopyOnWriteArrayList<ListenerRegistration>> listenersByPath = | ||
| new ConcurrentHashMap<>(); | ||
| private final AtomicBoolean disposed = new AtomicBoolean(false); |
There was a problem hiding this comment.
| private final AtomicBoolean disposed = new AtomicBoolean(false); | |
| private final AtomicBoolean isShutdown = new AtomicBoolean(false); |
| Map<String, Object> subtreeMap = DeclarativeConfigProperties.toMap(newSubtree); | ||
| while (true) { | ||
| DeclarativeConfigProperties current = requireNonNull(openTelemetryConfigModel.get()); | ||
| Map<String, Object> rootMap = DeclarativeConfigProperties.toMap(current); |
There was a problem hiding this comment.
#nit: I believe setSubtreeAtPath relies on the map being mutable, and is relying on coincidence that DeclarativeConfigProperties.toMap returns a mutable hashmap.
Instead, setSubtreeAtPath could return a new hashmap instance. We can make it all efficient later if needed.
There was a problem hiding this comment.
Good point, adapted
| @SuppressWarnings("unchecked") | ||
| private static void setSubtreeAtPath( | ||
| Map<String, Object> rootMap, String normalizedPath, Map<String, Object> subtreeMap) { | ||
| String relativePath = normalizedPath.substring(1); |
There was a problem hiding this comment.
Might be worth adding an JsonPath abstraction to gather up some of this logic about how to walk paths in a single place.
There was a problem hiding this comment.
commented as a TODO, if you want me to refactor in this PR, where to?
There was a problem hiding this comment.
A TODO is fine - this is just a prototype
| Object child = parent.get(segments[i]); | ||
| if (child instanceof Map) { | ||
| parent = (Map<String, Object>) child; | ||
| } else { |
There was a problem hiding this comment.
So this is interesting. If the child is not a map, it could either be null or another type (like a scalar or a array). In the null case, I think the behavior here to makes sense. But if the type isn't a map, the we have a conflict between the current schema and the structure the updater is trying to apply. I think we ought to be conservative in this case and throw an exception. Can advertise the exception in the update method signatures, and let the caller figure out what to do if they produce a conflict.
There was a problem hiding this comment.
modified accordingly
| return current; | ||
| } | ||
|
|
||
| private static boolean hasSameContents( |
There was a problem hiding this comment.
Oof this is an expensive equality check. Maybe a TODO to optimize later. An optimized version could use the introspection APIs like getPropertyKeys to a allocation free recursive comparison.
| * @param componentLoader the component loader to use | ||
| * @return a {@link DeclarativeConfigProperties} backed by the map | ||
| */ | ||
| static DeclarativeConfigProperties fromMap( |
There was a problem hiding this comment.
I'm inclined to avoid this type of thing in the API. I accept that we need these utilities in the SDK to make UX acceptable. An SDK version of this could be based on YamlDeclarativeConfigProperties, which is has a create method to instantiate from Map<String, Object>.
There was a problem hiding this comment.
okay nice! I didn't see that. I've had to move it into the sdk from incubator (otherwise accessing it becomes silly indirection), and followed your direction here
I agree you'll need APIs for that. Should probably be at the SDK level though (SdkConfigProvider) rather than API (ConfigProvider). API is accessible to instrumentation and we want instrumentation to be consuming config, not mucking around and editing it, right? |
Couple of options here
preference? or alternative? |
3a86394 to
f45b2bb
Compare
# Conflicts: # api/incubator/src/test/java/io/opentelemetry/api/incubator/config/InstrumentationConfigUtilTest.java # sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfiguration.java # sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/YamlDeclarativeConfigProperties.java # sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/YamlDeclarativeConfigPropertiesTest.java # sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/YamlDeclarativeConfigProperties.java # sdk/all/src/main/java/io/opentelemetry/sdk/internal/YamlDeclarativeConfigProperties.java
I've sketched out a few ideas in: jackshirazi#1 Easier to talk in code these days. Most notably:
|
|
I've applied your diff as is, with one addition, an extra My concern with this implementation is how do I call the setConfig from an extension? GlobalOpenTelemetry.get() is returned as an obfuscated wrapper, so can't be cast to ExtendedOpenTelemetrySdk |
First step in adding support for runtime changes to options, focusing only on API additions.
The assumption is that if an instrumentation is able to handle changes at runtime, it will register a callback against the ConfigProvider instance that the otel SDK is using.
Further steps (to be discussed):