Repository: helidon-io/helidon (java)

Issue:
4.x Telemetry scopes are not closed properly when using baggage
## Environment Details
* Helidon Version: 4.x
* Helidon SE or Helidon MP
* JDK version:
* OS:
* Docker version (if applicable):

----------

## Problem Description
In an app that uses OpenTelemetry tracing, adding baggage disrupts the handling of scopes.

Specifically, creating a span, activating it (which creates an autocloseable `Scope`), and then adding baggage to the span within the `try` block causes the `try-with-resources` close handling for the scope to act as a no-op.

## Steps to reproduce
You do not need to start Jaeger to receive the tracing; just ignore the runtime errors about being unable to send the tracing information.

1. Create a new SE app using `helidon init` and specify custom with tracing; take the other defaults.
2. Replace the generated `TracedService.TracedHandler#handle` method with this:
   ```
        @Override
        public void handle(ServerRequest req, ServerResponse res) {
            Tracer tracer = req.context().get(Tracer.class)
                    .orElseGet(Tracer::global);
            Span span = tracer.spanBuilder("custom-span")
                    .start();
            Span.current().ifPresentOrElse(s -> System.err.println("Current span before try-with-resources: " + s.context().spanId()),
                                           () -> System.err.println("No current span before try-with-resources"));

            try (Scope scope = span.activate()) {
                Span.current().ifPresentOrElse(s -> System.err.println("Current span in try-with-resources: " + s.context().spanId()),
                                               () -> System.err.println("No current span in try-with-resources"));
                span.addEvent("my nice log");

                res.send(message);
            } finally {
                span.end();
            }
            Span.current().ifPresentOrElse(s -> System.err.println("Current span after try-with-resources: " + s.context().spanId()),
                                           () -> System.err.println("No current span after try-with-resources"));
        }
   ```
   The changes do two things:
   1. Explicitly activate the custom span in the `try`, creating an auto-closable `Scope`.
   2. Display the current span just before, just inside, and just after the `try-with-resources` block.
3. Build and run the app and run `curl http://localhost:8080/traced`. You see the expected two different current spans at the expected times:
   ```
   Current span before try-with-resources: 268165d9440e2b8f
   Current span in try-with-resources: 1cfbe8a1c629213b
   Current span after try-with-resources: 268165d9440e2b8f
   ```
5. Add the following lines after the `span.addEvent` line in `TracedService.TracedHandler#handle`:
   ```
   span.baggage("myInfo", "myValue");
   Span.current().ifPresentOrElse(s -> System.err.println("Current span after setting baggage: " + s.context().spanId()),
                               () -> System.err.println("No current span after setting baggage"));
   ```
6. Stop, rebuild, and rerun the app and run `curl http://localhost:8080/traced` again.

The output reports the same span all three times:
```
Current span before try-with-resources: f3aa5b9b91198f31
Current span in try-with-resources: d93eaba9643f0c51
Current span after setting baggage: d93eaba9643f0c51
Current span after try-with-resources: d93eaba9643f0c51
```
The scope explicitly declared in the `try` that should be auto-closed is not.

This seems to occur because the Helidon `OpenTelemetrySpan#baggage(String, String)` method creates a new `Baggage` instance (as it should) and stores it in a new context _and makes the new context current_. This creates a new  active`Scope` but the scope is not exposed to the caller nor closed. 

Later, during the auto-close of the _explicit_ scope in the modified example code, the Helidon scope delegates the close operation to the OpenTelemetry scope which checks to see if the scope being closed is the current scope. _It is not_ in this scenario, so the OTel code ignores the attempt to close the scope. 

That is why the span ID after the try-with-resources block is unchanged once the baggage is added.

Pull request:
Manage scopes correctly with baggage; allow baggage to be mutable to honor the Helidon `Span#baggage` semantics
### Description
Resolves #8187 

See the end of the issue description for an explanation of what was going wrong. Briefly, the Helidon classes wrapping baggage and spans were not managing the scopes for each correctly.

This PR addresses two basic problems:
* the scope management of baggage and spans
* the impedance mismatch between the Helidon neutral tracing API (which permits baggage key/value pairs to be set on a `Span`) and OpenTelemetry baggage which (1) is not tied to a span and (2) is (by default) immutable once placed in the Otel context (from where other code can access it). The default Otel baggage is fully formed (immutable) before being made "current" (added to the current context) and that model does not match the neutral Helidon tracing API.

The PR solves these problems as follows:
* Adds a mutable implementation of Otel's `Baggage` interface. Each span now instantiates this class and then the `baggage` method updates that field. Updates to the baggage are allowed before and after the baggage is added to the context.
* When a span is activated, now the code first makes the baggage "current" (pushes a new context that contains the baggage), then makes the span "current" (pushes a new context that contains the span). The now-current context knows about both the span and the baggage (as well as any other settings that were present in the original context before we made the baggage current). This is just the way Otel's context works: when you make a span or a baggage instance current, that creates a new context instance that inherits all the settings from the preceding current context plus whatever new key/value pair is being set.
* The `Span.activate()` method always has returned an instance of our implementation of `Scope`. That scope now stores internally both the Otel baggage scope and the Otel span scope. The `Scope#close()` method now closes the span scope, then the baggage scope. This resolves the original problem with scope management that resulted in filing the issue.


### Documentation
Bug fix; no doc update

Patch:
diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java
index 75928efcfe..2dda8ca979 100644
--- a/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java
+++ b/builder/processor/src/main/java/io/helidon/builder/processor/TypeHandlerMap.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import io.helidon.common.processor.classmodel.Javadoc;
 import io.helidon.common.processor.classmodel.Method;
 import io.helidon.common.processor.classmodel.TypeArgument;
 import io.helidon.common.types.TypeName;
+import io.helidon.common.types.TypeNames;
 
 import static io.helidon.builder.processor.Types.STRING_TYPE;
 import static io.helidon.common.types.TypeNames.LIST;
@@ -98,12 +99,18 @@ class TypeHandlerMap extends TypeHandler {
     void generateFromConfig(Method.Builder method,
                             AnnotationDataOption configured,
                             FactoryMethods factoryMethods) {
-        method.addLine(configGet(configured)
-                               + ".asNodeList().ifPresent(nodes -> nodes.forEach"
-                               + "(node -> "
-                               + name() + ".put(node.get(\"name\").asString().orElse(node.name()), node"
-                               + generateFromConfig(factoryMethods)
-                               + ".get())));");
+        List<TypeName> typeArguments = declaredType().typeArguments();
+        if (TypeNames.STRING.equals(typeArguments.get(0)) && TypeNames.STRING.equals(typeArguments.get(1))) {
+            // the special case of Map<String, String>
+            method.addLine(configGet(configured) + ".detach().asMap().ifPresent(this::" + name() + ");");
+        } else {
+            method.addLine(configGet(configured)
+                                   + ".asNodeList().ifPresent(nodes -> nodes.forEach"
+                                   + "(node -> "
+                                   + name() + ".put(node.get(\"name\").asString().orElse(node.name()), node"
+                                   + generateFromConfig(factoryMethods)
+                                   + ".get())));");
+        }
     }
 
     @Override
diff --git a/tracing/providers/opentelemetry/pom.xml b/tracing/providers/opentelemetry/pom.xml
index 66c645892d..8fd6acc494 100644
--- a/tracing/providers/opentelemetry/pom.xml
+++ b/tracing/providers/opentelemetry/pom.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
-    Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+    Copyright (c) 2022, 2024 Oracle and/or its affiliates.
 
     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
@@ -58,7 +58,26 @@
             <scope>provided</scope>
             <optional>true</optional>
         </dependency>
-
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-sdk</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.opentelemetry</groupId>
+            <artifactId>opentelemetry-exporter-otlp</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.helidon.common.testing</groupId>
+            <artifactId>helidon-common-testing-junit5</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/MutableOpenTelemetryBaggage.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/MutableOpenTelemetryBaggage.java
new file mode 100644
index 0000000000..6bcfafab46
--- /dev/null
+++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/MutableOpenTelemetryBaggage.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.helidon.tracing.providers.opentelemetry;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+import io.opentelemetry.api.baggage.Baggage;
+import io.opentelemetry.api.baggage.BaggageBuilder;
+import io.opentelemetry.api.baggage.BaggageEntry;
+import io.opentelemetry.api.baggage.BaggageEntryMetadata;
+
+class MutableOpenTelemetryBaggage implements Baggage {
+
+    private final Map<String, BaggageEntry> values = new LinkedHashMap<>();
+
+    MutableOpenTelemetryBaggage() {
+    }
+
+    private MutableOpenTelemetryBaggage(Builder builder) {
+        values.putAll(builder.values);
+    }
+
+    @Override
+    public int size() {
+        return values.size();
+    }
+
+    @Override
+    public void forEach(BiConsumer<? super String, ? super BaggageEntry> consumer) {
+        values.forEach(consumer);
+    }
+
+    @Override
+    public Map<String, BaggageEntry> asMap() {
+        return Collections.unmodifiableMap(values);
+    }
+
+    @Override
+    public String getEntryValue(String entryKey) {
+        BaggageEntry entry = values.get(entryKey);
+        return entry != null ? entry.getValue() : null;
+    }
+
+    @Override
+    public BaggageBuilder toBuilder() {
+        return new Builder(values);
+    }
+
+    void baggage(String key, String value) {
+        values.put(key, new HBaggageEntry(value, new HBaggageEntryMetadata("")));
+    }
+
+    static class HBaggageEntry implements BaggageEntry {
+
+        private final String value;
+        private final BaggageEntryMetadata metadata;
+
+        HBaggageEntry(String value, BaggageEntryMetadata metadata) {
+            this.value = value;
+            this.metadata = metadata;
+        }
+
+        @Override
+        public String getValue() {
+            return value;
+        }
+
+        @Override
+        public BaggageEntryMetadata getMetadata() {
+            return metadata;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof HBaggageEntry that)) {
+                return false;
+            }
+            return Objects.equals(value, that.value) && Objects.equals(metadata, that.metadata);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(value, metadata);
+        }
+    }
+
+    static class Builder implements BaggageBuilder {
+
+        private final Map<String, BaggageEntry> values = new HashMap<>();
+
+        private Builder(Map<String, BaggageEntry> values) {
+            this.values.putAll(values);
+        }
+
+        @Override
+        public BaggageBuilder put(String key, String value, BaggageEntryMetadata entryMetadata) {
+            values.put(key, new HBaggageEntry(value, entryMetadata));
+            return this;
+        }
+
+        @Override
+        public BaggageBuilder remove(String key) {
+            values.remove(key);
+            return this;
+        }
+
+        @Override
+        public Baggage build() {
+            return new MutableOpenTelemetryBaggage(this);
+        }
+    }
+
+    static class HBaggageEntryMetadata implements BaggageEntryMetadata {
+
+        private final String metadata;
+
+        HBaggageEntryMetadata(String metadata) {
+            this.metadata = metadata;
+        }
+        @Override
+        public String getValue() {
+            return metadata;
+        }
+    }
+}
diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryScope.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryScope.java
index edd7aa13d0..89c5e4d620 100644
--- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryScope.java
+++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryScope.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -21,16 +21,21 @@ import io.helidon.tracing.Scope;
 
 class OpenTelemetryScope implements Scope {
     private final io.opentelemetry.context.Scope delegate;
+    private final io.opentelemetry.context.Scope baggageScope;
     private final AtomicBoolean closed = new AtomicBoolean();
 
-    OpenTelemetryScope(io.opentelemetry.context.Scope scope) {
+    OpenTelemetryScope(io.opentelemetry.context.Scope scope, io.opentelemetry.context.Scope baggageScope) {
         delegate = scope;
+        this.baggageScope = baggageScope;
     }
 
     @Override
     public void close() {
         if (closed.compareAndSet(false, true) && delegate != null) {
             delegate.close();
+            if (baggageScope != null) {
+                baggageScope.close();
+            }
         }
     }
 
diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetrySpan.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetrySpan.java
index 3fc6207ba6..3cae2355b1 100644
--- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetrySpan.java
+++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetrySpan.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -24,7 +24,6 @@ import io.helidon.tracing.Scope;
 import io.helidon.tracing.Span;
 import io.helidon.tracing.SpanContext;
 
-import io.opentelemetry.api.baggage.Baggage;
 import io.opentelemetry.api.common.Attributes;
 import io.opentelemetry.api.common.AttributesBuilder;
 import io.opentelemetry.api.trace.StatusCode;
@@ -32,6 +31,7 @@ import io.opentelemetry.context.Context;
 
 class OpenTelemetrySpan implements Span {
     private final io.opentelemetry.api.trace.Span delegate;
+    private final MutableOpenTelemetryBaggage baggage = new MutableOpenTelemetryBaggage();
 
     OpenTelemetrySpan(io.opentelemetry.api.trace.Span span) {
         this.delegate = span;
@@ -93,26 +93,22 @@ class OpenTelemetrySpan implements Span {
 
     @Override
     public Scope activate() {
-        return new OpenTelemetryScope(delegate.makeCurrent());
+        io.opentelemetry.context.Scope baggageScope = baggage.makeCurrent();
+        return new OpenTelemetryScope(delegate.makeCurrent(), baggageScope);
     }
 
     @Override
     public Span baggage(String key, String value) {
-        Objects.requireNonNull(key, "Baggage Key cannot be null");
-        Objects.requireNonNull(value, "Baggage Value cannot be null");
-
-        Baggage.builder()
-                .put(key, value)
-                .build()
-                .storeInContext(getContext())
-                .makeCurrent();
+        Objects.requireNonNull(key, "baggage key cannot be null");
+        Objects.requireNonNull(value, "baggage value cannot be null");
+        baggage.baggage(key, value);
         return this;
     }
 
     @Override
     public Optional<String> baggage(String key) {
         Objects.requireNonNull(key, "Baggage Key cannot be null");
-        return Optional.ofNullable(Baggage.fromContext(getContext()).getEntryValue(key));
+        return Optional.ofNullable(baggage.getEntryValue(key));
     }
 
     // Check if OTEL Context is already available in Global Helidon Context.


Test patch:
diff --git a/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ConfigMapBlueprint.java b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ConfigMapBlueprint.java
new file mode 100644
index 0000000000..b9af63c359
--- /dev/null
+++ b/builder/tests/builder/src/main/java/io/helidon/builder/test/testsubjects/ConfigMapBlueprint.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.builder.test.testsubjects;
+
+import java.util.Map;
+
+import io.helidon.builder.api.Option;
+import io.helidon.builder.api.Prototype;
+
+@Prototype.Configured
+@Prototype.Blueprint
+interface ConfigMapBlueprint {
+    @Option.Configured
+    @Option.Singular
+    Map<String, String> properties();
+}
diff --git a/builder/tests/builder/src/test/java/io/helidon/builder/test/ConfigMapTest.java b/builder/tests/builder/src/test/java/io/helidon/builder/test/ConfigMapTest.java
new file mode 100644
index 0000000000..daa57f3a4e
--- /dev/null
+++ b/builder/tests/builder/src/test/java/io/helidon/builder/test/ConfigMapTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.builder.test;
+
+import java.util.Map;
+
+import io.helidon.builder.test.testsubjects.ConfigMap;
+import io.helidon.config.Config;
+import io.helidon.config.ConfigSources;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasEntry;
+
+public class ConfigMapTest {
+    private static Config config;
+
+    @BeforeAll
+    static void beforeAll() {
+        config = Config.just(ConfigSources.classpath("config-map-test.yaml"));
+    }
+
+    @Test
+    void testMap() {
+        ConfigMap configMap = ConfigMap.create(config);
+        Map<String, String> properties = configMap.properties();
+
+        assertThat(properties, hasEntry("my.first.key", "firstValue"));
+        assertThat(properties, hasEntry("my.second.key", "secondValue"));
+    }
+}
diff --git a/builder/tests/builder/src/test/resources/config-map-test.yaml b/builder/tests/builder/src/test/resources/config-map-test.yaml
new file mode 100644
index 0000000000..1b16c2103a
--- /dev/null
+++ b/builder/tests/builder/src/test/resources/config-map-test.yaml
@@ -0,0 +1,21 @@
+#
+# Copyright (c) 2024 Oracle and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Used in ConfigMapTest
+properties:
+  my.first.key: "firstValue"
+  my.second.key: "secondValue"
+
diff --git a/tracing/providers/opentelemetry/src/test/java/io/helidon/tracing/providers/opentelemetry/TestSpanAndBaggage.java b/tracing/providers/opentelemetry/src/test/java/io/helidon/tracing/providers/opentelemetry/TestSpanAndBaggage.java
new file mode 100644
index 0000000000..796daa2f56
--- /dev/null
+++ b/tracing/providers/opentelemetry/src/test/java/io/helidon/tracing/providers/opentelemetry/TestSpanAndBaggage.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.helidon.tracing.providers.opentelemetry;
+
+import java.util.Optional;
+
+import io.helidon.common.testing.junit5.OptionalMatcher;
+import io.helidon.tracing.Scope;
+import io.helidon.tracing.Span;
+import io.helidon.tracing.Tracer;
+
+import io.opentelemetry.context.Context;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+class TestSpanAndBaggage {
+
+    private static final String OTEL_AUTO_CONFIGURE_PROP = "otel.java.global-autoconfigure.enabled";
+    private static final String OTEL_SDK_DISABLED_PROP = "otel.sdk.disabled";
+    private static String originalOtelSdkAutoConfiguredSetting;
+    private static String originalOtelSdkDisabledSetting;
+
+    @BeforeAll
+    static void init() {
+        originalOtelSdkAutoConfiguredSetting = System.setProperty(OTEL_AUTO_CONFIGURE_PROP, "true");
+        originalOtelSdkDisabledSetting = System.setProperty(OTEL_SDK_DISABLED_PROP, "true");
+    }
+
+    @AfterAll
+    static void wrapup() {
+        if (originalOtelSdkAutoConfiguredSetting != null) {
+            System.setProperty(OTEL_AUTO_CONFIGURE_PROP, originalOtelSdkAutoConfiguredSetting);
+        }
+        if (originalOtelSdkDisabledSetting != null) {
+            System.setProperty(OTEL_SDK_DISABLED_PROP, originalOtelSdkDisabledSetting);
+        }
+    }
+
+    @Test
+    void testActiveSpanScopeWithoutBaggage() {
+        Tracer tracer = Tracer.global();
+        Span span = tracer.spanBuilder("myParent")
+                .start();
+
+        // Make sure we get valid spans, not no-op ones all of which have zeros for span IDs.
+        assertThat("Span ID", span.context().spanId(), not(containsString("00000000")));
+
+        try (Scope scope = span.activate()) {
+            Optional<Span> spanInsideActivation = Span.current();
+            assertThat("Current span while activated", spanInsideActivation, OptionalMatcher.optionalPresent());
+            assertThat("Current span while activated",
+                       spanInsideActivation.get().context().spanId(),
+                       is(span.context().spanId()));
+            span.end();
+        } catch (Exception e) {
+            span.end(e);
+        }
+    }
+
+    @Test
+    void testActiveSpanScopeWithBaggage() {
+        // Make sure accessing baggage after span activation does not disrupt the scopes.
+
+        // otel.java.global-autoconfigure.enabled
+
+        Tracer tracer = Tracer.global();
+        Span outerSpan = tracer.spanBuilder("outer").start();
+
+        try (Scope outerScope = outerSpan.activate()) {
+
+            Optional<Span> currentJustAfterActivation = Span.current();
+            assertThat("Current span just after activation",
+                       currentJustAfterActivation,
+                       OptionalMatcher.optionalPresent());
+            assertThat("Current span just after activation",
+                       currentJustAfterActivation.get().context().spanId(),
+                       is(outerSpan.context().spanId()));
+
+            outerSpan.baggage("myItem", "myValue");
+            outerSpan.end();
+        } catch (Exception e) {
+            outerSpan.end(e);
+        }
+
+        // There was no active span before outerSpan was activated, so expect the "default" ad-hoc span ID of all zeroes.
+        Optional<Span> currentSpanAfterTryResourcesBlock = Span.current();
+        assertThat("Current span just after try-resources block",
+                   currentSpanAfterTryResourcesBlock,
+                   OptionalMatcher.optionalPresent());
+        assertThat("Current span just after try-resources block",
+                   currentSpanAfterTryResourcesBlock.get().context().spanId(),
+                   containsString("00000000"));
+    }
+}